From 964635ca136174f12445286d23d4cb434437bb4d Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 26 May 2023 15:42:51 +0200 Subject: [PATCH 01/27] Added go mod --- .gitignore | 12 ++++++------ go.mod | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 go.mod diff --git a/.gitignore b/.gitignore index 3b735ec..84285ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# # Binaries for programs and plugins *.exe *.exe~ @@ -15,7 +12,10 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ -# Go workspace file -go.work +# Binaries +bin/ + +# Packaged Helm charts +*.tgz \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b4a41c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/mariadb-operator/agent + +go 1.20 From e330eca330128a85de20290400635407d4625db2 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 26 May 2023 17:07:44 +0200 Subject: [PATCH 02/27] Initial scaffolding --- .dockerignore | 4 ++ .github/ISSUE_TEMPLATE/bug.md | 38 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 25 +++++++ .github/workflows/ci.yml | 85 +++++++++++++++++++++++ .github/workflows/release.yml | 77 ++++++++++++++++++++ .golangci.yml | 31 +++++++++ .goreleaser.yml | 14 ++++ Dockerfile | 21 ++++++ Makefile | 22 ++++++ cmd/agent/root.go | 21 ++++++ go.mod | 7 ++ go.sum | 10 +++ main.go | 7 ++ make/deploy.mk | 26 +++++++ make/dev.mk | 26 +++++++ make/tooling.mk | 24 +++++++ 16 files changed, 438 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/agent/root.go create mode 100644 go.sum create mode 100644 main.go create mode 100644 make/deploy.mk create mode 100644 make/dev.mk create mode 100644 make/tooling.mk diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f04682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..36272df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,38 @@ +--- +name: Bug +about: Create a bug report to help us improve +title: "[Bug] " +labels: bug +assignees: mmontes11 + +--- + + + +**Describe the bug** + + +**Expected behaviour** + + +**Steps to reproduce the bug** + + +**Additional context** + + +**Environment details**: +- Kubernetes version: +- mariadb-operator version: +- agent version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a466b23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: feature +assignees: mmontes11 + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + + +**Environment details**: +- Kubernetes version: +- mariadb-operator version: +- agent version: \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2154eaf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + GOLANGCI_VERSION: "v1.52.2" + +jobs: + detect-noop: + runs-on: ubuntu-latest + outputs: + noop: ${{ steps.noop.outputs.should_skip }} + steps: + - name: Detect no-op changes + id: noop + uses: fkirc/skip-duplicate-actions@v5.3.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + paths_ignore: '["**.md"]' + concurrent_skipping: false + + lint: + name: Lint + runs-on: ubuntu-latest + needs: detect-noop + if: ${{ needs.detect-noop.outputs.noop != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: "go.mod" + cache: true + + - name: GolangCI Lint + uses: golangci/golangci-lint-action@v3 + with: + version: ${{ env.GOLANGCI_VERSION }} + + build: + name: Build + runs-on: ubuntu-latest + needs: detect-noop + if: ${{ needs.detect-noop.outputs.noop != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: "go.mod" + cache: true + + - name: Build + run: make build + + - name: Build Docker + run: make docker-build + env: + PLATFORM: linux/amd64 + + test: + name: Test + runs-on: ubuntu-latest + needs: detect-noop + if: ${{ needs.detect-noop.outputs.noop != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: "go.mod" + cache: true + + - name: Test + run: make test \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d4164b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Release + +on: + push: + tags: + - "v*" + +env: + GORELEASER_VERSION: "v1.18.2" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --force --tags + + - name: Setup QEMU + uses: docker/setup-qemu-action@v2 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + id: buildx + + - name: Login to container Registry + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + + - name: Prepare + id: prep + run: | + VERSION=sha-${GITHUB_SHA::8} + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF/refs\/tags\//} + fi + echo ::set-output name=BUILD_DATE::$(date -u +'%Y-%m-%dT%H:%M:%SZ') + echo ::set-output name=VERSION::${VERSION} + + - name: Publish multi-arch Docker image + uses: docker/build-push-action@v2 + with: + push: true + builder: ${{ steps.buildx.outputs.name }} + context: . + file: ./Dockerfile + platforms: linux/arm64,linux/amd64 + tags: | + ghcr.io/${{ github.repository_owner }}/agent:${{ steps.prep.outputs.VERSION }} + ghcr.io/${{ github.repository_owner }}/agent:latest + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.description=${{ github.event.repository.description }} + org.opencontainers.image.source=${{ github.event.repository.html_url }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ steps.prep.outputs.VERSION }} + org.opencontainers.image.created=${{ steps.prep.outputs.BUILD_DATE }} + + - name: Set GORELEASER_PREVIOUS_TAG + run: echo "GORELEASER_PREVIOUS_TAG=$(git tag -l "v*" --sort=-version:refname | head -n 2 | tail -n 1)" >> $GITHUB_ENV + + - name: GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: ${{ env.GORELEASER_VERSION }} + args: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..0755e2d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,31 @@ +linters-settings: + gocyclo: + min-complexity: 20 + lll: + line-length: 140 + misspell: + locale: US + +linters: + disable-all: true + enable: + - unused + - errcheck + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nestif + - staticcheck + - typecheck + - unused + - bodyclose + - noctx + +run: + timeout: 5m + go: "1.20" diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..e4aa044 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,14 @@ +changelog: + use: github-native +builds: + - id: agent + main: main.go + binary: "agent_{{ .Version }}_{{ .Arch }}" + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..abf1438 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.20.4-alpine3.18 AS builder + +ARG TARGETOS +ARG TARGETARCH +ENV CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} + +WORKDIR /app + +COPY go.mod go.sum /app/ +RUN go mod download + +COPY . /app +RUN go build -o agent main.go + +FROM gcr.io/distroless/static AS app + +WORKDIR / +COPY --from=builder /app/agent /bin/agent +USER 65532:65532 + +ENTRYPOINT ["/bin/agent"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..479ebc3 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# 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 + +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: help + +##@ General + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +include make/deploy.mk +include make/dev.mk +include make/tooling.mk \ No newline at end of file diff --git a/cmd/agent/root.go b/cmd/agent/root.go new file mode 100644 index 0000000..25df601 --- /dev/null +++ b/cmd/agent/root.go @@ -0,0 +1,21 @@ +package agent + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "agent", + Short: "Agent", + Long: `🤖 Sidecar agent for MariaDB that co-operates with mariadb-operator`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("hello agent!") + }, +} + +func Execute() { + cobra.CheckErr(rootCmd.Execute()) +} diff --git a/go.mod b/go.mod index b4a41c3..33d32ba 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/mariadb-operator/agent go 1.20 + +require github.com/spf13/cobra v1.7.0 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f3366a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..07e3d2a --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/mariadb-operator/agent/cmd/agent" + +func main() { + agent.Execute() +} diff --git a/make/deploy.mk b/make/deploy.mk new file mode 100644 index 0000000..18933c3 --- /dev/null +++ b/make/deploy.mk @@ -0,0 +1,26 @@ +##@ Docker + +PLATFORM ?= linux/amd64,linux/arm64 +IMG ?= ghcr.io/mariadb-operator/agent:latest +BUILDX ?= docker buildx build --platform $(PLATFORM) -t $(IMG) +BUILDER ?= agent + +.PHONY: docker-builder +docker-builder: ## Configure docker builder. + docker buildx create --name $(BUILDER) --use --platform $(PLATFORM) + +.PHONY: docker-build +docker-build: ## Build docker image. + docker build -t $(IMG) . + +.PHONY: docker-buildx +docker-buildx: ## Build multi-arch docker image. + $(BUILDX) . + +.PHONY: docker-push +docker-push: ## Build multi-arch docker image and push it to the registry. + $(BUILDX) --push . + +.PHONY: docker-inspect +docker-inspect: ## Inspect docker image. + docker buildx imagetools inspect $(IMG) \ No newline at end of file diff --git a/make/dev.mk b/make/dev.mk new file mode 100644 index 0000000..3182775 --- /dev/null +++ b/make/dev.mk @@ -0,0 +1,26 @@ +##@ Dev + +.PHONY: lint +lint: golangci-lint ## Lint. + $(GOLANGCI_LINT) run + +.PHONY: build +build: ## Build binary. + go build -o bin/agent main.go + +.PHONY: test +test: ## Run tests. + go test ./... -coverprofile cover.out + +.PHONY: cover +cover: test ## Run tests and generate coverage. + @go tool cover -html=cover.out -o=cover.html + +.PHONY: release +release: goreleaser ## Test release locally. + $(GORELEASER) release --snapshot --rm-dist + +RUN_FLAGS ?= +.PHONY: run +run: lint ## Run a controller from your host. + go run main.go $(RUN_FLAGS) \ No newline at end of file diff --git a/make/tooling.mk b/make/tooling.mk new file mode 100644 index 0000000..5c3886d --- /dev/null +++ b/make/tooling.mk @@ -0,0 +1,24 @@ +##@ Tooling + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint +GORELEASER ?= $(LOCALBIN)/goreleaser + +## Tool Versions +GOLANGCI_LINT_VERSION ?= v1.52.2 +GORELEASER_VERSION ?= v1.18.2 + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + +.PHONY: goreleaser +goreleaser: $(GORELEASER) ## Download goreleaser locally if necessary. +$(GORELEASER): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION) \ No newline at end of file From 2db8194b1eb2b18b95868b55374fe413a9c2f846 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sun, 28 May 2023 22:49:52 +0200 Subject: [PATCH 03/27] HTTP server setup --- cmd/agent/root.go | 53 +++++++++++++++++++++++++++++++++++++++++-- go.mod | 7 +++++- go.sum | 6 +++++ pkg/router/router.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 pkg/router/router.go diff --git a/cmd/agent/root.go b/cmd/agent/root.go index 25df601..b55c17a 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -1,21 +1,70 @@ package agent import ( - "fmt" + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + "github.com/mariadb-operator/agent/pkg/router" "github.com/spf13/cobra" ) +var ( + addr string + compressLevel int + rateLimitRequests int + rateLimitDuration time.Duration +) + var rootCmd = &cobra.Command{ Use: "agent", Short: "Agent", Long: `🤖 Sidecar agent for MariaDB that co-operates with mariadb-operator`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("hello agent!") + router := router.NewRouter( + router.WithCompressLevel(compressLevel), + router.WithRateLimit(rateLimitRequests, rateLimitDuration), + ) + server := http.Server{ + Addr: addr, + Handler: router, + } + + serverContext, stopServer := context.WithCancel(context.Background()) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + go func() { + <-sig + defer stopServer() + + log.Println("shutting down server") + if err := server.Shutdown(context.Background()); err != nil { + log.Fatalf("error shutting down server: %v", err) + } + }() + + log.Printf("server listening at %s", addr) + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("error starting server: %v", err) + } + + <-serverContext.Done() }, } func Execute() { cobra.CheckErr(rootCmd.Execute()) } + +func init() { + rootCmd.Flags().StringVar(&addr, "addr", ":5555", "The address that the HTTP server binds to") + rootCmd.Flags().IntVar(&compressLevel, "compress-level", 5, "HTTP compression level") + rootCmd.Flags().IntVar(&rateLimitRequests, "rate-limit-requests", 100, "Number of requests to be used as rate limit") + rootCmd.Flags().DurationVar(&rateLimitDuration, "rate-limit-duration", 1*time.Minute, "Duration to be used as rate limit") +} diff --git a/go.mod b/go.mod index 33d32ba..06328bd 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,14 @@ module github.com/mariadb-operator/agent go 1.20 -require github.com/spf13/cobra v1.7.0 +require ( + github.com/go-chi/chi/v5 v5.0.8 + github.com/go-chi/httprate v0.7.4 + github.com/spf13/cobra v1.7.0 +) require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index f3366a9..4a7980e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,10 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M= +github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/pkg/router/router.go b/pkg/router/router.go new file mode 100644 index 0000000..9c4350c --- /dev/null +++ b/pkg/router/router.go @@ -0,0 +1,54 @@ +package router + +import ( + "net/http" + "time" + + chi "github.com/go-chi/chi/v5" + middleware "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/httprate" +) + +type Options struct { + CompressLevel int + RateLimitRequests int + RateLimitDuration time.Duration +} + +type Option func(*Options) + +func WithCompressLevel(level int) Option { + return func(o *Options) { + o.CompressLevel = level + } +} + +func WithRateLimit(requests int, duration time.Duration) Option { + return func(o *Options) { + o.RateLimitRequests = requests + o.RateLimitDuration = duration + } +} + +func NewRouter(opts ...Option) http.Handler { + routerOpts := Options{ + CompressLevel: 5, + RateLimitRequests: 100, + RateLimitDuration: 1 * time.Minute, + } + for _, setOpt := range opts { + setOpt(&routerOpts) + } + r := chi.NewRouter() + + r.Use(middleware.Compress(routerOpts.CompressLevel)) + r.Use(httprate.LimitAll(routerOpts.RateLimitRequests, routerOpts.RateLimitDuration)) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + return r +} From 0aaee7bd83e019fbda7922d8bc1aa7abbd267f17 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Mon, 29 May 2023 18:46:31 +0200 Subject: [PATCH 04/27] Moved server to a pkg --- cmd/agent/root.go | 32 ++++---------------------- pkg/server/server.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 pkg/server/server.go diff --git a/cmd/agent/root.go b/cmd/agent/root.go index b55c17a..d0bc181 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -3,13 +3,10 @@ package agent import ( "context" "log" - "net/http" - "os" - "os/signal" - "syscall" "time" "github.com/mariadb-operator/agent/pkg/router" + "github.com/mariadb-operator/agent/pkg/server" "github.com/spf13/cobra" ) @@ -30,31 +27,10 @@ var rootCmd = &cobra.Command{ router.WithCompressLevel(compressLevel), router.WithRateLimit(rateLimitRequests, rateLimitDuration), ) - server := http.Server{ - Addr: addr, - Handler: router, + server := server.NewServer(addr, router) + if err := server.Start(context.Background()); err != nil { + log.Fatal(err) } - - serverContext, stopServer := context.WithCancel(context.Background()) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - go func() { - <-sig - defer stopServer() - - log.Println("shutting down server") - if err := server.Shutdown(context.Background()); err != nil { - log.Fatalf("error shutting down server: %v", err) - } - }() - - log.Printf("server listening at %s", addr) - if err := server.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("error starting server: %v", err) - } - - <-serverContext.Done() }, } diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..bc173df --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,54 @@ +package server + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" +) + +type Server struct { + httpServer *http.Server +} + +func NewServer(addr string, handler http.Handler) *Server { + return &Server{ + httpServer: &http.Server{ + Addr: addr, + Handler: handler, + }, + } +} + +func (s *Server) Start(ctx context.Context) error { + serverContext, stopServer := context.WithCancel(ctx) + errChan := make(chan error) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + go func() { + <-sig + defer stopServer() + + log.Println("shutting down server") + if err := s.httpServer.Shutdown(serverContext); err != nil { + errChan <- fmt.Errorf("error shutting down server: %v", err) + } + }() + + log.Printf("server listening at %s", s.httpServer.Addr) + if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed { + errChan <- fmt.Errorf("error starting server: %v", err) + } + + select { + case <-serverContext.Done(): + return nil + case err := <-errChan: + return err + } +} From 036ce5503bd518fc23de3d4524f6a02cb1fbd35d Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Tue, 30 May 2023 18:53:57 +0200 Subject: [PATCH 05/27] Added logger --- cmd/agent/root.go | 26 ++++++++++- go.mod | 22 ++++++++- go.sum | 105 +++++++++++++++++++++++++++++++++++++++++++ make/dev.mk | 2 +- pkg/logger/logger.go | 54 ++++++++++++++++++++++ pkg/server/server.go | 11 +++-- 6 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 pkg/logger/logger.go diff --git a/cmd/agent/root.go b/cmd/agent/root.go index d0bc181..395fff6 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -3,8 +3,10 @@ package agent import ( "context" "log" + "os" "time" + "github.com/mariadb-operator/agent/pkg/logger" "github.com/mariadb-operator/agent/pkg/router" "github.com/mariadb-operator/agent/pkg/server" "github.com/spf13/cobra" @@ -15,6 +17,9 @@ var ( compressLevel int rateLimitRequests int rateLimitDuration time.Duration + logLevel string + logTimeEncoder string + logDev bool ) var rootCmd = &cobra.Command{ @@ -23,13 +28,25 @@ var rootCmd = &cobra.Command{ Long: `🤖 Sidecar agent for MariaDB that co-operates with mariadb-operator`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + logger, err := logger.NewLogger( + logger.WithLogLevel(logLevel), + logger.WithTimeEncoder(logTimeEncoder), + logger.WithDevelopment(logDev), + ) + if err != nil { + log.Fatalf("error creating logger: %v", err) + } + router := router.NewRouter( router.WithCompressLevel(compressLevel), router.WithRateLimit(rateLimitRequests, rateLimitDuration), ) - server := server.NewServer(addr, router) + + serverLogger := logger.WithName("server") + server := server.NewServer(addr, router, &serverLogger) if err := server.Start(context.Background()); err != nil { - log.Fatal(err) + logger.Error(err, "error starting server") + os.Exit(1) } }, } @@ -43,4 +60,9 @@ func init() { rootCmd.Flags().IntVar(&compressLevel, "compress-level", 5, "HTTP compression level") rootCmd.Flags().IntVar(&rateLimitRequests, "rate-limit-requests", 100, "Number of requests to be used as rate limit") rootCmd.Flags().DurationVar(&rateLimitDuration, "rate-limit-duration", 1*time.Minute, "Duration to be used as rate limit") + rootCmd.Flags().StringVar(&logLevel, "log-level", "info", "Log level to use, one of: "+ + "debug, info, warn, error, dpanic, panic, fatal.") + rootCmd.Flags().StringVar(&logTimeEncoder, "log-time-encoder", "epoch", "Log time encoder to use, one of: "+ + "epoch, millis, nano, iso8601, rfc3339 or rfc3339nano") + rootCmd.Flags().BoolVar(&logDev, "log-dev", false, "Enable development logs.") } diff --git a/go.mod b/go.mod index 06328bd..43bed22 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,31 @@ go 1.20 require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/httprate v0.7.4 + github.com/go-logr/logr v1.2.4 github.com/spf13/cobra v1.7.0 + go.uber.org/zap v1.24.0 + sigs.k8s.io/controller-runtime v0.15.0 ) require ( - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/go-logr/zapr v1.2.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/text v0.9.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apimachinery v0.27.2 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) diff --git a/go.sum b/go.sum index 4a7980e..c46de46 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,121 @@ +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M= github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +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= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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-20190620200207-3b0461eec859/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= +k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= +sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= diff --git a/make/dev.mk b/make/dev.mk index 3182775..01f8659 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -20,7 +20,7 @@ cover: test ## Run tests and generate coverage. release: goreleaser ## Test release locally. $(GORELEASER) release --snapshot --rm-dist -RUN_FLAGS ?= +RUN_FLAGS ?= --log-dev .PHONY: run run: lint ## Run a controller from your host. go run main.go $(RUN_FLAGS) \ No newline at end of file diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..e060df1 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,54 @@ +package logger + +import ( + "fmt" + + "github.com/go-logr/logr" + "go.uber.org/zap/zapcore" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +type Option func(*zap.Options) error + +func WithLogLevel(level string) Option { + return func(o *zap.Options) error { + var lvl zapcore.Level + if err := lvl.UnmarshalText([]byte(level)); err != nil { + return err + } + o.Level = lvl + return nil + } +} + +func WithTimeEncoder(encoder string) Option { + return func(o *zap.Options) error { + var enc zapcore.TimeEncoder + if err := enc.UnmarshalText([]byte(encoder)); err != nil { + return err + } + o.TimeEncoder = enc + return nil + } +} + +func WithDevelopment(dev bool) Option { + return func(o *zap.Options) error { + o.Development = dev + return nil + } +} + +func NewLogger(opts ...Option) (logr.Logger, error) { + zapOpts := zap.Options{ + Level: zapcore.InfoLevel, + TimeEncoder: zapcore.EpochTimeEncoder, + Development: false, + } + for _, setpOpt := range opts { + if err := setpOpt(&zapOpts); err != nil { + return logr.Logger{}, fmt.Errorf("error setting logger option: %v", err) + } + } + return zap.New(zap.UseFlagOptions(&zapOpts)), nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index bc173df..27881ca 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,23 +3,26 @@ package server import ( "context" "fmt" - "log" "net/http" "os" "os/signal" "syscall" + + "github.com/go-logr/logr" ) type Server struct { httpServer *http.Server + logger *logr.Logger } -func NewServer(addr string, handler http.Handler) *Server { +func NewServer(addr string, handler http.Handler, logger *logr.Logger) *Server { return &Server{ httpServer: &http.Server{ Addr: addr, Handler: handler, }, + logger: logger, } } @@ -34,13 +37,13 @@ func (s *Server) Start(ctx context.Context) error { <-sig defer stopServer() - log.Println("shutting down server") + s.logger.Info("shutting down server") if err := s.httpServer.Shutdown(serverContext); err != nil { errChan <- fmt.Errorf("error shutting down server: %v", err) } }() - log.Printf("server listening at %s", s.httpServer.Addr) + s.logger.Info("server listening", "addr", s.httpServer.Addr) if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed { errChan <- fmt.Errorf("error starting server: %v", err) } From a71dddf21c40253dc8c1538d528fae56e576adf3 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Tue, 30 May 2023 20:11:51 +0200 Subject: [PATCH 06/27] Router scaffolding --- cmd/agent/root.go | 5 +++++ pkg/handler/bootstrap.go | 19 +++++++++++++++++++ pkg/handler/galerastate.go | 19 +++++++++++++++++++ pkg/handler/handler.go | 34 ++++++++++++++++++++++++++++++++++ pkg/handler/mysqld.go | 15 +++++++++++++++ pkg/handler/recovery.go | 23 +++++++++++++++++++++++ pkg/router/router.go | 30 +++++++++++++++++++++++++++--- 7 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 pkg/handler/bootstrap.go create mode 100644 pkg/handler/galerastate.go create mode 100644 pkg/handler/handler.go create mode 100644 pkg/handler/mysqld.go create mode 100644 pkg/handler/recovery.go diff --git a/cmd/agent/root.go b/cmd/agent/root.go index 395fff6..c947aba 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/mariadb-operator/agent/pkg/handler" "github.com/mariadb-operator/agent/pkg/logger" "github.com/mariadb-operator/agent/pkg/router" "github.com/mariadb-operator/agent/pkg/server" @@ -37,7 +38,11 @@ var rootCmd = &cobra.Command{ log.Fatalf("error creating logger: %v", err) } + handlerLogger := logger.WithName("handler") + handler := handler.NewHandler(&handlerLogger) + router := router.NewRouter( + handler, router.WithCompressLevel(compressLevel), router.WithRateLimit(rateLimitRequests, rateLimitDuration), ) diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go new file mode 100644 index 0000000..8a4b4e5 --- /dev/null +++ b/pkg/handler/bootstrap.go @@ -0,0 +1,19 @@ +package handler + +import ( + "net/http" + + "github.com/go-logr/logr" +) + +type Bootstrap struct { + logger *logr.Logger +} + +func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (h *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go new file mode 100644 index 0000000..fa8c4e8 --- /dev/null +++ b/pkg/handler/galerastate.go @@ -0,0 +1,19 @@ +package handler + +import ( + "net/http" + + "github.com/go-logr/logr" +) + +type GaleraState struct { + logger *logr.Logger +} + +func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (h *GaleraState) Post(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..30d0cc3 --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,34 @@ +package handler + +import ( + "github.com/go-logr/logr" +) + +type Handler struct { + GaleraState *GaleraState + Bootstrap *Bootstrap + Recovery *Recovery + Mysld *Mysld +} + +func NewHandler(logger *logr.Logger) *Handler { + galeraStateLogger := logger.WithName("galerastate") + bootstrapLogger := logger.WithName("bootstrap") + mysldLogger := logger.WithName("mysqld") + recoveryLogger := logger.WithName("recovery") + + return &Handler{ + GaleraState: &GaleraState{ + logger: &galeraStateLogger, + }, + Bootstrap: &Bootstrap{ + logger: &bootstrapLogger, + }, + Mysld: &Mysld{ + logger: &mysldLogger, + }, + Recovery: &Recovery{ + logger: &recoveryLogger, + }, + } +} diff --git a/pkg/handler/mysqld.go b/pkg/handler/mysqld.go new file mode 100644 index 0000000..a3f5b4c --- /dev/null +++ b/pkg/handler/mysqld.go @@ -0,0 +1,15 @@ +package handler + +import ( + "net/http" + + "github.com/go-logr/logr" +) + +type Mysld struct { + logger *logr.Logger +} + +func (h *Mysld) Post(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go new file mode 100644 index 0000000..a2608f9 --- /dev/null +++ b/pkg/handler/recovery.go @@ -0,0 +1,23 @@ +package handler + +import ( + "net/http" + + "github.com/go-logr/logr" +) + +type Recovery struct { + logger *logr.Logger +} + +func (h *Recovery) Get(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/router/router.go b/pkg/router/router.go index 9c4350c..44b5685 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -7,6 +7,7 @@ import ( chi "github.com/go-chi/chi/v5" middleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httprate" + "github.com/mariadb-operator/agent/pkg/handler" ) type Options struct { @@ -30,7 +31,7 @@ func WithRateLimit(requests int, duration time.Duration) Option { } } -func NewRouter(opts ...Option) http.Handler { +func NewRouter(handler *handler.Handler, opts ...Option) http.Handler { routerOpts := Options{ CompressLevel: 5, RateLimitRequests: 100, @@ -40,15 +41,38 @@ func NewRouter(opts ...Option) http.Handler { setOpt(&routerOpts) } r := chi.NewRouter() - r.Use(middleware.Compress(routerOpts.CompressLevel)) - r.Use(httprate.LimitAll(routerOpts.RateLimitRequests, routerOpts.RateLimitDuration)) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) + r.Mount("/api", apiRouter(handler, &routerOpts)) + + return r +} + +func apiRouter(h *handler.Handler, opts *Options) http.Handler { + r := chi.NewRouter() + r.Use(httprate.LimitAll(opts.RateLimitRequests, opts.RateLimitDuration)) + + r.Route("/bootstrap", func(r chi.Router) { + r.Put("/", h.Bootstrap.Put) + r.Delete("/", h.Bootstrap.Delete) + }) + r.Route("/galerastate", func(r chi.Router) { + r.Get("/", h.GaleraState.Get) + r.Post("/", h.GaleraState.Post) + }) + r.Route("/mysqld", func(r chi.Router) { + r.Post("/", h.Mysld.Post) + }) + r.Route("/recovery", func(r chi.Router) { + r.Get("/", h.Recovery.Get) + r.Put("/", h.Recovery.Put) + r.Delete("/", h.Recovery.Delete) + }) return r } From 675331a7645257f5d8193bb104855480030ec2d5 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 12:43:08 +0200 Subject: [PATCH 07/27] Added filemanager and examples --- .gitignore | 6 ++++- cmd/agent/root.go | 25 ++++++++++++++++---- examples/1-bootstrap.cnf | 2 ++ examples/2-recovery.cnf | 3 +++ examples/grastate-recovery.dat | 4 ++++ examples/grastate-safe-bootstrap.dat | 4 ++++ examples/grastate-seqno.dat | 4 ++++ make/dev.mk | 20 +++++++++++++--- pkg/filemanager/filemanager.go | 35 ++++++++++++++++++++++++++++ pkg/handler/bootstrap.go | 4 +++- pkg/handler/galerastate.go | 4 +++- pkg/handler/handler.go | 14 +++++++---- pkg/handler/recovery.go | 4 +++- 13 files changed, 112 insertions(+), 17 deletions(-) create mode 100644 examples/1-bootstrap.cnf create mode 100644 examples/2-recovery.cnf create mode 100644 examples/grastate-recovery.dat create mode 100644 examples/grastate-safe-bootstrap.dat create mode 100644 examples/grastate-seqno.dat create mode 100644 pkg/filemanager/filemanager.go diff --git a/.gitignore b/.gitignore index 84285ca..4f36dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +*.html # Dependency directories (remove the comment below to include it) vendor/ @@ -18,4 +19,7 @@ vendor/ bin/ # Packaged Helm charts -*.tgz \ No newline at end of file +*.tgz + +# Directory to keep MariaDB files used for development +mariadb/ \ No newline at end of file diff --git a/cmd/agent/root.go b/cmd/agent/root.go index c947aba..8540875 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/handler" "github.com/mariadb-operator/agent/pkg/logger" "github.com/mariadb-operator/agent/pkg/router" @@ -14,13 +15,17 @@ import ( ) var ( - addr string + addr string + configDir string + stateDir string + compressLevel int rateLimitRequests int rateLimitDuration time.Duration - logLevel string - logTimeEncoder string - logDev bool + + logLevel string + logTimeEncoder string + logDev bool ) var rootCmd = &cobra.Command{ @@ -38,8 +43,14 @@ var rootCmd = &cobra.Command{ log.Fatalf("error creating logger: %v", err) } + fileManager, err := filemanager.NewFileManager(configDir, stateDir) + if err != nil { + logger.Error(err, "error creating file manager") + os.Exit(1) + } + handlerLogger := logger.WithName("handler") - handler := handler.NewHandler(&handlerLogger) + handler := handler.NewHandler(fileManager, &handlerLogger) router := router.NewRouter( handler, @@ -62,9 +73,13 @@ func Execute() { func init() { rootCmd.Flags().StringVar(&addr, "addr", ":5555", "The address that the HTTP server binds to") + rootCmd.Flags().StringVar(&configDir, "config-dir", "/etc/mysql/mariadb.conf.d", "The directory that contains MariaDB configuration files") + rootCmd.Flags().StringVar(&stateDir, "state-dir", "/var/lib/mysql", "The directory that contains MariaDB state files") + rootCmd.Flags().IntVar(&compressLevel, "compress-level", 5, "HTTP compression level") rootCmd.Flags().IntVar(&rateLimitRequests, "rate-limit-requests", 100, "Number of requests to be used as rate limit") rootCmd.Flags().DurationVar(&rateLimitDuration, "rate-limit-duration", 1*time.Minute, "Duration to be used as rate limit") + rootCmd.Flags().StringVar(&logLevel, "log-level", "info", "Log level to use, one of: "+ "debug, info, warn, error, dpanic, panic, fatal.") rootCmd.Flags().StringVar(&logTimeEncoder, "log-time-encoder", "epoch", "Log time encoder to use, one of: "+ diff --git a/examples/1-bootstrap.cnf b/examples/1-bootstrap.cnf new file mode 100644 index 0000000..e6db0a6 --- /dev/null +++ b/examples/1-bootstrap.cnf @@ -0,0 +1,2 @@ +[galera] +wsrep_new_cluster="ON" diff --git a/examples/2-recovery.cnf b/examples/2-recovery.cnf new file mode 100644 index 0000000..d342c9c --- /dev/null +++ b/examples/2-recovery.cnf @@ -0,0 +1,3 @@ +[galera] +log_error=mariadb.err +wsrep_recover="ON" diff --git a/examples/grastate-recovery.dat b/examples/grastate-recovery.dat new file mode 100644 index 0000000..348b3b0 --- /dev/null +++ b/examples/grastate-recovery.dat @@ -0,0 +1,4 @@ +version: 2.1 +uuid: 220dcdcb-1629-11e4-add3-aec059ad3734 +seqno: -1 +safe_to_bootstrap: 0 \ No newline at end of file diff --git a/examples/grastate-safe-bootstrap.dat b/examples/grastate-safe-bootstrap.dat new file mode 100644 index 0000000..77b603e --- /dev/null +++ b/examples/grastate-safe-bootstrap.dat @@ -0,0 +1,4 @@ +version: 2.1 +uuid: 220dcdcb-1629-11e4-add3-aec059ad3734 +seqno: -1 +safe_to_bootstrap: 1 \ No newline at end of file diff --git a/examples/grastate-seqno.dat b/examples/grastate-seqno.dat new file mode 100644 index 0000000..201cae7 --- /dev/null +++ b/examples/grastate-seqno.dat @@ -0,0 +1,4 @@ +version: 2.1 +uuid: 220dcdcb-1629-11e4-add3-aec059ad3734 +seqno: 1122 +safe_to_bootstrap: 0 \ No newline at end of file diff --git a/make/dev.mk b/make/dev.mk index 01f8659..16febe0 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -14,13 +14,27 @@ test: ## Run tests. .PHONY: cover cover: test ## Run tests and generate coverage. - @go tool cover -html=cover.out -o=cover.html + go tool cover -html=cover.out -o=cover.html .PHONY: release release: goreleaser ## Test release locally. $(GORELEASER) release --snapshot --rm-dist -RUN_FLAGS ?= --log-dev +CONFIG_DIR ?= mariadb/config +CONFIG_FILE ?= 1-bootstrap.cnf +.PHONY: config +config: ## Copies a example config file for development purposes. + @mkdir -p $(CONFIG_DIR) + cp "examples/$(CONFIG_FILE)" $(CONFIG_DIR) + +STATE_DIR ?= mariadb/state +STATE_FILE ?= grastate-recovery.dat +.PHONY: state +state: ## Copies a example state file for development purposes. + @mkdir -p $(STATE_DIR) + cp "examples/$(STATE_FILE)" "$(STATE_DIR)/grastate.dat" + +RUN_FLAGS ?= --log-dev --config-dir=$(CONFIG_DIR) --state-dir=$(STATE_DIR) .PHONY: run -run: lint ## Run a controller from your host. +run: lint config state ## Run a controller from your host. go run main.go $(RUN_FLAGS) \ No newline at end of file diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go new file mode 100644 index 0000000..b90fabe --- /dev/null +++ b/pkg/filemanager/filemanager.go @@ -0,0 +1,35 @@ +package filemanager + +import ( + "fmt" + "os" +) + +type FileManager struct { + configDir string + stateDir string +} + +func NewFileManager(configDir, stateDir string) (*FileManager, error) { + if err := mustExist(configDir); err != nil { + return nil, err + } + if err := mustExist(stateDir); err != nil { + return nil, err + } + return &FileManager{ + configDir: configDir, + stateDir: stateDir, + }, nil +} + +func mustExist(path string) error { + _, err := os.Stat(path) + if err == nil { + return nil + } + if os.IsNotExist(err) { + return fmt.Errorf("'%s' does not exist", path) + } + return err +} diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index 8a4b4e5..5496999 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -4,10 +4,12 @@ import ( "net/http" "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/filemanager" ) type Bootstrap struct { - logger *logr.Logger + fileManager *filemanager.FileManager + logger *logr.Logger } func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index fa8c4e8..a99146c 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -4,10 +4,12 @@ import ( "net/http" "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/filemanager" ) type GaleraState struct { - logger *logr.Logger + fileManager *filemanager.FileManager + logger *logr.Logger } func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 30d0cc3..78a0df4 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -2,16 +2,17 @@ package handler import ( "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/filemanager" ) type Handler struct { GaleraState *GaleraState Bootstrap *Bootstrap - Recovery *Recovery Mysld *Mysld + Recovery *Recovery } -func NewHandler(logger *logr.Logger) *Handler { +func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Handler { galeraStateLogger := logger.WithName("galerastate") bootstrapLogger := logger.WithName("bootstrap") mysldLogger := logger.WithName("mysqld") @@ -19,16 +20,19 @@ func NewHandler(logger *logr.Logger) *Handler { return &Handler{ GaleraState: &GaleraState{ - logger: &galeraStateLogger, + fileManager: fileManager, + logger: &galeraStateLogger, }, Bootstrap: &Bootstrap{ - logger: &bootstrapLogger, + fileManager: fileManager, + logger: &bootstrapLogger, }, Mysld: &Mysld{ logger: &mysldLogger, }, Recovery: &Recovery{ - logger: &recoveryLogger, + fileManager: fileManager, + logger: &recoveryLogger, }, } } diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index a2608f9..d744fe0 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -4,10 +4,12 @@ import ( "net/http" "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/filemanager" ) type Recovery struct { - logger *logr.Logger + fileManager *filemanager.FileManager + logger *logr.Logger } func (h *Recovery) Get(w http.ResponseWriter, r *http.Request) { From 407712a7e5c38a2fb078618292276297895f0893 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 14:57:54 +0200 Subject: [PATCH 08/27] Get galerastate endpoint --- pkg/filemanager/filemanager.go | 26 +++++++++++--- pkg/galerastate/galerastate.go | 62 ++++++++++++++++++++++++++++++++++ pkg/handler/galerastate.go | 21 +++++++++++- pkg/handler/handler.go | 22 +++++++++++- 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 pkg/galerastate/galerastate.go diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go index b90fabe..db9bccf 100644 --- a/pkg/filemanager/filemanager.go +++ b/pkg/filemanager/filemanager.go @@ -3,6 +3,7 @@ package filemanager import ( "fmt" "os" + "path/filepath" ) type FileManager struct { @@ -11,11 +12,11 @@ type FileManager struct { } func NewFileManager(configDir, stateDir string) (*FileManager, error) { - if err := mustExist(configDir); err != nil { - return nil, err + if err := fileMustExist(configDir); err != nil { + return nil, fmt.Errorf("config directory does not exist: %v", err) } - if err := mustExist(stateDir); err != nil { - return nil, err + if err := fileMustExist(stateDir); err != nil { + return nil, fmt.Errorf("state directory does not exist: %v", err) } return &FileManager{ configDir: configDir, @@ -23,7 +24,22 @@ func NewFileManager(configDir, stateDir string) (*FileManager, error) { }, nil } -func mustExist(path string) error { +func (f *FileManager) ReadStateFile(name string) ([]byte, error) { + return readFile(filepath.Join(f.stateDir, name)) +} + +func readFile(path string) ([]byte, error) { + if err := fileMustExist(path); err != nil { + return nil, fmt.Errorf("file does not exist: %v", err) + } + bytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading file: %v", err) + } + return bytes, nil +} + +func fileMustExist(path string) error { _, err := os.Stat(path) if err == nil { return nil diff --git a/pkg/galerastate/galerastate.go b/pkg/galerastate/galerastate.go new file mode 100644 index 0000000..f75f50a --- /dev/null +++ b/pkg/galerastate/galerastate.go @@ -0,0 +1,62 @@ +package galerastate + +import ( + "bufio" + "bytes" + "fmt" + "strconv" + "strings" +) + +type GaleraState struct { + Version string `json:"version"` + UUID string `json:"uuid"` + Seqno int `json:"seqno"` + SafeToBootstrap bool `json:"safeToBootstrap"` +} + +func (g *GaleraState) Unmarshal(b []byte) error { + fileScanner := bufio.NewScanner(bytes.NewReader(b)) + fileScanner.Split(bufio.ScanLines) + + for fileScanner.Scan() { + line := fileScanner.Text() + parts := strings.Split(fileScanner.Text(), ":") + if len(parts) != 2 { + return fmt.Errorf("error unmarshalling galera state: invalid '%s'", line) + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + switch key { + case "version": + g.Version = value + case "uuid": + g.UUID = value + case "seqno": + i, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("error parsing seqno: %v", err) + } + g.Seqno = i + case "safe_to_bootstrap": + b, err := parseBool(value) + if err != nil { + return fmt.Errorf("error parsing safe_to_bootstrap: %v", err) + } + g.SafeToBootstrap = b + } + } + return nil +} + +func parseBool(s string) (bool, error) { + i, err := strconv.Atoi(s) + if err != nil { + return false, fmt.Errorf("error parsing integer bool: %v", err) + } + if i != 0 && i != 1 { + return false, fmt.Errorf("invalid integer bool: %d", i) + } + return i == 1, nil +} diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index a99146c..3360076 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -5,15 +5,34 @@ import ( "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" + "github.com/mariadb-operator/agent/pkg/galerastate" +) + +var ( + galeraStateFile = "grastate.dat" ) type GaleraState struct { fileManager *filemanager.FileManager + jsonEncoder *jsonEncoder logger *logr.Logger } func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + bytes, err := h.fileManager.ReadStateFile(galeraStateFile) + if err != nil { + h.logger.Error(err, "error reading file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + var galeraState galerastate.GaleraState + if err := galeraState.Unmarshal(bytes); err != nil { + h.logger.Error(err, "error unmarshalling galera state") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + h.jsonEncoder.encode(w, galeraState) } func (h *GaleraState) Post(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 78a0df4..e7eb775 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -1,6 +1,9 @@ package handler import ( + "encoding/json" + "net/http" + "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" ) @@ -21,7 +24,10 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand return &Handler{ GaleraState: &GaleraState{ fileManager: fileManager, - logger: &galeraStateLogger, + jsonEncoder: &jsonEncoder{ + logger: &galeraStateLogger, + }, + logger: &galeraStateLogger, }, Bootstrap: &Bootstrap{ fileManager: fileManager, @@ -36,3 +42,17 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand }, } } + +type jsonEncoder struct { + logger *logr.Logger +} + +func (j *jsonEncoder) encode(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + j.logger.Error(err, "error encoding json") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} From 4f0515007894e00897d7528ada60504d482bb285 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 15:45:12 +0200 Subject: [PATCH 09/27] 404 when grastate.dat not exists --- pkg/filemanager/filemanager.go | 4 ++-- pkg/handler/galerastate.go | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go index db9bccf..fcca1a9 100644 --- a/pkg/filemanager/filemanager.go +++ b/pkg/filemanager/filemanager.go @@ -29,8 +29,8 @@ func (f *FileManager) ReadStateFile(name string) ([]byte, error) { } func readFile(path string) ([]byte, error) { - if err := fileMustExist(path); err != nil { - return nil, fmt.Errorf("file does not exist: %v", err) + if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + return nil, err } bytes, err := os.ReadFile(path) if err != nil { diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index 3360076..ca14a44 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "os" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" @@ -21,17 +22,21 @@ type GaleraState struct { func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { bytes, err := h.fileManager.ReadStateFile(galeraStateFile) if err != nil { + if os.IsNotExist(err) { + http.Error(w, "Not found", http.StatusNotFound) + return + } h.logger.Error(err, "error reading file") http.Error(w, "Internal server error", http.StatusInternalServerError) return } + var galeraState galerastate.GaleraState if err := galeraState.Unmarshal(bytes); err != nil { h.logger.Error(err, "error unmarshalling galera state") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.jsonEncoder.encode(w, galeraState) } From cdc5e206afa74f0e9c33e6c67027f835308fbece Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 17:16:39 +0200 Subject: [PATCH 10/27] Set safe to bootstrap --- pkg/filemanager/filemanager.go | 28 ++++++----- .../galerastate.go => galera/galera.go} | 46 +++++++++++++++++-- pkg/handler/bootstrap.go | 35 ++++++++++++++ pkg/handler/galerastate.go | 16 ++----- pkg/handler/handler.go | 10 ++-- pkg/router/router.go | 9 +--- 6 files changed, 104 insertions(+), 40 deletions(-) rename pkg/{galerastate/galerastate.go => galera/galera.go} (54%) diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go index fcca1a9..22906a0 100644 --- a/pkg/filemanager/filemanager.go +++ b/pkg/filemanager/filemanager.go @@ -12,11 +12,11 @@ type FileManager struct { } func NewFileManager(configDir, stateDir string) (*FileManager, error) { - if err := fileMustExist(configDir); err != nil { - return nil, fmt.Errorf("config directory does not exist: %v", err) + if _, err := os.Stat(configDir); err != nil { + return nil, fmt.Errorf("error reading config directory: %v", err) } - if err := fileMustExist(stateDir); err != nil { - return nil, fmt.Errorf("state directory does not exist: %v", err) + if _, err := os.Stat(stateDir); err != nil { + return nil, fmt.Errorf("error reading state directory: %v", err) } return &FileManager{ configDir: configDir, @@ -28,8 +28,12 @@ func (f *FileManager) ReadStateFile(name string) ([]byte, error) { return readFile(filepath.Join(f.stateDir, name)) } +func (f *FileManager) WriteStateFile(name string, bytes []byte) error { + return writeFile(filepath.Join(f.stateDir, name), bytes) +} + func readFile(path string) ([]byte, error) { - if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + if _, err := os.Stat(path); err != nil { return nil, err } bytes, err := os.ReadFile(path) @@ -39,13 +43,13 @@ func readFile(path string) ([]byte, error) { return bytes, nil } -func fileMustExist(path string) error { - _, err := os.Stat(path) - if err == nil { - return nil +func writeFile(path string, bytes []byte) error { + info, err := os.Stat(path) + if err != nil { + return err } - if os.IsNotExist(err) { - return fmt.Errorf("'%s' does not exist", path) + if err := os.WriteFile(path, bytes, info.Mode()); err != nil { + return fmt.Errorf("error writing file: %v", err) } - return err + return nil } diff --git a/pkg/galerastate/galerastate.go b/pkg/galera/galera.go similarity index 54% rename from pkg/galerastate/galerastate.go rename to pkg/galera/galera.go index f75f50a..fadcc57 100644 --- a/pkg/galerastate/galerastate.go +++ b/pkg/galera/galera.go @@ -1,13 +1,18 @@ -package galerastate +package galera import ( "bufio" "bytes" "fmt" + "html/template" "strconv" "strings" ) +var ( + GaleraStateFile = "grastate.dat" +) + type GaleraState struct { Version string `json:"version"` UUID string `json:"uuid"` @@ -15,15 +20,44 @@ type GaleraState struct { SafeToBootstrap bool `json:"safeToBootstrap"` } -func (g *GaleraState) Unmarshal(b []byte) error { - fileScanner := bufio.NewScanner(bytes.NewReader(b)) +func (g *GaleraState) MarshalText() ([]byte, error) { + type tplOpts struct { + Version string + UUID string + Seqno int + SafeToBootstrap int + } + tpl := createTpl("grastate.dat", `version: {{ .Version }} +uuid: {{ .UUID }} +seqno: {{ .Seqno }} +safe_to_bootstrap: {{ .SafeToBootstrap }}`) + buf := new(bytes.Buffer) + err := tpl.Execute(buf, tplOpts{ + Version: g.Version, + UUID: g.UUID, + Seqno: g.Seqno, + SafeToBootstrap: func() int { + if g.SafeToBootstrap { + return 1 + } + return 0 + }(), + }) + if err != nil { + return nil, fmt.Errorf("error rendering template: %v", err) + } + return buf.Bytes(), nil +} + +func (g *GaleraState) UnmarshalText(text []byte) error { + fileScanner := bufio.NewScanner(bytes.NewReader(text)) fileScanner.Split(bufio.ScanLines) for fileScanner.Scan() { line := fileScanner.Text() parts := strings.Split(fileScanner.Text(), ":") if len(parts) != 2 { - return fmt.Errorf("error unmarshalling galera state: invalid '%s'", line) + return fmt.Errorf("invalid galera state line: '%s'", line) } key := strings.TrimSpace(parts[0]) @@ -50,6 +84,10 @@ func (g *GaleraState) Unmarshal(b []byte) error { return nil } +func createTpl(name, t string) *template.Template { + return template.Must(template.New(name).Parse(t)) +} + func parseBool(s string) (bool, error) { i, err := strconv.Atoi(s) if err != nil { diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index 5496999..df1a4fb 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -1,10 +1,13 @@ package handler import ( + "fmt" "net/http" + "os" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" + "github.com/mariadb-operator/agent/pkg/galera" ) type Bootstrap struct { @@ -13,9 +16,41 @@ type Bootstrap struct { } func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { + if err := h.setSafeToBootstrap(); err != nil { + h.logger.Error(err, "error setting safe to bootstrap") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) } func (h *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +func (h *Bootstrap) setSafeToBootstrap() error { + bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("error reading galera state: %v", err) + } + + var galeraState galera.GaleraState + if err := galeraState.UnmarshalText(bytes); err != nil { + return fmt.Errorf("error unmarshaling galera state: %v", err) + } + + galeraState.SafeToBootstrap = true + bytes, err = galeraState.MarshalText() + if err != nil { + return fmt.Errorf("error marshallng galera state: %v", err) + } + + if err := h.fileManager.WriteStateFile(galera.GaleraStateFile, bytes); err != nil { + return fmt.Errorf("error writing galera state: %v", err) + } + return nil +} diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index ca14a44..2d77570 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -6,11 +6,7 @@ import ( "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" - "github.com/mariadb-operator/agent/pkg/galerastate" -) - -var ( - galeraStateFile = "grastate.dat" + "github.com/mariadb-operator/agent/pkg/galera" ) type GaleraState struct { @@ -20,7 +16,7 @@ type GaleraState struct { } func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { - bytes, err := h.fileManager.ReadStateFile(galeraStateFile) + bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFile) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) @@ -31,15 +27,11 @@ func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { return } - var galeraState galerastate.GaleraState - if err := galeraState.Unmarshal(bytes); err != nil { + var galeraState galera.GaleraState + if err := galeraState.UnmarshalText(bytes); err != nil { h.logger.Error(err, "error unmarshalling galera state") http.Error(w, "Internal server error", http.StatusInternalServerError) return } h.jsonEncoder.encode(w, galeraState) } - -func (h *GaleraState) Post(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index e7eb775..1db0bda 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -9,8 +9,8 @@ import ( ) type Handler struct { - GaleraState *GaleraState Bootstrap *Bootstrap + GaleraState *GaleraState Mysld *Mysld Recovery *Recovery } @@ -22,6 +22,10 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand recoveryLogger := logger.WithName("recovery") return &Handler{ + Bootstrap: &Bootstrap{ + fileManager: fileManager, + logger: &bootstrapLogger, + }, GaleraState: &GaleraState{ fileManager: fileManager, jsonEncoder: &jsonEncoder{ @@ -29,10 +33,6 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand }, logger: &galeraStateLogger, }, - Bootstrap: &Bootstrap{ - fileManager: fileManager, - logger: &bootstrapLogger, - }, Mysld: &Mysld{ logger: &mysldLogger, }, diff --git a/pkg/router/router.go b/pkg/router/router.go index 44b5685..c71b876 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -61,13 +61,8 @@ func apiRouter(h *handler.Handler, opts *Options) http.Handler { r.Put("/", h.Bootstrap.Put) r.Delete("/", h.Bootstrap.Delete) }) - r.Route("/galerastate", func(r chi.Router) { - r.Get("/", h.GaleraState.Get) - r.Post("/", h.GaleraState.Post) - }) - r.Route("/mysqld", func(r chi.Router) { - r.Post("/", h.Mysld.Post) - }) + r.Get("/galerastate", h.GaleraState.Get) + r.Post("/mysqld", h.Mysld.Post) r.Route("/recovery", func(r chi.Router) { r.Get("/", h.Recovery.Get) r.Put("/", h.Recovery.Put) From 43460a6ebbdb70ae051794e983672ecf360f04c9 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 18:17:56 +0200 Subject: [PATCH 11/27] Adding and deleting bootstrap file --- pkg/filemanager/filemanager.go | 31 +++++++++++-------------------- pkg/galera/galera.go | 9 ++++++++- pkg/handler/bootstrap.go | 18 ++++++++++++++++-- pkg/handler/galerastate.go | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go index 22906a0..64355bc 100644 --- a/pkg/filemanager/filemanager.go +++ b/pkg/filemanager/filemanager.go @@ -2,10 +2,15 @@ package filemanager import ( "fmt" + "io/fs" "os" "path/filepath" ) +var ( + writeFileMode = fs.FileMode(0777) +) + type FileManager struct { configDir string stateDir string @@ -25,31 +30,17 @@ func NewFileManager(configDir, stateDir string) (*FileManager, error) { } func (f *FileManager) ReadStateFile(name string) ([]byte, error) { - return readFile(filepath.Join(f.stateDir, name)) + return os.ReadFile(filepath.Join(f.stateDir, name)) } func (f *FileManager) WriteStateFile(name string, bytes []byte) error { - return writeFile(filepath.Join(f.stateDir, name), bytes) + return os.WriteFile(filepath.Join(f.stateDir, name), bytes, writeFileMode) } -func readFile(path string) ([]byte, error) { - if _, err := os.Stat(path); err != nil { - return nil, err - } - bytes, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("error reading file: %v", err) - } - return bytes, nil +func (f *FileManager) WriteConfigFile(name string, bytes []byte) error { + return os.WriteFile(filepath.Join(f.configDir, name), bytes, writeFileMode) } -func writeFile(path string, bytes []byte) error { - info, err := os.Stat(path) - if err != nil { - return err - } - if err := os.WriteFile(path, bytes, info.Mode()); err != nil { - return fmt.Errorf("error writing file: %v", err) - } - return nil +func (f *FileManager) DeleteConfigFile(name string) error { + return os.Remove(filepath.Join(f.configDir, name)) } diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index fadcc57..da5fbf2 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -10,7 +10,14 @@ import ( ) var ( - GaleraStateFile = "grastate.dat" + GaleraStateFileName = "grastate.dat" + BootstrapFileName = "1-bootstrap.cnf" + BootstrapFile = `[galera] +wsrep_new_cluster="ON"` + RecoveryFileName = "2-recovery.cnf" + RecoveryFile = `[galera] +log_error=mariadb.err +wsrep_recover="ON"` ) type GaleraState struct { diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index df1a4fb..cebe460 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -22,15 +22,29 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { return } + if err := h.fileManager.WriteConfigFile(galera.BootstrapFileName, []byte(galera.BootstrapFile)); err != nil { + h.logger.Error(err, "error writing bootstrap file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } w.WriteHeader(http.StatusOK) } func (h *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { + if err := h.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil { + if os.IsNotExist(err) { + http.Error(w, "Not found", http.StatusNotFound) + return + } + h.logger.Error(err, "error deleting bootstrap file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } w.WriteHeader(http.StatusOK) } func (h *Bootstrap) setSafeToBootstrap() error { - bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFile) + bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { return nil @@ -49,7 +63,7 @@ func (h *Bootstrap) setSafeToBootstrap() error { return fmt.Errorf("error marshallng galera state: %v", err) } - if err := h.fileManager.WriteStateFile(galera.GaleraStateFile, bytes); err != nil { + if err := h.fileManager.WriteStateFile(galera.GaleraStateFileName, bytes); err != nil { return fmt.Errorf("error writing galera state: %v", err) } return nil diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index 2d77570..574d707 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -16,7 +16,7 @@ type GaleraState struct { } func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { - bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFile) + bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) From 31abbbdf4f3e74c8bbb3a9d26704afb5d7ba4475 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 19:53:48 +0200 Subject: [PATCH 12/27] Added galera local setup --- docker-compose.yml | 53 ++++++++++++++++++++++++++++++++++++++++++++++ hack/galera.sh | 35 ++++++++++++++++++++++++++++++ make/deploy.mk | 21 +++++++++++++++++- make/dev.mk | 14 ++++++++---- 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 docker-compose.yml create mode 100755 hack/galera.sh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cacbc4b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: "3.3" + +services: + mariadb-0: + container_name: mariadb-0 + image: mariadb:10.7.4 + pid: host + command: + - bash + - -c + - /scripts/galera.sh + restart: unless-stopped + environment: + - HOSTNAME=mariadb-0 + - MYSQL_TCP_PORT=3306 + - MARIADB_ROOT_PASSWORD=mariadb + - MARIADB_DATABASE=mariadb + - MARIADB_USER=mariadb + - MARIADB_PASSWORD=mariadb + ports: + - "3306" + - "4444" + - "4567" + - "4568" + volumes: + - ./hack/galera.sh:/scripts/galera.sh + - ./mariadb/config/mariadb-0:/etc/mysql/mariadb.conf.d + - ./mariadb/state/mariadb-0:/var/lib/mysql + mariadb-1: + container_name: mariadb-1 + image: mariadb:10.7.4 + pid: host + command: + - bash + - -c + - /scripts/galera.sh + restart: unless-stopped + environment: + - HOSTNAME=mariadb-1 + - MYSQL_TCP_PORT=3306 + - MARIADB_ROOT_PASSWORD=mariadb + - MARIADB_DATABASE=mariadb + - MARIADB_USER=mariadb + - MARIADB_PASSWORD=mariadb + ports: + - "3306" + - "4444" + - "4567" + - "4568" + volumes: + - ./hack/galera.sh:/scripts/galera.sh + - ./mariadb/config/mariadb-1:/etc/mysql/mariadb.conf.d + - ./mariadb/state/mariadb-1:/var/lib/mysql \ No newline at end of file diff --git a/hack/galera.sh b/hack/galera.sh new file mode 100755 index 0000000..2fbd6ee --- /dev/null +++ b/hack/galera.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +if [ -z "$HOSTNAME" ]; then + echo "HOSTNAME environment variable not set" + exit 1 +fi + +if [ -z "$ENTRYPOINT" ]; then + ENTRYPOINT="/usr/local/bin/docker-entrypoint.sh" +fi + +cat < Date: Wed, 31 May 2023 20:09:14 +0200 Subject: [PATCH 13/27] More reliable galera state unmarshaler --- pkg/galera/galera.go | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index da5fbf2..e74ea54 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -60,34 +60,49 @@ func (g *GaleraState) UnmarshalText(text []byte) error { fileScanner := bufio.NewScanner(bytes.NewReader(text)) fileScanner.Split(bufio.ScanLines) + var version *string + var uuid *string + var seqno *int + var safeToBootstrap *bool + for fileScanner.Scan() { - line := fileScanner.Text() parts := strings.Split(fileScanner.Text(), ":") if len(parts) != 2 { - return fmt.Errorf("invalid galera state line: '%s'", line) + continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "version": - g.Version = value + version = &value case "uuid": - g.UUID = value + uuid = &value case "seqno": i, err := strconv.Atoi(value) if err != nil { return fmt.Errorf("error parsing seqno: %v", err) } - g.Seqno = i + seqno = &i case "safe_to_bootstrap": b, err := parseBool(value) if err != nil { return fmt.Errorf("error parsing safe_to_bootstrap: %v", err) } - g.SafeToBootstrap = b + safeToBootstrap = &b } } + + if version == nil || uuid == nil || seqno == nil || safeToBootstrap == nil { + return fmt.Errorf( + "invalid galera state file: version=%v uuid=%v seqno=%v safeToBootstrap=%v", + version, uuid, seqno, safeToBootstrap, + ) + } + g.Version = *version + g.UUID = *uuid + g.Seqno = *seqno + g.SafeToBootstrap = *safeToBootstrap return nil } From 3c19a1e4eb950a0941d12aac89f12c781521fc28 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Wed, 31 May 2023 22:43:41 +0200 Subject: [PATCH 14/27] Reloading mariadbd process after bootstrap --- .vscode/launch.json | 15 +++++++++++++++ docker-compose.yml | 4 ++-- go.mod | 1 + go.sum | 2 ++ make/deploy.mk | 4 ++-- pkg/handler/bootstrap.go | 10 ++++++++++ pkg/handler/handler.go | 5 ----- pkg/handler/mysqld.go | 15 --------------- pkg/handler/recovery.go | 4 ---- pkg/mariadbd/mariadbd.go | 31 +++++++++++++++++++++++++++++++ pkg/router/router.go | 2 -- 11 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 pkg/handler/mysqld.go create mode 100644 pkg/mariadbd/mariadbd.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e6885dd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go" + } + ] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cacbc4b..61a4685 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3.3" services: mariadb-0: container_name: mariadb-0 - image: mariadb:10.7.4 + image: mariadb:10.11.3 pid: host command: - bash @@ -28,7 +28,7 @@ services: - ./mariadb/state/mariadb-0:/var/lib/mysql mariadb-1: container_name: mariadb-1 - image: mariadb:10.7.4 + image: mariadb:10.11.3 pid: host command: - bash diff --git a/go.mod b/go.mod index 43bed22..519a970 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/httprate v0.7.4 github.com/go-logr/logr v1.2.4 + github.com/mitchellh/go-ps v1.0.0 github.com/spf13/cobra v1.7.0 go.uber.org/zap v1.24.0 sigs.k8s.io/controller-runtime v0.15.0 diff --git a/go.sum b/go.sum index c46de46..896f1f6 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 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= diff --git a/make/deploy.mk b/make/deploy.mk index bee25a6..a14bfd9 100644 --- a/make/deploy.mk +++ b/make/deploy.mk @@ -31,8 +31,8 @@ docker-inspect: ## Inspect docker image. mariadb: ## Create a MariaDB galera cluster using docker compose. docker compose up -d -.PHONY: mariadb-delete -mariadb-delete: ## Delete the MariaDB galera cluster. +.PHONY: mariadb-rm +mariadb-rm: ## Remove the MariaDB galera cluster. docker compose rm --stop --force sudo rm -rf mariadb diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index cebe460..e0eead0 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/galera" + "github.com/mariadb-operator/agent/pkg/mariadbd" ) type Bootstrap struct { @@ -27,6 +28,15 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } + + h.logger.Info("reloading mariadbd process") + if err := mariadbd.Reload(); err != nil { + h.logger.Error(err, "error reloading mariadbd process") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + h.logger.Info("mariadbd process reloaded") + w.WriteHeader(http.StatusOK) } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 1db0bda..03d7e0a 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -11,14 +11,12 @@ import ( type Handler struct { Bootstrap *Bootstrap GaleraState *GaleraState - Mysld *Mysld Recovery *Recovery } func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Handler { galeraStateLogger := logger.WithName("galerastate") bootstrapLogger := logger.WithName("bootstrap") - mysldLogger := logger.WithName("mysqld") recoveryLogger := logger.WithName("recovery") return &Handler{ @@ -33,9 +31,6 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand }, logger: &galeraStateLogger, }, - Mysld: &Mysld{ - logger: &mysldLogger, - }, Recovery: &Recovery{ fileManager: fileManager, logger: &recoveryLogger, diff --git a/pkg/handler/mysqld.go b/pkg/handler/mysqld.go deleted file mode 100644 index a3f5b4c..0000000 --- a/pkg/handler/mysqld.go +++ /dev/null @@ -1,15 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/go-logr/logr" -) - -type Mysld struct { - logger *logr.Logger -} - -func (h *Mysld) Post(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index d744fe0..dc01806 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -12,10 +12,6 @@ type Recovery struct { logger *logr.Logger } -func (h *Recovery) Get(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } diff --git a/pkg/mariadbd/mariadbd.go b/pkg/mariadbd/mariadbd.go new file mode 100644 index 0000000..a946de7 --- /dev/null +++ b/pkg/mariadbd/mariadbd.go @@ -0,0 +1,31 @@ +package mariadbd + +import ( + "fmt" + "syscall" + + "github.com/mitchellh/go-ps" +) + +var ( + mariadbdProcessName = "mariadbd" + reloadSysCall = syscall.SIGKILL +) + +func Reload() error { + processes, err := ps.Processes() + if err != nil { + return fmt.Errorf("error getting processes: %v", err) + } + + for _, p := range processes { + if p.Executable() == mariadbdProcessName { + if err := syscall.Kill(p.Pid(), reloadSysCall); err != nil { + return fmt.Errorf("error sending kill signal to process '%s' with pid %d: %v", mariadbdProcessName, p.Pid(), err) + } + return nil + } + } + + return fmt.Errorf("process '%s' not found", mariadbdProcessName) +} diff --git a/pkg/router/router.go b/pkg/router/router.go index c71b876..b5fa4c3 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -62,9 +62,7 @@ func apiRouter(h *handler.Handler, opts *Options) http.Handler { r.Delete("/", h.Bootstrap.Delete) }) r.Get("/galerastate", h.GaleraState.Get) - r.Post("/mysqld", h.Mysld.Post) r.Route("/recovery", func(r chi.Router) { - r.Get("/", h.Recovery.Get) r.Put("/", h.Recovery.Put) r.Delete("/", h.Recovery.Delete) }) From 5989cf032e351ee5831b791bbfe48ba0fa3de201 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Thu, 1 Jun 2023 17:46:04 +0200 Subject: [PATCH 15/27] Added recovery --- hack/galera.sh | 2 +- pkg/handler/recovery.go | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/hack/galera.sh b/hack/galera.sh index 2fbd6ee..6316079 100755 --- a/hack/galera.sh +++ b/hack/galera.sh @@ -28,7 +28,7 @@ wsrep_node_address="${HOSTNAME}" wsrep_node_name="${HOSTNAME}" EOF -if [ "$HOSTNAME" = "mariadb-0" ]; then +if [ "$HOSTNAME" = "mariadb-0" ] && [ ! -n "$(ls -A /var/lib/mysql)" ]; then bash -c "$ENTRYPOINT mariadbd --wsrep-new-cluster" else bash -c "$ENTRYPOINT mariadbd" diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index dc01806..6760d2d 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -2,9 +2,12 @@ package handler import ( "net/http" + "os" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" + "github.com/mariadb-operator/agent/pkg/galera" + "github.com/mariadb-operator/agent/pkg/mariadbd" ) type Recovery struct { @@ -13,9 +16,31 @@ type Recovery struct { } func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { + if err := h.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { + h.logger.Error(err, "error writing recovery file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + h.logger.Info("reloading mariadbd process") + if err := mariadbd.Reload(); err != nil { + h.logger.Error(err, "error reloading mariadbd process") + } else { + h.logger.Info("mariadbd process reloaded") + } + w.WriteHeader(http.StatusOK) } func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { + if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { + if os.IsNotExist(err) { + http.Error(w, "Not found", http.StatusNotFound) + return + } + h.logger.Error(err, "error deleting recovery file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } w.WriteHeader(http.StatusOK) } From a5429a5499556f6680df1209cb57fcfe6d9f8c63 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 2 Jun 2023 19:23:51 +0200 Subject: [PATCH 16/27] Recover from log file --- docker-compose.yml | 33 +++----------------- hack/galera.sh | 4 +-- make/dev.mk | 6 ++-- pkg/filemanager/filemanager.go | 8 +++-- pkg/galera/galera.go | 56 ++++++++++++++++++++++++++++++++-- pkg/handler/handler.go | 5 ++- pkg/handler/recovery.go | 51 +++++++++++++++++++++++++++++-- 7 files changed, 120 insertions(+), 43 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 61a4685..7e716af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3.3" services: - mariadb-0: - container_name: mariadb-0 + mariadb: + container_name: mariadb image: mariadb:10.11.3 pid: host command: @@ -24,30 +24,5 @@ services: - "4568" volumes: - ./hack/galera.sh:/scripts/galera.sh - - ./mariadb/config/mariadb-0:/etc/mysql/mariadb.conf.d - - ./mariadb/state/mariadb-0:/var/lib/mysql - mariadb-1: - container_name: mariadb-1 - image: mariadb:10.11.3 - pid: host - command: - - bash - - -c - - /scripts/galera.sh - restart: unless-stopped - environment: - - HOSTNAME=mariadb-1 - - MYSQL_TCP_PORT=3306 - - MARIADB_ROOT_PASSWORD=mariadb - - MARIADB_DATABASE=mariadb - - MARIADB_USER=mariadb - - MARIADB_PASSWORD=mariadb - ports: - - "3306" - - "4444" - - "4567" - - "4568" - volumes: - - ./hack/galera.sh:/scripts/galera.sh - - ./mariadb/config/mariadb-1:/etc/mysql/mariadb.conf.d - - ./mariadb/state/mariadb-1:/var/lib/mysql \ No newline at end of file + - ./mariadb/config/docker:/etc/mysql/mariadb.conf.d + - ./mariadb/state/docker:/var/lib/mysql \ No newline at end of file diff --git a/hack/galera.sh b/hack/galera.sh index 6316079..0fd8320 100755 --- a/hack/galera.sh +++ b/hack/galera.sh @@ -21,14 +21,14 @@ innodb_autoinc_lock_mode=2 [galera] wsrep_on=ON wsrep_provider=/usr/lib/galera/libgalera_smm.so -wsrep_cluster_address="gcomm://mariadb-0,mariadb-1" +wsrep_cluster_address="gcomm://mariadb" wsrep_cluster_name="mariadb-galera-cluster" wsrep_sst_method=rsync wsrep_node_address="${HOSTNAME}" wsrep_node_name="${HOSTNAME}" EOF -if [ "$HOSTNAME" = "mariadb-0" ] && [ ! -n "$(ls -A /var/lib/mysql)" ]; then +if [ ! -n "$(ls -A /var/lib/mysql)" ]; then bash -c "$ENTRYPOINT mariadbd --wsrep-new-cluster" else bash -c "$ENTRYPOINT mariadbd" diff --git a/make/dev.mk b/make/dev.mk index efe16a0..fff35bc 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -20,14 +20,14 @@ cover: test ## Run tests and generate coverage. release: goreleaser ## Test release locally. $(GORELEASER) release --snapshot --rm-dist -CONFIG_DIR ?= mariadb/config/mariadb +CONFIG_DIR ?= mariadb/config/local CONFIG_FILE ?= 1-bootstrap.cnf .PHONY: config config: ## Copies a example config file for development purposes. @mkdir -p $(CONFIG_DIR) cp "examples/$(CONFIG_FILE)" $(CONFIG_DIR) -STATE_DIR ?= mariadb/state/mariadb +STATE_DIR ?= mariadb/state/local STATE_FILE ?= grastate-recovery.dat .PHONY: state state: ## Copies a example state file for development purposes. @@ -40,7 +40,7 @@ run: config state ## Run agent from your host. go run main.go $(RUN_FLAGS) AGENT ?= $(LOCALBIN)/agent -SU_RUN_FLAGS ?= --log-dev --config-dir=mariadb/config/mariadb-0 --state-dir=mariadb/state/mariadb-0 +SU_RUN_FLAGS ?= --log-dev --config-dir=mariadb/config/docker --state-dir=mariadb/state/docker .PHONY: su-run su-run: build ## Run agent from your host as root to be able to access mariadb volumes and process. sudo $(AGENT) $(SU_RUN_FLAGS) \ No newline at end of file diff --git a/pkg/filemanager/filemanager.go b/pkg/filemanager/filemanager.go index 64355bc..4745c0b 100644 --- a/pkg/filemanager/filemanager.go +++ b/pkg/filemanager/filemanager.go @@ -29,12 +29,16 @@ func NewFileManager(configDir, stateDir string) (*FileManager, error) { }, nil } +func (f *FileManager) WriteStateFile(name string, bytes []byte) error { + return os.WriteFile(filepath.Join(f.stateDir, name), bytes, writeFileMode) +} + func (f *FileManager) ReadStateFile(name string) ([]byte, error) { return os.ReadFile(filepath.Join(f.stateDir, name)) } -func (f *FileManager) WriteStateFile(name string, bytes []byte) error { - return os.WriteFile(filepath.Join(f.stateDir, name), bytes, writeFileMode) +func (f *FileManager) DeleteStateFile(name string) error { + return os.Remove(filepath.Join(f.stateDir, name)) } func (f *FileManager) WriteConfigFile(name string, bytes []byte) error { diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index e74ea54..dd6f390 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -15,9 +15,10 @@ var ( BootstrapFile = `[galera] wsrep_new_cluster="ON"` RecoveryFileName = "2-recovery.cnf" - RecoveryFile = `[galera] -log_error=mariadb.err -wsrep_recover="ON"` + RecoveryLog = "mariadb.err" + RecoveryFile = fmt.Sprintf(`[galera] +log_error=%s +wsrep_recover="ON"`, RecoveryLog) ) type GaleraState struct { @@ -77,6 +78,7 @@ func (g *GaleraState) UnmarshalText(text []byte) error { case "version": version = &value case "uuid": + // TODO: validate UUID uuid = &value case "seqno": i, err := strconv.Atoi(value) @@ -106,6 +108,54 @@ func (g *GaleraState) UnmarshalText(text []byte) error { return nil } +type Recover struct { + UUID string `json:"uuid"` + Seqno int `json:"seqno"` +} + +func (r *Recover) UnmarshalText(text []byte) error { + fileScanner := bufio.NewScanner(bytes.NewReader(text)) + fileScanner.Split(bufio.ScanLines) + + var uuid *string + var seqno *int + + for fileScanner.Scan() { + parts := strings.Split(fileScanner.Text(), "WSREP: Recovered position: ") + if len(parts) != 2 { + continue + } + parts = strings.Split(parts[1], ":") + if len(parts) != 2 { + continue + } + // TODO: validate UUID + parsedUUID := strings.TrimSpace(parts[0]) + parsedSeqno, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + return fmt.Errorf("error parsing seqno: %v", err) + } + uuid = &parsedUUID + seqno = &parsedSeqno + } + if uuid == nil || seqno == nil { + return fmt.Errorf( + "unable to parse uuid and seqno: uuid=%v seqno=%v", + uuid, seqno, + ) + } + r.UUID = *uuid + r.Seqno = *seqno + return nil +} + +func (r *Recover) Validate() error { + if r.UUID == "" || r.Seqno == 0 { + return fmt.Errorf("uuid and seqno are mandatory") + } + return nil +} + func createTpl(name, t string) *template.Template { return template.Must(template.New(name).Parse(t)) } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 03d7e0a..2221b63 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -33,7 +33,10 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Hand }, Recovery: &Recovery{ fileManager: fileManager, - logger: &recoveryLogger, + jsonEncoder: &jsonEncoder{ + logger: &recoveryLogger, + }, + logger: &recoveryLogger, }, } } diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index 6760d2d..8b5ff95 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -1,8 +1,10 @@ package handler import ( + "fmt" "net/http" "os" + "time" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" @@ -10,14 +12,24 @@ import ( "github.com/mariadb-operator/agent/pkg/mariadbd" ) +var ( + recoverRetries = 10 + recoverWait = 3 * time.Second +) + type Recovery struct { fileManager *filemanager.FileManager + jsonEncoder *jsonEncoder logger *logr.Logger } func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { + if err := h.fileManager.DeleteStateFile(galera.RecoveryLog); err != nil && !os.IsNotExist(err) { + h.logger.Error(err, "error deleting existing recovery log") + } + if err := h.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { - h.logger.Error(err, "error writing recovery file") + h.logger.Error(err, "error writing recovery config") http.Error(w, "Internal server error", http.StatusInternalServerError) return } @@ -29,7 +41,20 @@ func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { h.logger.Info("mariadbd process reloaded") } - w.WriteHeader(http.StatusOK) + recover, err := h.recover() + if err != nil { + h.logger.Error(err, "error recovering galera") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { + h.logger.Error(err, "error deleting recovery file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + h.jsonEncoder.encode(w, recover) } func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { @@ -38,9 +63,29 @@ func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not found", http.StatusNotFound) return } - h.logger.Error(err, "error deleting recovery file") http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } + +func (h *Recovery) recover() (*galera.Recover, error) { + for i := 0; i < recoverRetries; i++ { + time.Sleep(recoverWait) + + bytes, err := h.fileManager.ReadStateFile(galera.RecoveryLog) + if err != nil { + h.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", recoverRetries) + continue + } + + var recover galera.Recover + err = recover.UnmarshalText(bytes) + if err == nil { + return &recover, nil + } + + h.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", recoverRetries) + } + return nil, fmt.Errorf("maximum retries (%d) reached attempting to recover galera from recovery log", recoverRetries) +} From 8ac22f9f7f81df23025ce4bdbed81cbe12a29ada Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 2 Jun 2023 20:43:49 +0200 Subject: [PATCH 17/27] Setting UUID and seqno when bootstrapping --- pkg/galera/galera.go | 6 +++--- pkg/handler/bootstrap.go | 19 +++++++++++++++++-- pkg/handler/recovery.go | 10 +++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index dd6f390..73e2f30 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -108,12 +108,12 @@ func (g *GaleraState) UnmarshalText(text []byte) error { return nil } -type Recover struct { +type Bootstrap struct { UUID string `json:"uuid"` Seqno int `json:"seqno"` } -func (r *Recover) UnmarshalText(text []byte) error { +func (r *Bootstrap) Unmarshal(text []byte) error { fileScanner := bufio.NewScanner(bytes.NewReader(text)) fileScanner.Split(bufio.ScanLines) @@ -149,7 +149,7 @@ func (r *Recover) UnmarshalText(text []byte) error { return nil } -func (r *Recover) Validate() error { +func (r *Bootstrap) Validate() error { if r.UUID == "" || r.Seqno == 0 { return fmt.Errorf("uuid and seqno are mandatory") } diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index e0eead0..40660fe 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -1,6 +1,7 @@ package handler import ( + "encoding/json" "fmt" "net/http" "os" @@ -17,7 +18,19 @@ type Bootstrap struct { } func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { - if err := h.setSafeToBootstrap(); err != nil { + var bootstrap galera.Bootstrap + if err := json.NewDecoder(r.Body).Decode(&bootstrap); err != nil { + h.logger.Error(err, "error decoding bootstrap") + http.Error(w, "invalid body: a bootstrap object must be provided", http.StatusBadRequest) + return + } + + if err := bootstrap.Validate(); err != nil { + http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) + return + } + + if err := h.setSafeToBootstrap(&bootstrap); err != nil { h.logger.Error(err, "error setting safe to bootstrap") http.Error(w, "Internal server error", http.StatusInternalServerError) return @@ -53,7 +66,7 @@ func (h *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (h *Bootstrap) setSafeToBootstrap() error { +func (h *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { @@ -67,6 +80,8 @@ func (h *Bootstrap) setSafeToBootstrap() error { return fmt.Errorf("error unmarshaling galera state: %v", err) } + galeraState.UUID = bootstrap.UUID + galeraState.Seqno = bootstrap.Seqno galeraState.SafeToBootstrap = true bytes, err = galeraState.MarshalText() if err != nil { diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index 8b5ff95..a25feec 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -41,7 +41,7 @@ func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { h.logger.Info("mariadbd process reloaded") } - recover, err := h.recover() + bootstrap, err := h.recover() if err != nil { h.logger.Error(err, "error recovering galera") http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -54,7 +54,7 @@ func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { return } - h.jsonEncoder.encode(w, recover) + h.jsonEncoder.encode(w, bootstrap) } func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { @@ -69,7 +69,7 @@ func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (h *Recovery) recover() (*galera.Recover, error) { +func (h *Recovery) recover() (*galera.Bootstrap, error) { for i := 0; i < recoverRetries; i++ { time.Sleep(recoverWait) @@ -79,8 +79,8 @@ func (h *Recovery) recover() (*galera.Recover, error) { continue } - var recover galera.Recover - err = recover.UnmarshalText(bytes) + var recover galera.Bootstrap + err = recover.Unmarshal(bytes) if err == nil { return &recover, nil } From 2f92ae597011eeda3da02087017fbfa4df0c1171 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 2 Jun 2023 22:35:25 +0200 Subject: [PATCH 18/27] Avoid issues with JSON Marshaler by not implementing TextMarshaler --- pkg/galera/galera.go | 4 ++-- pkg/handler/bootstrap.go | 4 ++-- pkg/handler/galerastate.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index 73e2f30..0f159ac 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -28,7 +28,7 @@ type GaleraState struct { SafeToBootstrap bool `json:"safeToBootstrap"` } -func (g *GaleraState) MarshalText() ([]byte, error) { +func (g *GaleraState) Marshal() ([]byte, error) { type tplOpts struct { Version string UUID string @@ -57,7 +57,7 @@ safe_to_bootstrap: {{ .SafeToBootstrap }}`) return buf.Bytes(), nil } -func (g *GaleraState) UnmarshalText(text []byte) error { +func (g *GaleraState) Unmarshal(text []byte) error { fileScanner := bufio.NewScanner(bytes.NewReader(text)) fileScanner.Split(bufio.ScanLines) diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index 40660fe..1c21a9d 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -76,14 +76,14 @@ func (h *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { } var galeraState galera.GaleraState - if err := galeraState.UnmarshalText(bytes); err != nil { + if err := galeraState.Unmarshal(bytes); err != nil { return fmt.Errorf("error unmarshaling galera state: %v", err) } galeraState.UUID = bootstrap.UUID galeraState.Seqno = bootstrap.Seqno galeraState.SafeToBootstrap = true - bytes, err = galeraState.MarshalText() + bytes, err = galeraState.Marshal() if err != nil { return fmt.Errorf("error marshallng galera state: %v", err) } diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate.go index 574d707..3f6e58d 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate.go @@ -28,7 +28,7 @@ func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { } var galeraState galera.GaleraState - if err := galeraState.UnmarshalText(bytes); err != nil { + if err := galeraState.Unmarshal(bytes); err != nil { h.logger.Error(err, "error unmarshalling galera state") http.Error(w, "Internal server error", http.StatusInternalServerError) return From 9fd167f2ebccd4ec23df79fa1cacaac45302fc1f Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 2 Jun 2023 22:50:54 +0200 Subject: [PATCH 19/27] Fix validation --- pkg/handler/bootstrap.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index 1c21a9d..3bd10bf 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -21,12 +21,7 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { var bootstrap galera.Bootstrap if err := json.NewDecoder(r.Body).Decode(&bootstrap); err != nil { h.logger.Error(err, "error decoding bootstrap") - http.Error(w, "invalid body: a bootstrap object must be provided", http.StatusBadRequest) - return - } - - if err := bootstrap.Validate(); err != nil { - http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) + http.Error(w, "invalid body: a valid bootstrap object must be provided", http.StatusBadRequest) return } From ab397a1d49fb785d993a236b341cd71e73e378bc Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Fri, 2 Jun 2023 23:25:13 +0200 Subject: [PATCH 20/27] Validating UUIDs --- go.mod | 1 + go.sum | 4 ++-- pkg/galera/galera.go | 35 +++++++++++++++++++++-------------- pkg/handler/bootstrap.go | 5 +++++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 519a970..32eae71 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/cobra v1.7.0 go.uber.org/zap v1.24.0 sigs.k8s.io/controller-runtime v0.15.0 + github.com/google/uuid v1.3.0 ) require ( diff --git a/go.sum b/go.sum index 896f1f6..13809ab 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -20,6 +18,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index 0f159ac..ff964e6 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -7,6 +7,8 @@ import ( "html/template" "strconv" "strings" + + guuid "github.com/google/uuid" ) var ( @@ -78,7 +80,9 @@ func (g *GaleraState) Unmarshal(text []byte) error { case "version": version = &value case "uuid": - // TODO: validate UUID + if _, err := guuid.Parse(value); err != nil { + return fmt.Errorf("error parsing uuid: %v", err) + } uuid = &value case "seqno": i, err := strconv.Atoi(value) @@ -113,6 +117,13 @@ type Bootstrap struct { Seqno int `json:"seqno"` } +func (b *Bootstrap) Validate() error { + if _, err := guuid.Parse(b.UUID); err != nil { + return fmt.Errorf("invalid uuid: %v", err) + } + return nil +} + func (r *Bootstrap) Unmarshal(text []byte) error { fileScanner := bufio.NewScanner(bytes.NewReader(text)) fileScanner.Split(bufio.ScanLines) @@ -129,18 +140,21 @@ func (r *Bootstrap) Unmarshal(text []byte) error { if len(parts) != 2 { continue } - // TODO: validate UUID - parsedUUID := strings.TrimSpace(parts[0]) - parsedSeqno, err := strconv.Atoi(strings.TrimSpace(parts[1])) + + currentUUID := strings.TrimSpace(parts[0]) + if _, err := guuid.Parse(currentUUID); err != nil { + return fmt.Errorf("error parsing uuid: %v", err) + } + currentSeqno, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil { return fmt.Errorf("error parsing seqno: %v", err) } - uuid = &parsedUUID - seqno = &parsedSeqno + uuid = ¤tUUID + seqno = ¤tSeqno } if uuid == nil || seqno == nil { return fmt.Errorf( - "unable to parse uuid and seqno: uuid=%v seqno=%v", + "unable to find uuid and seqno: uuid=%v seqno=%v", uuid, seqno, ) } @@ -149,13 +163,6 @@ func (r *Bootstrap) Unmarshal(text []byte) error { return nil } -func (r *Bootstrap) Validate() error { - if r.UUID == "" || r.Seqno == 0 { - return fmt.Errorf("uuid and seqno are mandatory") - } - return nil -} - func createTpl(name, t string) *template.Template { return template.Must(template.New(name).Parse(t)) } diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index 3bd10bf..a373c0f 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -24,6 +24,11 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid body: a valid bootstrap object must be provided", http.StatusBadRequest) return } + if err := bootstrap.Validate(); err != nil { + h.logger.Error(err, "invalid bootstrap") + http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) + return + } if err := h.setSafeToBootstrap(&bootstrap); err != nil { h.logger.Error(err, "error setting safe to bootstrap") From daf08cb49256696ca8448a1a47d19bd510de1c66 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 10:44:12 +0200 Subject: [PATCH 21/27] Mariadbd reload retries --- pkg/handler/bootstrap.go | 8 +++++++- pkg/handler/recovery.go | 8 +++++--- pkg/mariadbd/mariadbd.go | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index a373c0f..d0e9720 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os" + "time" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" @@ -12,6 +13,11 @@ import ( "github.com/mariadb-operator/agent/pkg/mariadbd" ) +var ( + bootstrapMariadbdReloadRetries = 10 + bootstrapMariadbdReloadWait = 1 * time.Second +) + type Bootstrap struct { fileManager *filemanager.FileManager logger *logr.Logger @@ -43,7 +49,7 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { } h.logger.Info("reloading mariadbd process") - if err := mariadbd.Reload(); err != nil { + if err := mariadbd.ReloadWithRetries(bootstrapMariadbdReloadRetries, bootstrapMariadbdReloadWait); err != nil { h.logger.Error(err, "error reloading mariadbd process") http.Error(w, "Internal server error", http.StatusInternalServerError) return diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index a25feec..63681fb 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -13,8 +13,10 @@ import ( ) var ( - recoverRetries = 10 - recoverWait = 3 * time.Second + recoverMariadbdReloadRetries = 3 + recoverMariadbdReloadWait = 1 * time.Second + recoverRetries = 10 + recoverWait = 3 * time.Second ) type Recovery struct { @@ -35,7 +37,7 @@ func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { } h.logger.Info("reloading mariadbd process") - if err := mariadbd.Reload(); err != nil { + if err := mariadbd.ReloadWithRetries(recoverMariadbdReloadRetries, recoverMariadbdReloadWait); err != nil { h.logger.Error(err, "error reloading mariadbd process") } else { h.logger.Info("mariadbd process reloaded") diff --git a/pkg/mariadbd/mariadbd.go b/pkg/mariadbd/mariadbd.go index a946de7..778aa7d 100644 --- a/pkg/mariadbd/mariadbd.go +++ b/pkg/mariadbd/mariadbd.go @@ -1,8 +1,10 @@ package mariadbd import ( + "errors" "fmt" "syscall" + "time" "github.com/mitchellh/go-ps" ) @@ -10,6 +12,7 @@ import ( var ( mariadbdProcessName = "mariadbd" reloadSysCall = syscall.SIGKILL + errProcessNotFound = fmt.Errorf("process '%s' not found", mariadbdProcessName) ) func Reload() error { @@ -27,5 +30,21 @@ func Reload() error { } } - return fmt.Errorf("process '%s' not found", mariadbdProcessName) + return errProcessNotFound +} + +func ReloadWithRetries(retries int, waitRetry time.Duration) error { + for i := 0; i < retries; i++ { + err := Reload() + if err == nil { + return nil + } + if errors.Is(err, errProcessNotFound) { + time.Sleep(waitRetry) + continue + } + return err + } + return fmt.Errorf("maximum retries (%d) reached attempting to reload '%s' process", retries, mariadbdProcessName) + } From c637f5cade39ac13428acd161d0339d1e5018a32 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 11:22:16 +0200 Subject: [PATCH 22/27] Bootstrap and recovery mutually exclusive --- pkg/galera/galera.go | 8 ++++---- pkg/handler/bootstrap.go | 6 ++++++ pkg/handler/recovery.go | 12 ++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index ff964e6..4b44124 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -16,11 +16,11 @@ var ( BootstrapFileName = "1-bootstrap.cnf" BootstrapFile = `[galera] wsrep_new_cluster="ON"` - RecoveryFileName = "2-recovery.cnf" - RecoveryLog = "mariadb.err" - RecoveryFile = fmt.Sprintf(`[galera] + RecoveryFileName = "2-recovery.cnf" + RecoveryLogFileName = "mariadb.err" + RecoveryFile = fmt.Sprintf(`[galera] log_error=%s -wsrep_recover="ON"`, RecoveryLog) +wsrep_recover="ON"`, RecoveryLogFileName) ) type GaleraState struct { diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap.go index d0e9720..723b871 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap.go @@ -36,6 +36,12 @@ func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { return } + if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil && !os.IsNotExist(err) { + h.logger.Error(err, "error deleting existing recovery config") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + if err := h.setSafeToBootstrap(&bootstrap); err != nil { h.logger.Error(err, "error setting safe to bootstrap") http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go index 63681fb..f950ec9 100644 --- a/pkg/handler/recovery.go +++ b/pkg/handler/recovery.go @@ -26,8 +26,16 @@ type Recovery struct { } func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { - if err := h.fileManager.DeleteStateFile(galera.RecoveryLog); err != nil && !os.IsNotExist(err) { + if err := h.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil && !os.IsNotExist(err) { + h.logger.Error(err, "error deleting existing bootstrap config") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if err := h.fileManager.DeleteStateFile(galera.RecoveryLogFileName); err != nil && !os.IsNotExist(err) { h.logger.Error(err, "error deleting existing recovery log") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return } if err := h.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { @@ -75,7 +83,7 @@ func (h *Recovery) recover() (*galera.Bootstrap, error) { for i := 0; i < recoverRetries; i++ { time.Sleep(recoverWait) - bytes, err := h.fileManager.ReadStateFile(galera.RecoveryLog) + bytes, err := h.fileManager.ReadStateFile(galera.RecoveryLogFileName) if err != nil { h.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", recoverRetries) continue From caf74e30d8648f585fa7645991860d46cc7e79fc Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 13:04:27 +0200 Subject: [PATCH 23/27] Support for retry configuration --- pkg/handler/{ => bootstrap}/bootstrap.go | 71 ++++++---- pkg/handler/{ => galerastate}/galerastate.go | 23 ++- pkg/handler/handler.go | 83 ++++++----- pkg/handler/jsonencoder/jsonencoder.go | 28 ++++ pkg/handler/recovery.go | 101 ------------- pkg/handler/recovery/recovery.go | 142 +++++++++++++++++++ pkg/mariadbd/mariadbd.go | 14 +- 7 files changed, 287 insertions(+), 175 deletions(-) rename pkg/handler/{ => bootstrap}/bootstrap.go (51%) rename pkg/handler/{ => galerastate}/galerastate.go (50%) create mode 100644 pkg/handler/jsonencoder/jsonencoder.go delete mode 100644 pkg/handler/recovery.go create mode 100644 pkg/handler/recovery/recovery.go diff --git a/pkg/handler/bootstrap.go b/pkg/handler/bootstrap/bootstrap.go similarity index 51% rename from pkg/handler/bootstrap.go rename to pkg/handler/bootstrap/bootstrap.go index 723b871..b572ef1 100644 --- a/pkg/handler/bootstrap.go +++ b/pkg/handler/bootstrap/bootstrap.go @@ -1,4 +1,4 @@ -package handler +package bootstrap import ( "encoding/json" @@ -14,72 +14,95 @@ import ( ) var ( - bootstrapMariadbdReloadRetries = 10 - bootstrapMariadbdReloadWait = 1 * time.Second + defaultMariadbdRetryOpts = mariadbd.RetryOptions{ + Retries: 10, + WaitRetry: 1 * time.Second, + } ) type Bootstrap struct { - fileManager *filemanager.FileManager - logger *logr.Logger + fileManager *filemanager.FileManager + logger *logr.Logger + mariadbdRetryOptions *mariadbd.RetryOptions +} + +type Option func(*Bootstrap) + +func WithMariadbdRetry(opts *mariadbd.RetryOptions) Option { + return func(b *Bootstrap) { + b.mariadbdRetryOptions = opts + } +} + +func NewBootstrap(fileManager *filemanager.FileManager, logger *logr.Logger, opts ...Option) *Bootstrap { + bootstrap := &Bootstrap{ + fileManager: fileManager, + logger: logger, + mariadbdRetryOptions: &defaultMariadbdRetryOpts, + } + for _, setOpts := range opts { + setOpts(bootstrap) + } + return bootstrap } -func (h *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { +func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { var bootstrap galera.Bootstrap if err := json.NewDecoder(r.Body).Decode(&bootstrap); err != nil { - h.logger.Error(err, "error decoding bootstrap") + b.logger.Error(err, "error decoding bootstrap") http.Error(w, "invalid body: a valid bootstrap object must be provided", http.StatusBadRequest) return } if err := bootstrap.Validate(); err != nil { - h.logger.Error(err, "invalid bootstrap") + b.logger.Error(err, "invalid bootstrap") http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) return } - if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil && !os.IsNotExist(err) { - h.logger.Error(err, "error deleting existing recovery config") + if err := b.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil && !os.IsNotExist(err) { + b.logger.Error(err, "error deleting existing recovery config") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - if err := h.setSafeToBootstrap(&bootstrap); err != nil { - h.logger.Error(err, "error setting safe to bootstrap") + if err := b.setSafeToBootstrap(&bootstrap); err != nil { + b.logger.Error(err, "error setting safe to bootstrap") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - if err := h.fileManager.WriteConfigFile(galera.BootstrapFileName, []byte(galera.BootstrapFile)); err != nil { - h.logger.Error(err, "error writing bootstrap file") + if err := b.fileManager.WriteConfigFile(galera.BootstrapFileName, []byte(galera.BootstrapFile)); err != nil { + b.logger.Error(err, "error writing bootstrap file") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.logger.Info("reloading mariadbd process") - if err := mariadbd.ReloadWithRetries(bootstrapMariadbdReloadRetries, bootstrapMariadbdReloadWait); err != nil { - h.logger.Error(err, "error reloading mariadbd process") + b.logger.Info("reloading mariadbd process") + if err := mariadbd.ReloadWithRetries(b.mariadbdRetryOptions); err != nil { + b.logger.Error(err, "error reloading mariadbd process") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.logger.Info("mariadbd process reloaded") + b.logger.Info("mariadbd process reloaded") w.WriteHeader(http.StatusOK) } -func (h *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { - if err := h.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil { +func (b *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { + if err := b.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) return } - h.logger.Error(err, "error deleting bootstrap file") + b.logger.Error(err, "error deleting bootstrap file") http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } -func (h *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { - bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFileName) +func (b *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { + bytes, err := b.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { return nil @@ -100,7 +123,7 @@ func (h *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { return fmt.Errorf("error marshallng galera state: %v", err) } - if err := h.fileManager.WriteStateFile(galera.GaleraStateFileName, bytes); err != nil { + if err := b.fileManager.WriteStateFile(galera.GaleraStateFileName, bytes); err != nil { return fmt.Errorf("error writing galera state: %v", err) } return nil diff --git a/pkg/handler/galerastate.go b/pkg/handler/galerastate/galerastate.go similarity index 50% rename from pkg/handler/galerastate.go rename to pkg/handler/galerastate/galerastate.go index 3f6e58d..c83f86b 100644 --- a/pkg/handler/galerastate.go +++ b/pkg/handler/galerastate/galerastate.go @@ -1,4 +1,4 @@ -package handler +package galerastate import ( "net/http" @@ -7,31 +7,40 @@ import ( "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/galera" + "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" ) type GaleraState struct { fileManager *filemanager.FileManager - jsonEncoder *jsonEncoder + jsonEncoder *jsonencoder.JSONEncoder logger *logr.Logger } -func (h *GaleraState) Get(w http.ResponseWriter, r *http.Request) { - bytes, err := h.fileManager.ReadStateFile(galera.GaleraStateFileName) +func NewGaleraState(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, logger *logr.Logger) *GaleraState { + return &GaleraState{ + fileManager: fileManager, + jsonEncoder: jsonEncoder, + logger: logger, + } +} + +func (g *GaleraState) Get(w http.ResponseWriter, r *http.Request) { + bytes, err := g.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) return } - h.logger.Error(err, "error reading file") + g.logger.Error(err, "error reading file") http.Error(w, "Internal server error", http.StatusInternalServerError) return } var galeraState galera.GaleraState if err := galeraState.Unmarshal(bytes); err != nil { - h.logger.Error(err, "error unmarshalling galera state") + g.logger.Error(err, "error unmarshalling galera state") http.Error(w, "Internal server error", http.StatusInternalServerError) return } - h.jsonEncoder.encode(w, galeraState) + g.jsonEncoder.Encode(w, galeraState) } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 2221b63..bf63e91 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -1,56 +1,63 @@ package handler import ( - "encoding/json" - "net/http" - "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" + "github.com/mariadb-operator/agent/pkg/handler/bootstrap" + "github.com/mariadb-operator/agent/pkg/handler/galerastate" + "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" + "github.com/mariadb-operator/agent/pkg/handler/recovery" + "github.com/mariadb-operator/agent/pkg/mariadbd" ) type Handler struct { - Bootstrap *Bootstrap - GaleraState *GaleraState - Recovery *Recovery + Bootstrap *bootstrap.Bootstrap + GaleraState *galerastate.GaleraState + Recovery *recovery.Recovery } -func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger) *Handler { - galeraStateLogger := logger.WithName("galerastate") - bootstrapLogger := logger.WithName("bootstrap") - recoveryLogger := logger.WithName("recovery") +type Options struct { + bootstrap []bootstrap.Option + recovery []recovery.Option +} - return &Handler{ - Bootstrap: &Bootstrap{ - fileManager: fileManager, - logger: &bootstrapLogger, - }, - GaleraState: &GaleraState{ - fileManager: fileManager, - jsonEncoder: &jsonEncoder{ - logger: &galeraStateLogger, - }, - logger: &galeraStateLogger, - }, - Recovery: &Recovery{ - fileManager: fileManager, - jsonEncoder: &jsonEncoder{ - logger: &recoveryLogger, - }, - logger: &recoveryLogger, - }, +type Option func(*Options) + +func WithBootstrapMariadbRetryOptions(opts *mariadbd.RetryOptions) Option { + return func(o *Options) { + o.bootstrap = append(o.bootstrap, bootstrap.WithMariadbdRetry(opts)) } } -type jsonEncoder struct { - logger *logr.Logger +func WithRecoveryMariadbRetryOptions(opts *mariadbd.RetryOptions) Option { + return func(o *Options) { + o.recovery = append(o.recovery, recovery.WithMariadbdRetry(opts)) + } } -func (j *jsonEncoder) encode(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(v); err != nil { - j.logger.Error(err, "error encoding json") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return +func WithRecoveryRetryOptions(opts *recovery.RecoverRetryOptions) Option { + return func(o *Options) { + o.recovery = append(o.recovery, recovery.WithRecoverRetry(opts)) + } +} + +func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger, handlerOpts ...Option) *Handler { + opts := &Options{} + for _, setOpts := range handlerOpts { + setOpts(opts) + } + + bootstrapLogger := logger.WithName("bootstrap") + galeraStateLogger := logger.WithName("galerastate") + recoveryLogger := logger.WithName("recovery") + + bootstrap := bootstrap.NewBootstrap(fileManager, &bootstrapLogger, opts.bootstrap...) + galerastate := galerastate.NewGaleraState(fileManager, jsonencoder.NewJSONEncoder(&galeraStateLogger), &galeraStateLogger) + recovery := recovery.NewRecover(fileManager, jsonencoder.NewJSONEncoder(&recoveryLogger), &recoveryLogger, opts.recovery...) + + return &Handler{ + Bootstrap: bootstrap, + GaleraState: galerastate, + Recovery: recovery, } - w.WriteHeader(http.StatusOK) } diff --git a/pkg/handler/jsonencoder/jsonencoder.go b/pkg/handler/jsonencoder/jsonencoder.go new file mode 100644 index 0000000..1890cd5 --- /dev/null +++ b/pkg/handler/jsonencoder/jsonencoder.go @@ -0,0 +1,28 @@ +package jsonencoder + +import ( + "encoding/json" + "net/http" + + "github.com/go-logr/logr" +) + +type JSONEncoder struct { + logger *logr.Logger +} + +func NewJSONEncoder(logger *logr.Logger) *JSONEncoder { + return &JSONEncoder{ + logger: logger, + } +} + +func (j *JSONEncoder) Encode(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + j.logger.Error(err, "error encoding json") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/handler/recovery.go b/pkg/handler/recovery.go deleted file mode 100644 index f950ec9..0000000 --- a/pkg/handler/recovery.go +++ /dev/null @@ -1,101 +0,0 @@ -package handler - -import ( - "fmt" - "net/http" - "os" - "time" - - "github.com/go-logr/logr" - "github.com/mariadb-operator/agent/pkg/filemanager" - "github.com/mariadb-operator/agent/pkg/galera" - "github.com/mariadb-operator/agent/pkg/mariadbd" -) - -var ( - recoverMariadbdReloadRetries = 3 - recoverMariadbdReloadWait = 1 * time.Second - recoverRetries = 10 - recoverWait = 3 * time.Second -) - -type Recovery struct { - fileManager *filemanager.FileManager - jsonEncoder *jsonEncoder - logger *logr.Logger -} - -func (h *Recovery) Put(w http.ResponseWriter, r *http.Request) { - if err := h.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil && !os.IsNotExist(err) { - h.logger.Error(err, "error deleting existing bootstrap config") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if err := h.fileManager.DeleteStateFile(galera.RecoveryLogFileName); err != nil && !os.IsNotExist(err) { - h.logger.Error(err, "error deleting existing recovery log") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if err := h.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { - h.logger.Error(err, "error writing recovery config") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - h.logger.Info("reloading mariadbd process") - if err := mariadbd.ReloadWithRetries(recoverMariadbdReloadRetries, recoverMariadbdReloadWait); err != nil { - h.logger.Error(err, "error reloading mariadbd process") - } else { - h.logger.Info("mariadbd process reloaded") - } - - bootstrap, err := h.recover() - if err != nil { - h.logger.Error(err, "error recovering galera") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { - h.logger.Error(err, "error deleting recovery file") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - h.jsonEncoder.encode(w, bootstrap) -} - -func (h *Recovery) Delete(w http.ResponseWriter, r *http.Request) { - if err := h.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { - if os.IsNotExist(err) { - http.Error(w, "Not found", http.StatusNotFound) - return - } - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) -} - -func (h *Recovery) recover() (*galera.Bootstrap, error) { - for i := 0; i < recoverRetries; i++ { - time.Sleep(recoverWait) - - bytes, err := h.fileManager.ReadStateFile(galera.RecoveryLogFileName) - if err != nil { - h.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", recoverRetries) - continue - } - - var recover galera.Bootstrap - err = recover.Unmarshal(bytes) - if err == nil { - return &recover, nil - } - - h.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", recoverRetries) - } - return nil, fmt.Errorf("maximum retries (%d) reached attempting to recover galera from recovery log", recoverRetries) -} diff --git a/pkg/handler/recovery/recovery.go b/pkg/handler/recovery/recovery.go new file mode 100644 index 0000000..859c8d0 --- /dev/null +++ b/pkg/handler/recovery/recovery.go @@ -0,0 +1,142 @@ +package recovery + +import ( + "fmt" + "net/http" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/filemanager" + "github.com/mariadb-operator/agent/pkg/galera" + "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" + "github.com/mariadb-operator/agent/pkg/mariadbd" +) + +var ( + defaultMariadbdRetryOpts = mariadbd.RetryOptions{ + Retries: 3, + WaitRetry: 1 * time.Second, + } + defaultRecoverRetryOpts = RecoverRetryOptions{ + Retries: 10, + WaitRetry: 3 * time.Second, + } +) + +type RecoverRetryOptions struct { + Retries int + WaitRetry time.Duration +} + +type Recovery struct { + fileManager *filemanager.FileManager + jsonEncoder *jsonencoder.JSONEncoder + logger *logr.Logger + mariadbdRetryOptions *mariadbd.RetryOptions + recoverRetryOptions *RecoverRetryOptions +} + +type Option func(*Recovery) + +func WithMariadbdRetry(opts *mariadbd.RetryOptions) Option { + return func(b *Recovery) { + b.mariadbdRetryOptions = opts + } +} + +func WithRecoverRetry(opts *RecoverRetryOptions) Option { + return func(r *Recovery) { + r.recoverRetryOptions = opts + } +} + +func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, logger *logr.Logger, + opts ...Option) *Recovery { + recovery := &Recovery{ + fileManager: fileManager, + jsonEncoder: jsonEncoder, + logger: logger, + mariadbdRetryOptions: &defaultMariadbdRetryOpts, + recoverRetryOptions: &defaultRecoverRetryOpts, + } + for _, setOpts := range opts { + setOpts(recovery) + } + return recovery +} + +func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { + if err := r.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil && !os.IsNotExist(err) { + r.logger.Error(err, "error deleting existing bootstrap config") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if err := r.fileManager.DeleteStateFile(galera.RecoveryLogFileName); err != nil && !os.IsNotExist(err) { + r.logger.Error(err, "error deleting existing recovery log") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if err := r.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { + r.logger.Error(err, "error writing recovery config") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + r.logger.Info("reloading mariadbd process") + if err := mariadbd.ReloadWithRetries(r.mariadbdRetryOptions); err != nil { + r.logger.Error(err, "error reloading mariadbd process") + } else { + r.logger.Info("mariadbd process reloaded") + } + + bootstrap, err := r.recover() + if err != nil { + r.logger.Error(err, "error recovering galera") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if err := r.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { + r.logger.Error(err, "error deleting recovery file") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + r.jsonEncoder.Encode(w, bootstrap) +} + +func (r *Recovery) Delete(w http.ResponseWriter, req *http.Request) { + if err := r.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { + if os.IsNotExist(err) { + http.Error(w, "Not found", http.StatusNotFound) + return + } + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +func (r *Recovery) recover() (*galera.Bootstrap, error) { + for i := 0; i < r.recoverRetryOptions.Retries; i++ { + time.Sleep(r.recoverRetryOptions.WaitRetry) + + bytes, err := r.fileManager.ReadStateFile(galera.RecoveryLogFileName) + if err != nil { + r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoverRetryOptions.Retries) + continue + } + + var recover galera.Bootstrap + err = recover.Unmarshal(bytes) + if err == nil { + return &recover, nil + } + + r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoverRetryOptions.Retries) + } + return nil, fmt.Errorf("maximum retries (%d) reached attempting to recover galera from recovery log", r.recoverRetryOptions.Retries) +} diff --git a/pkg/mariadbd/mariadbd.go b/pkg/mariadbd/mariadbd.go index 778aa7d..d13adb2 100644 --- a/pkg/mariadbd/mariadbd.go +++ b/pkg/mariadbd/mariadbd.go @@ -33,18 +33,22 @@ func Reload() error { return errProcessNotFound } -func ReloadWithRetries(retries int, waitRetry time.Duration) error { - for i := 0; i < retries; i++ { +type RetryOptions struct { + Retries int + WaitRetry time.Duration +} + +func ReloadWithRetries(opts *RetryOptions) error { + for i := 0; i < opts.Retries; i++ { err := Reload() if err == nil { return nil } if errors.Is(err, errProcessNotFound) { - time.Sleep(waitRetry) + time.Sleep(opts.WaitRetry) continue } return err } - return fmt.Errorf("maximum retries (%d) reached attempting to reload '%s' process", retries, mariadbdProcessName) - + return fmt.Errorf("maximum retries (%d) reached attempting to reload '%s' process", opts.Retries, mariadbdProcessName) } From 568d39413750c8748aab9d068af0d4232c725943 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 19:54:15 +0200 Subject: [PATCH 24/27] Retry flags for bootstrap and recovery --- cmd/agent/root.go | 44 +++++++++++++++++++++++++++- make/dev.mk | 6 ++-- pkg/handler/bootstrap/bootstrap.go | 20 ++++++------- pkg/handler/handler.go | 16 +++-------- pkg/handler/recovery/recovery.go | 46 +++++++++++++++--------------- pkg/mariadbd/mariadbd.go | 4 +-- 6 files changed, 86 insertions(+), 50 deletions(-) diff --git a/cmd/agent/root.go b/cmd/agent/root.go index 8540875..05c32b8 100644 --- a/cmd/agent/root.go +++ b/cmd/agent/root.go @@ -8,7 +8,10 @@ import ( "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/handler" + "github.com/mariadb-operator/agent/pkg/handler/bootstrap" + "github.com/mariadb-operator/agent/pkg/handler/recovery" "github.com/mariadb-operator/agent/pkg/logger" + "github.com/mariadb-operator/agent/pkg/mariadbd" "github.com/mariadb-operator/agent/pkg/router" "github.com/mariadb-operator/agent/pkg/server" "github.com/spf13/cobra" @@ -26,6 +29,14 @@ var ( logLevel string logTimeEncoder string logDev bool + + bootstrapMariadbdReloadRetries int + bootstrapMariadbdReloadRetryWait time.Duration + + recoveryMariadbdReloadRetries int + recoveryMariadbdReloadRetryWait time.Duration + recoveryRetries int + recoveryRetryWait time.Duration ) var rootCmd = &cobra.Command{ @@ -50,7 +61,24 @@ var rootCmd = &cobra.Command{ } handlerLogger := logger.WithName("handler") - handler := handler.NewHandler(fileManager, &handlerLogger) + handler := handler.NewHandler(fileManager, &handlerLogger, + handler.WithBootstrapOptions( + bootstrap.WithMariadbdReload(&mariadbd.ReloadOptions{ + Retries: bootstrapMariadbdReloadRetries, + WaitRetry: bootstrapMariadbdReloadRetryWait, + }), + ), + handler.WithRecoveryOptions( + recovery.WithMariadbdReload(&mariadbd.ReloadOptions{ + Retries: recoveryMariadbdReloadRetries, + WaitRetry: recoveryMariadbdReloadRetryWait, + }), + recovery.WithRecovery(&recovery.RecoveryOptions{ + Retries: recoveryRetries, + WaitRetry: recoveryRetryWait, + }), + ), + ) router := router.NewRouter( handler, @@ -85,4 +113,18 @@ func init() { rootCmd.Flags().StringVar(&logTimeEncoder, "log-time-encoder", "epoch", "Log time encoder to use, one of: "+ "epoch, millis, nano, iso8601, rfc3339 or rfc3339nano") rootCmd.Flags().BoolVar(&logDev, "log-dev", false, "Enable development logs.") + + rootCmd.Flags().IntVar(&bootstrapMariadbdReloadRetries, "bootstrap-mariadbd-reload-retries", 10, "Maximum number of attempts "+ + "to reload MariaDB process when bootstraping a new Galera cluster") + rootCmd.Flags().DurationVar(&bootstrapMariadbdReloadRetryWait, "bootstrap-mariadbd-reload-retry-wait", 1*time.Second, + "Time to wait between MariaDB process reload attempts when bootstrapping a new Galera cluster") + + rootCmd.Flags().IntVar(&recoveryMariadbdReloadRetries, "recovery-mariadbd-reload-retries", 3, "Maximum number of attempts "+ + "to reload MariaDB process when recovering the Galera cluster") + rootCmd.Flags().DurationVar(&recoveryMariadbdReloadRetryWait, "recovery-mariadbd-reload-retry-wait", 1*time.Second, + "Time to wait between MariaDB process reload attempts when recovering the Galera cluster") + rootCmd.Flags().IntVar(&recoveryRetries, "recovery-retries", 10, "Maximum number of attempts "+ + "to recover the Galera cluster") + rootCmd.Flags().DurationVar(&recoveryRetryWait, "recovery-retry-wait", 3*time.Second, "Time to wait between "+ + "Galera cluster recover attempts ") } diff --git a/make/dev.mk b/make/dev.mk index fff35bc..a3dc845 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -34,13 +34,15 @@ state: ## Copies a example state file for development purposes. @mkdir -p $(STATE_DIR) cp "examples/$(STATE_FILE)" "$(STATE_DIR)/grastate.dat" -RUN_FLAGS ?= --log-dev --config-dir=$(CONFIG_DIR) --state-dir=$(STATE_DIR) +BASE_RUN_FLAGS ?= --log-dev + +RUN_FLAGS ?= $(BASE_RUN_FLAGS) --config-dir=$(CONFIG_DIR) --state-dir=$(STATE_DIR) .PHONY: run run: config state ## Run agent from your host. go run main.go $(RUN_FLAGS) AGENT ?= $(LOCALBIN)/agent -SU_RUN_FLAGS ?= --log-dev --config-dir=mariadb/config/docker --state-dir=mariadb/state/docker +SU_RUN_FLAGS ?= $(BASE_RUN_FLAGS) --config-dir=mariadb/config/docker --state-dir=mariadb/state/docker .PHONY: su-run su-run: build ## Run agent from your host as root to be able to access mariadb volumes and process. sudo $(AGENT) $(SU_RUN_FLAGS) \ No newline at end of file diff --git a/pkg/handler/bootstrap/bootstrap.go b/pkg/handler/bootstrap/bootstrap.go index b572ef1..96328a6 100644 --- a/pkg/handler/bootstrap/bootstrap.go +++ b/pkg/handler/bootstrap/bootstrap.go @@ -14,31 +14,31 @@ import ( ) var ( - defaultMariadbdRetryOpts = mariadbd.RetryOptions{ + defaultMariadbdReloadOpts = mariadbd.ReloadOptions{ Retries: 10, WaitRetry: 1 * time.Second, } ) type Bootstrap struct { - fileManager *filemanager.FileManager - logger *logr.Logger - mariadbdRetryOptions *mariadbd.RetryOptions + fileManager *filemanager.FileManager + logger *logr.Logger + mariadbdReloadOptions *mariadbd.ReloadOptions } type Option func(*Bootstrap) -func WithMariadbdRetry(opts *mariadbd.RetryOptions) Option { +func WithMariadbdReload(opts *mariadbd.ReloadOptions) Option { return func(b *Bootstrap) { - b.mariadbdRetryOptions = opts + b.mariadbdReloadOptions = opts } } func NewBootstrap(fileManager *filemanager.FileManager, logger *logr.Logger, opts ...Option) *Bootstrap { bootstrap := &Bootstrap{ - fileManager: fileManager, - logger: logger, - mariadbdRetryOptions: &defaultMariadbdRetryOpts, + fileManager: fileManager, + logger: logger, + mariadbdReloadOptions: &defaultMariadbdReloadOpts, } for _, setOpts := range opts { setOpts(bootstrap) @@ -78,7 +78,7 @@ func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { } b.logger.Info("reloading mariadbd process") - if err := mariadbd.ReloadWithRetries(b.mariadbdRetryOptions); err != nil { + if err := mariadbd.ReloadWithOptions(b.mariadbdReloadOptions); err != nil { b.logger.Error(err, "error reloading mariadbd process") http.Error(w, "Internal server error", http.StatusInternalServerError) return diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index bf63e91..18a0a7c 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -7,7 +7,6 @@ import ( "github.com/mariadb-operator/agent/pkg/handler/galerastate" "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" "github.com/mariadb-operator/agent/pkg/handler/recovery" - "github.com/mariadb-operator/agent/pkg/mariadbd" ) type Handler struct { @@ -23,21 +22,15 @@ type Options struct { type Option func(*Options) -func WithBootstrapMariadbRetryOptions(opts *mariadbd.RetryOptions) Option { +func WithBootstrapOptions(opts ...bootstrap.Option) Option { return func(o *Options) { - o.bootstrap = append(o.bootstrap, bootstrap.WithMariadbdRetry(opts)) + o.bootstrap = append(o.bootstrap, opts...) } } -func WithRecoveryMariadbRetryOptions(opts *mariadbd.RetryOptions) Option { +func WithRecoveryOptions(opts ...recovery.Option) Option { return func(o *Options) { - o.recovery = append(o.recovery, recovery.WithMariadbdRetry(opts)) - } -} - -func WithRecoveryRetryOptions(opts *recovery.RecoverRetryOptions) Option { - return func(o *Options) { - o.recovery = append(o.recovery, recovery.WithRecoverRetry(opts)) + o.recovery = append(o.recovery, opts...) } } @@ -46,7 +39,6 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger, handl for _, setOpts := range handlerOpts { setOpts(opts) } - bootstrapLogger := logger.WithName("bootstrap") galeraStateLogger := logger.WithName("galerastate") recoveryLogger := logger.WithName("recovery") diff --git a/pkg/handler/recovery/recovery.go b/pkg/handler/recovery/recovery.go index 859c8d0..d32612c 100644 --- a/pkg/handler/recovery/recovery.go +++ b/pkg/handler/recovery/recovery.go @@ -14,51 +14,51 @@ import ( ) var ( - defaultMariadbdRetryOpts = mariadbd.RetryOptions{ + defaultMariadbdReloadOpts = mariadbd.ReloadOptions{ Retries: 3, WaitRetry: 1 * time.Second, } - defaultRecoverRetryOpts = RecoverRetryOptions{ + defaultRecoveryOpts = RecoveryOptions{ Retries: 10, WaitRetry: 3 * time.Second, } ) -type RecoverRetryOptions struct { +type RecoveryOptions struct { Retries int WaitRetry time.Duration } type Recovery struct { - fileManager *filemanager.FileManager - jsonEncoder *jsonencoder.JSONEncoder - logger *logr.Logger - mariadbdRetryOptions *mariadbd.RetryOptions - recoverRetryOptions *RecoverRetryOptions + fileManager *filemanager.FileManager + jsonEncoder *jsonencoder.JSONEncoder + logger *logr.Logger + mariadbdReloadOptions *mariadbd.ReloadOptions + recoveryOptions *RecoveryOptions } type Option func(*Recovery) -func WithMariadbdRetry(opts *mariadbd.RetryOptions) Option { +func WithMariadbdReload(opts *mariadbd.ReloadOptions) Option { return func(b *Recovery) { - b.mariadbdRetryOptions = opts + b.mariadbdReloadOptions = opts } } -func WithRecoverRetry(opts *RecoverRetryOptions) Option { +func WithRecovery(opts *RecoveryOptions) Option { return func(r *Recovery) { - r.recoverRetryOptions = opts + r.recoveryOptions = opts } } func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, logger *logr.Logger, opts ...Option) *Recovery { recovery := &Recovery{ - fileManager: fileManager, - jsonEncoder: jsonEncoder, - logger: logger, - mariadbdRetryOptions: &defaultMariadbdRetryOpts, - recoverRetryOptions: &defaultRecoverRetryOpts, + fileManager: fileManager, + jsonEncoder: jsonEncoder, + logger: logger, + mariadbdReloadOptions: &defaultMariadbdReloadOpts, + recoveryOptions: &defaultRecoveryOpts, } for _, setOpts := range opts { setOpts(recovery) @@ -86,7 +86,7 @@ func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { } r.logger.Info("reloading mariadbd process") - if err := mariadbd.ReloadWithRetries(r.mariadbdRetryOptions); err != nil { + if err := mariadbd.ReloadWithOptions(r.mariadbdReloadOptions); err != nil { r.logger.Error(err, "error reloading mariadbd process") } else { r.logger.Info("mariadbd process reloaded") @@ -121,12 +121,12 @@ func (r *Recovery) Delete(w http.ResponseWriter, req *http.Request) { } func (r *Recovery) recover() (*galera.Bootstrap, error) { - for i := 0; i < r.recoverRetryOptions.Retries; i++ { - time.Sleep(r.recoverRetryOptions.WaitRetry) + for i := 0; i < r.recoveryOptions.Retries; i++ { + time.Sleep(r.recoveryOptions.WaitRetry) bytes, err := r.fileManager.ReadStateFile(galera.RecoveryLogFileName) if err != nil { - r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoverRetryOptions.Retries) + r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoveryOptions.Retries) continue } @@ -136,7 +136,7 @@ func (r *Recovery) recover() (*galera.Bootstrap, error) { return &recover, nil } - r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoverRetryOptions.Retries) + r.logger.Error(err, "error recovering galera from recovery log", "retry", i, "max-retries", r.recoveryOptions.Retries) } - return nil, fmt.Errorf("maximum retries (%d) reached attempting to recover galera from recovery log", r.recoverRetryOptions.Retries) + return nil, fmt.Errorf("maximum retries (%d) reached attempting to recover galera from recovery log", r.recoveryOptions.Retries) } diff --git a/pkg/mariadbd/mariadbd.go b/pkg/mariadbd/mariadbd.go index d13adb2..a07b5fe 100644 --- a/pkg/mariadbd/mariadbd.go +++ b/pkg/mariadbd/mariadbd.go @@ -33,12 +33,12 @@ func Reload() error { return errProcessNotFound } -type RetryOptions struct { +type ReloadOptions struct { Retries int WaitRetry time.Duration } -func ReloadWithRetries(opts *RetryOptions) error { +func ReloadWithOptions(opts *ReloadOptions) error { for i := 0; i < opts.Retries; i++ { err := Reload() if err == nil { From 55b9b88e6d3444ed622accd1aec5fb2c11ea8a7e Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 20:24:16 +0200 Subject: [PATCH 25/27] Added mutex for preventing race conditions on config/state files --- make/dev.mk | 2 +- pkg/handler/bootstrap/bootstrap.go | 12 +++++++++++- pkg/handler/galerastate/galerastate.go | 10 +++++++++- pkg/handler/handler.go | 26 +++++++++++++++++++++++--- pkg/handler/recovery/recovery.go | 10 +++++++++- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/make/dev.mk b/make/dev.mk index a3dc845..3f8c26d 100644 --- a/make/dev.mk +++ b/make/dev.mk @@ -34,7 +34,7 @@ state: ## Copies a example state file for development purposes. @mkdir -p $(STATE_DIR) cp "examples/$(STATE_FILE)" "$(STATE_DIR)/grastate.dat" -BASE_RUN_FLAGS ?= --log-dev +BASE_RUN_FLAGS ?= --log-level=debug --log-dev RUN_FLAGS ?= $(BASE_RUN_FLAGS) --config-dir=$(CONFIG_DIR) --state-dir=$(STATE_DIR) .PHONY: run diff --git a/pkg/handler/bootstrap/bootstrap.go b/pkg/handler/bootstrap/bootstrap.go index 96328a6..21d280f 100644 --- a/pkg/handler/bootstrap/bootstrap.go +++ b/pkg/handler/bootstrap/bootstrap.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os" + "sync" "time" "github.com/go-logr/logr" @@ -22,6 +23,7 @@ var ( type Bootstrap struct { fileManager *filemanager.FileManager + locker sync.Locker logger *logr.Logger mariadbdReloadOptions *mariadbd.ReloadOptions } @@ -34,9 +36,10 @@ func WithMariadbdReload(opts *mariadbd.ReloadOptions) Option { } } -func NewBootstrap(fileManager *filemanager.FileManager, logger *logr.Logger, opts ...Option) *Bootstrap { +func NewBootstrap(fileManager *filemanager.FileManager, locker sync.Locker, logger *logr.Logger, opts ...Option) *Bootstrap { bootstrap := &Bootstrap{ fileManager: fileManager, + locker: locker, logger: logger, mariadbdReloadOptions: &defaultMariadbdReloadOpts, } @@ -58,6 +61,9 @@ func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) return } + b.locker.Lock() + defer b.locker.Unlock() + b.logger.V(1).Info("enabling bootstrap") if err := b.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil && !os.IsNotExist(err) { b.logger.Error(err, "error deleting existing recovery config") @@ -89,6 +95,10 @@ func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { } func (b *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { + b.locker.Lock() + defer b.locker.Unlock() + b.logger.V(1).Info("disabling bootstrap") + if err := b.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) diff --git a/pkg/handler/galerastate/galerastate.go b/pkg/handler/galerastate/galerastate.go index c83f86b..5fa7b00 100644 --- a/pkg/handler/galerastate/galerastate.go +++ b/pkg/handler/galerastate/galerastate.go @@ -3,6 +3,7 @@ package galerastate import ( "net/http" "os" + "sync" "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" @@ -13,18 +14,25 @@ import ( type GaleraState struct { fileManager *filemanager.FileManager jsonEncoder *jsonencoder.JSONEncoder + locker sync.Locker logger *logr.Logger } -func NewGaleraState(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, logger *logr.Logger) *GaleraState { +func NewGaleraState(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, locker sync.Locker, + logger *logr.Logger) *GaleraState { return &GaleraState{ fileManager: fileManager, jsonEncoder: jsonEncoder, + locker: locker, logger: logger, } } func (g *GaleraState) Get(w http.ResponseWriter, r *http.Request) { + g.locker.Lock() + defer g.locker.Unlock() + g.logger.V(1).Info("getting galera state") + bytes, err := g.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 18a0a7c..8a7a1b1 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -1,6 +1,8 @@ package handler import ( + "sync" + "github.com/go-logr/logr" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/handler/bootstrap" @@ -39,13 +41,31 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger, handl for _, setOpts := range handlerOpts { setOpts(opts) } + + mux := &sync.RWMutex{} bootstrapLogger := logger.WithName("bootstrap") galeraStateLogger := logger.WithName("galerastate") recoveryLogger := logger.WithName("recovery") - bootstrap := bootstrap.NewBootstrap(fileManager, &bootstrapLogger, opts.bootstrap...) - galerastate := galerastate.NewGaleraState(fileManager, jsonencoder.NewJSONEncoder(&galeraStateLogger), &galeraStateLogger) - recovery := recovery.NewRecover(fileManager, jsonencoder.NewJSONEncoder(&recoveryLogger), &recoveryLogger, opts.recovery...) + bootstrap := bootstrap.NewBootstrap( + fileManager, + mux, + &bootstrapLogger, + opts.bootstrap..., + ) + galerastate := galerastate.NewGaleraState( + fileManager, + jsonencoder.NewJSONEncoder(&galeraStateLogger), + mux.RLocker(), + &galeraStateLogger, + ) + recovery := recovery.NewRecover( + fileManager, + jsonencoder.NewJSONEncoder(&recoveryLogger), + mux, + &recoveryLogger, + opts.recovery..., + ) return &Handler{ Bootstrap: bootstrap, diff --git a/pkg/handler/recovery/recovery.go b/pkg/handler/recovery/recovery.go index d32612c..75665c4 100644 --- a/pkg/handler/recovery/recovery.go +++ b/pkg/handler/recovery/recovery.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "sync" "time" "github.com/go-logr/logr" @@ -32,6 +33,7 @@ type RecoveryOptions struct { type Recovery struct { fileManager *filemanager.FileManager jsonEncoder *jsonencoder.JSONEncoder + locker sync.Locker logger *logr.Logger mariadbdReloadOptions *mariadbd.ReloadOptions recoveryOptions *RecoveryOptions @@ -51,11 +53,12 @@ func WithRecovery(opts *RecoveryOptions) Option { } } -func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, logger *logr.Logger, +func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, locker sync.Locker, logger *logr.Logger, opts ...Option) *Recovery { recovery := &Recovery{ fileManager: fileManager, jsonEncoder: jsonEncoder, + locker: locker, logger: logger, mariadbdReloadOptions: &defaultMariadbdReloadOpts, recoveryOptions: &defaultRecoveryOpts, @@ -67,6 +70,10 @@ func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.J } func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { + r.locker.Lock() + defer r.locker.Unlock() + r.logger.V(1).Info("starting recovery") + if err := r.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil && !os.IsNotExist(err) { r.logger.Error(err, "error deleting existing bootstrap config") http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -105,6 +112,7 @@ func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { return } + r.logger.V(1).Info("finished recovery") r.jsonEncoder.Encode(w, bootstrap) } From cc93bfed8007d1396ab6d590f5f3b9cb94d6fcb3 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sat, 3 Jun 2023 22:05:14 +0200 Subject: [PATCH 26/27] Improved error handling notably by introducing response writer --- pkg/errors/errors.go | 23 +++++++++++++++ pkg/handler/bootstrap/bootstrap.go | 35 +++++++++++----------- pkg/handler/galerastate/galerastate.go | 31 ++++++++++---------- pkg/handler/handler.go | 7 +++-- pkg/handler/jsonencoder/jsonencoder.go | 28 ------------------ pkg/handler/recovery/recovery.go | 32 +++++++++------------ pkg/mariadbd/mariadbd.go | 2 -- pkg/responsewriter/responsewriter.go | 40 ++++++++++++++++++++++++++ 8 files changed, 113 insertions(+), 85 deletions(-) create mode 100644 pkg/errors/errors.go delete mode 100644 pkg/handler/jsonencoder/jsonencoder.go create mode 100644 pkg/responsewriter/responsewriter.go diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..0f036a9 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,23 @@ +package errors + +import "fmt" + +type APIError struct { + Message string `json:"message"` +} + +func (e *APIError) Error() string { + return e.Message +} + +func NewAPIError(message string) *APIError { + return &APIError{ + Message: message, + } +} + +func NewAPIErrorf(format string, a ...any) *APIError { + return &APIError{ + Message: fmt.Sprintf(format, a...), + } +} diff --git a/pkg/handler/bootstrap/bootstrap.go b/pkg/handler/bootstrap/bootstrap.go index 21d280f..9f6005d 100644 --- a/pkg/handler/bootstrap/bootstrap.go +++ b/pkg/handler/bootstrap/bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "encoding/json" + "errors" "fmt" "net/http" "os" @@ -9,9 +10,11 @@ import ( "time" "github.com/go-logr/logr" + agenterrors "github.com/mariadb-operator/agent/pkg/errors" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/galera" "github.com/mariadb-operator/agent/pkg/mariadbd" + "github.com/mariadb-operator/agent/pkg/responsewriter" ) var ( @@ -23,6 +26,7 @@ var ( type Bootstrap struct { fileManager *filemanager.FileManager + responseWriter *responsewriter.ResponseWriter locker sync.Locker logger *logr.Logger mariadbdReloadOptions *mariadbd.ReloadOptions @@ -36,9 +40,11 @@ func WithMariadbdReload(opts *mariadbd.ReloadOptions) Option { } } -func NewBootstrap(fileManager *filemanager.FileManager, locker sync.Locker, logger *logr.Logger, opts ...Option) *Bootstrap { +func NewBootstrap(fileManager *filemanager.FileManager, responseWriter *responsewriter.ResponseWriter, locker sync.Locker, + logger *logr.Logger, opts ...Option) *Bootstrap { bootstrap := &Bootstrap{ fileManager: fileManager, + responseWriter: responseWriter, locker: locker, logger: logger, mariadbdReloadOptions: &defaultMariadbdReloadOpts, @@ -52,13 +58,11 @@ func NewBootstrap(fileManager *filemanager.FileManager, locker sync.Locker, logg func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { var bootstrap galera.Bootstrap if err := json.NewDecoder(r.Body).Decode(&bootstrap); err != nil { - b.logger.Error(err, "error decoding bootstrap") - http.Error(w, "invalid body: a valid bootstrap object must be provided", http.StatusBadRequest) + b.responseWriter.Write(w, agenterrors.NewAPIErrorf("error decoding bootstrap: %v", err), http.StatusBadRequest) return } if err := bootstrap.Validate(); err != nil { - b.logger.Error(err, "invalid bootstrap") - http.Error(w, fmt.Sprintf("invalid bootstrap: %v", err), http.StatusBadRequest) + b.responseWriter.Write(w, agenterrors.NewAPIErrorf("invalid bootstrap: %v", err), http.StatusBadRequest) return } b.locker.Lock() @@ -66,27 +70,23 @@ func (b *Bootstrap) Put(w http.ResponseWriter, r *http.Request) { b.logger.V(1).Info("enabling bootstrap") if err := b.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil && !os.IsNotExist(err) { - b.logger.Error(err, "error deleting existing recovery config") - http.Error(w, "Internal server error", http.StatusInternalServerError) + b.responseWriter.WriteErrorf(w, "error deleting existing recovery config: %v", err) return } if err := b.setSafeToBootstrap(&bootstrap); err != nil { - b.logger.Error(err, "error setting safe to bootstrap") - http.Error(w, "Internal server error", http.StatusInternalServerError) + b.responseWriter.WriteErrorf(w, "error setting safe to bootstrap: %v", err) return } if err := b.fileManager.WriteConfigFile(galera.BootstrapFileName, []byte(galera.BootstrapFile)); err != nil { - b.logger.Error(err, "error writing bootstrap file") - http.Error(w, "Internal server error", http.StatusInternalServerError) + b.responseWriter.WriteErrorf(w, "error writing bootstrap config: %v", err) return } b.logger.Info("reloading mariadbd process") if err := mariadbd.ReloadWithOptions(b.mariadbdReloadOptions); err != nil { - b.logger.Error(err, "error reloading mariadbd process") - http.Error(w, "Internal server error", http.StatusInternalServerError) + b.responseWriter.WriteErrorf(w, "error reloading mariadbd process: %v", err) return } b.logger.Info("mariadbd process reloaded") @@ -101,11 +101,10 @@ func (b *Bootstrap) Delete(w http.ResponseWriter, r *http.Request) { if err := b.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil { if os.IsNotExist(err) { - http.Error(w, "Not found", http.StatusNotFound) + b.responseWriter.Write(w, agenterrors.NewAPIError("bootstrap config not found"), http.StatusNotFound) return } - b.logger.Error(err, "error deleting bootstrap file") - http.Error(w, "Internal server error", http.StatusInternalServerError) + b.responseWriter.WriteErrorf(w, "error deleting bootstrap config: %v", err) return } w.WriteHeader(http.StatusOK) @@ -115,7 +114,7 @@ func (b *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { bytes, err := b.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { - return nil + return errors.New("galera state does not exist") } return fmt.Errorf("error reading galera state: %v", err) } @@ -130,7 +129,7 @@ func (b *Bootstrap) setSafeToBootstrap(bootstrap *galera.Bootstrap) error { galeraState.SafeToBootstrap = true bytes, err = galeraState.Marshal() if err != nil { - return fmt.Errorf("error marshallng galera state: %v", err) + return fmt.Errorf("error marshaling galera state: %v", err) } if err := b.fileManager.WriteStateFile(galera.GaleraStateFileName, bytes); err != nil { diff --git a/pkg/handler/galerastate/galerastate.go b/pkg/handler/galerastate/galerastate.go index 5fa7b00..fb50c37 100644 --- a/pkg/handler/galerastate/galerastate.go +++ b/pkg/handler/galerastate/galerastate.go @@ -6,25 +6,26 @@ import ( "sync" "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/errors" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/galera" - "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" + "github.com/mariadb-operator/agent/pkg/responsewriter" ) type GaleraState struct { - fileManager *filemanager.FileManager - jsonEncoder *jsonencoder.JSONEncoder - locker sync.Locker - logger *logr.Logger + fileManager *filemanager.FileManager + responseWriter *responsewriter.ResponseWriter + locker sync.Locker + logger *logr.Logger } -func NewGaleraState(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, locker sync.Locker, +func NewGaleraState(fileManager *filemanager.FileManager, responseWriter *responsewriter.ResponseWriter, locker sync.Locker, logger *logr.Logger) *GaleraState { return &GaleraState{ - fileManager: fileManager, - jsonEncoder: jsonEncoder, - locker: locker, - logger: logger, + fileManager: fileManager, + responseWriter: responseWriter, + locker: locker, + logger: logger, } } @@ -36,19 +37,17 @@ func (g *GaleraState) Get(w http.ResponseWriter, r *http.Request) { bytes, err := g.fileManager.ReadStateFile(galera.GaleraStateFileName) if err != nil { if os.IsNotExist(err) { - http.Error(w, "Not found", http.StatusNotFound) + g.responseWriter.Write(w, errors.NewAPIError("galera state not found"), http.StatusNotFound) return } - g.logger.Error(err, "error reading file") - http.Error(w, "Internal server error", http.StatusInternalServerError) + g.responseWriter.WriteErrorf(w, "error reading galera state: %v", err) return } var galeraState galera.GaleraState if err := galeraState.Unmarshal(bytes); err != nil { - g.logger.Error(err, "error unmarshalling galera state") - http.Error(w, "Internal server error", http.StatusInternalServerError) + g.responseWriter.WriteErrorf(w, "error unmarshaling galera state: %v", err) return } - g.jsonEncoder.Encode(w, galeraState) + g.responseWriter.WriteOK(w, galeraState) } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 8a7a1b1..80ea881 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -7,8 +7,8 @@ import ( "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/handler/bootstrap" "github.com/mariadb-operator/agent/pkg/handler/galerastate" - "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" "github.com/mariadb-operator/agent/pkg/handler/recovery" + "github.com/mariadb-operator/agent/pkg/responsewriter" ) type Handler struct { @@ -49,19 +49,20 @@ func NewHandler(fileManager *filemanager.FileManager, logger *logr.Logger, handl bootstrap := bootstrap.NewBootstrap( fileManager, + responsewriter.NewResponseWriter(&bootstrapLogger), mux, &bootstrapLogger, opts.bootstrap..., ) galerastate := galerastate.NewGaleraState( fileManager, - jsonencoder.NewJSONEncoder(&galeraStateLogger), + responsewriter.NewResponseWriter(&galeraStateLogger), mux.RLocker(), &galeraStateLogger, ) recovery := recovery.NewRecover( fileManager, - jsonencoder.NewJSONEncoder(&recoveryLogger), + responsewriter.NewResponseWriter(&recoveryLogger), mux, &recoveryLogger, opts.recovery..., diff --git a/pkg/handler/jsonencoder/jsonencoder.go b/pkg/handler/jsonencoder/jsonencoder.go deleted file mode 100644 index 1890cd5..0000000 --- a/pkg/handler/jsonencoder/jsonencoder.go +++ /dev/null @@ -1,28 +0,0 @@ -package jsonencoder - -import ( - "encoding/json" - "net/http" - - "github.com/go-logr/logr" -) - -type JSONEncoder struct { - logger *logr.Logger -} - -func NewJSONEncoder(logger *logr.Logger) *JSONEncoder { - return &JSONEncoder{ - logger: logger, - } -} - -func (j *JSONEncoder) Encode(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(v); err != nil { - j.logger.Error(err, "error encoding json") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) -} diff --git a/pkg/handler/recovery/recovery.go b/pkg/handler/recovery/recovery.go index 75665c4..dec9a2a 100644 --- a/pkg/handler/recovery/recovery.go +++ b/pkg/handler/recovery/recovery.go @@ -8,10 +8,11 @@ import ( "time" "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/errors" "github.com/mariadb-operator/agent/pkg/filemanager" "github.com/mariadb-operator/agent/pkg/galera" - "github.com/mariadb-operator/agent/pkg/handler/jsonencoder" "github.com/mariadb-operator/agent/pkg/mariadbd" + "github.com/mariadb-operator/agent/pkg/responsewriter" ) var ( @@ -32,7 +33,7 @@ type RecoveryOptions struct { type Recovery struct { fileManager *filemanager.FileManager - jsonEncoder *jsonencoder.JSONEncoder + responseWriter *responsewriter.ResponseWriter locker sync.Locker logger *logr.Logger mariadbdReloadOptions *mariadbd.ReloadOptions @@ -53,11 +54,11 @@ func WithRecovery(opts *RecoveryOptions) Option { } } -func NewRecover(fileManager *filemanager.FileManager, jsonEncoder *jsonencoder.JSONEncoder, locker sync.Locker, logger *logr.Logger, - opts ...Option) *Recovery { +func NewRecover(fileManager *filemanager.FileManager, responseWriter *responsewriter.ResponseWriter, locker sync.Locker, + logger *logr.Logger, opts ...Option) *Recovery { recovery := &Recovery{ fileManager: fileManager, - jsonEncoder: jsonEncoder, + responseWriter: responseWriter, locker: locker, logger: logger, mariadbdReloadOptions: &defaultMariadbdReloadOpts, @@ -75,20 +76,17 @@ func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { r.logger.V(1).Info("starting recovery") if err := r.fileManager.DeleteConfigFile(galera.BootstrapFileName); err != nil && !os.IsNotExist(err) { - r.logger.Error(err, "error deleting existing bootstrap config") - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error deleting existing bootstrap config: %v", err) return } if err := r.fileManager.DeleteStateFile(galera.RecoveryLogFileName); err != nil && !os.IsNotExist(err) { - r.logger.Error(err, "error deleting existing recovery log") - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error deleting existing recovery log: %v", err) return } if err := r.fileManager.WriteConfigFile(galera.RecoveryFileName, []byte(galera.RecoveryFile)); err != nil { - r.logger.Error(err, "error writing recovery config") - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error writing recovery config: %v", err) return } @@ -101,28 +99,26 @@ func (r *Recovery) Put(w http.ResponseWriter, req *http.Request) { bootstrap, err := r.recover() if err != nil { - r.logger.Error(err, "error recovering galera") - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error recovering galera: %v", err) return } if err := r.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { - r.logger.Error(err, "error deleting recovery file") - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error deleting recovery config: %v", err) return } r.logger.V(1).Info("finished recovery") - r.jsonEncoder.Encode(w, bootstrap) + r.responseWriter.WriteOK(w, bootstrap) } func (r *Recovery) Delete(w http.ResponseWriter, req *http.Request) { if err := r.fileManager.DeleteConfigFile(galera.RecoveryFileName); err != nil { if os.IsNotExist(err) { - http.Error(w, "Not found", http.StatusNotFound) + r.responseWriter.Write(w, errors.NewAPIError("recovery config not found"), http.StatusNotFound) return } - http.Error(w, "Internal server error", http.StatusInternalServerError) + r.responseWriter.WriteErrorf(w, "error deleting recovery config: %v", err) return } w.WriteHeader(http.StatusOK) diff --git a/pkg/mariadbd/mariadbd.go b/pkg/mariadbd/mariadbd.go index a07b5fe..f6578c5 100644 --- a/pkg/mariadbd/mariadbd.go +++ b/pkg/mariadbd/mariadbd.go @@ -20,7 +20,6 @@ func Reload() error { if err != nil { return fmt.Errorf("error getting processes: %v", err) } - for _, p := range processes { if p.Executable() == mariadbdProcessName { if err := syscall.Kill(p.Pid(), reloadSysCall); err != nil { @@ -29,7 +28,6 @@ func Reload() error { return nil } } - return errProcessNotFound } diff --git a/pkg/responsewriter/responsewriter.go b/pkg/responsewriter/responsewriter.go new file mode 100644 index 0000000..3b7db9f --- /dev/null +++ b/pkg/responsewriter/responsewriter.go @@ -0,0 +1,40 @@ +package responsewriter + +import ( + "encoding/json" + "net/http" + + "github.com/go-logr/logr" + "github.com/mariadb-operator/agent/pkg/errors" +) + +type ResponseWriter struct { + logger *logr.Logger +} + +func NewResponseWriter(logger *logr.Logger) *ResponseWriter { + return &ResponseWriter{ + logger: logger, + } +} + +func (r *ResponseWriter) Write(w http.ResponseWriter, v any, statusCode int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := json.NewEncoder(w).Encode(v); err != nil { + r.logger.Error(err, "error encoding json") + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} + +func (r *ResponseWriter) WriteOK(w http.ResponseWriter, v any) { + r.Write(w, v, http.StatusOK) +} + +func (r *ResponseWriter) WriteError(w http.ResponseWriter, msg string) { + r.Write(w, errors.NewAPIError(msg), http.StatusInternalServerError) +} + +func (r *ResponseWriter) WriteErrorf(w http.ResponseWriter, format string, a ...any) { + r.Write(w, errors.NewAPIErrorf(format, a...), http.StatusInternalServerError) +} From 021a5a646fc6f87a14138947dc91e010c4c461f6 Mon Sep 17 00:00:00 2001 From: Martin Montes Date: Sun, 4 Jun 2023 10:53:40 +0200 Subject: [PATCH 27/27] Added galera tests --- pkg/galera/galera.go | 3 + pkg/galera/galera_test.go | 440 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 pkg/galera/galera_test.go diff --git a/pkg/galera/galera.go b/pkg/galera/galera.go index 4b44124..e6adcad 100644 --- a/pkg/galera/galera.go +++ b/pkg/galera/galera.go @@ -31,6 +31,9 @@ type GaleraState struct { } func (g *GaleraState) Marshal() ([]byte, error) { + if _, err := guuid.Parse(g.UUID); err != nil { + return nil, fmt.Errorf("invalid uuid: %v", err) + } type tplOpts struct { Version string UUID string diff --git a/pkg/galera/galera_test.go b/pkg/galera/galera_test.go new file mode 100644 index 0000000..14ac335 --- /dev/null +++ b/pkg/galera/galera_test.go @@ -0,0 +1,440 @@ +package galera + +import ( + "reflect" + "testing" +) + +func TestGaleraStateMarshal(t *testing.T) { + tests := []struct { + name string + galeraState *GaleraState + want string + wantErr bool + }{ + { + name: "invalid uuid", + galeraState: &GaleraState{ + Version: "2.1", + UUID: "foo", + Seqno: 1, + SafeToBootstrap: false, + }, + want: "", + wantErr: true, + }, + { + name: "safe_to_bootstrap false", + galeraState: &GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: false, + }, + want: `version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 0`, + wantErr: false, + }, + { + name: "safe_to_bootstrap true", + galeraState: &GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: true, + }, + want: `version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 1`, + wantErr: false, + }, + { + name: "negative seqno", + galeraState: &GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: -1, + SafeToBootstrap: false, + }, + want: `version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: -1 +safe_to_bootstrap: 0`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bytes, err := tt.galeraState.Marshal() + if tt.wantErr && err == nil { + t.Fatal("error expected, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("error unexpected, got %v", err) + } + if tt.want != string(bytes) { + t.Fatalf("unexpected result:\nexpected:\n%s\ngot:\n%s\n", tt.want, string(bytes)) + } + }) + } +} + +func TestGaleraStateUnmarshal(t *testing.T) { + tests := []struct { + name string + bytes []byte + want GaleraState + wantErr bool + }{ + { + name: "empty", + bytes: []byte(` +`), + want: GaleraState{}, + wantErr: true, + }, + { + name: "comment", + bytes: []byte(`# GALERA saved state +version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 1`), + want: GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: true, + }, + wantErr: false, + }, + { + name: "indentation", + bytes: []byte(`# GALERA saved state +version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 1`), + want: GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: true, + }, + wantErr: false, + }, + { + name: "invalid uuid", + bytes: []byte(`# GALERA saved state +version: 2.1 +uuid: foo +seqno: -1 +safe_to_bootstrap: 1`), + want: GaleraState{}, + wantErr: true, + }, + { + name: "invalid seqno", + bytes: []byte(`# GALERA saved state +version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: foo +safe_to_bootstrap: 1`), + want: GaleraState{}, + wantErr: true, + }, + { + name: "invalid safe_to_bootstrap", + bytes: []byte(`# GALERA saved state +version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: true`), + want: GaleraState{}, + wantErr: true, + }, + { + name: "safe_to_bootstrap true", + bytes: []byte(`version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 1`), + want: GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: true, + }, + wantErr: false, + }, + { + name: "safe_to_bootstrap false", + bytes: []byte(`version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: 1 +safe_to_bootstrap: 0`), + want: GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + SafeToBootstrap: false, + }, + wantErr: false, + }, + { + name: "negative seqno", + bytes: []byte(`version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +seqno: -1 +safe_to_bootstrap: 0`), + want: GaleraState{ + Version: "2.1", + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: -1, + SafeToBootstrap: false, + }, + wantErr: false, + }, + { + name: "missing safe_to_bootstrap", + bytes: []byte(`version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +safe_to_bootstrap: 0`), + want: GaleraState{}, + wantErr: true, + }, + { + name: "missing seqno", + bytes: []byte(`version: 2.1 +uuid: 05f061bd-02a3-11ee-857c-aa370ff6666b +safe_to_bootstrap: 0`), + want: GaleraState{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var galeraState GaleraState + err := galeraState.Unmarshal(tt.bytes) + if tt.wantErr && err == nil { + t.Fatal("error expected, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("error unexpected, got %v", err) + } + if !reflect.DeepEqual(tt.want, galeraState) { + t.Fatalf("unexpected result:\nexpected:\n%v\ngot:\n%v\n", tt.want, galeraState) + } + }) + } +} + +func TestBootstrapValidate(t *testing.T) { + tests := []struct { + name string + bootstrap Bootstrap + wantErr bool + }{ + { + name: "invalid uuid", + bootstrap: Bootstrap{ + UUID: "foo", + Seqno: 1, + }, + wantErr: true, + }, + { + name: "seqno", + bootstrap: Bootstrap{ + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: 1, + }, + wantErr: false, + }, + { + name: "negative seqno", + bootstrap: Bootstrap{ + UUID: "05f061bd-02a3-11ee-857c-aa370ff6666b", + Seqno: -1, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.bootstrap.Validate() + if tt.wantErr && err == nil { + t.Fatal("error expected, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("error unexpected, got %v", err) + } + }) + } +} + +func TestBootstrapUnmarshal(t *testing.T) { + tests := []struct { + name string + bytes []byte + want Bootstrap + wantErr bool + }{ + { + name: "empty", + bytes: []byte(` +`), + want: Bootstrap{}, + wantErr: true, + }, + { + name: "missig position", + //nolint + bytes: []byte(`2023-06-04 8:24:23 0 [Note] Starting MariaDB 10.11.3-MariaDB-1:10.11.3+maria~ubu2204 source revision 0bb31039f54bd6a0dc8f0fc7d40e6b58a51998b0 as process 86033 +2023-06-04 8:24:23 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 +2023-06-04 8:24:23 0 [Note] InnoDB: Number of transaction pools: 1 +2023-06-04 8:24:23 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions +2023-06-04 8:24:23 0 [Note] mariadbd: O_TMPFILE is not supported on /tmp (disabling future attempts) +2023-06-04 8:24:23 0 [Note] InnoDB: Using liburing +2023-06-04 8:24:23 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB +2023-06-04 8:24:23 0 [Note] InnoDB: Completed initialization of buffer pool +2023-06-04 8:24:23 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes) +2023-06-04 8:24:23 0 [Note] InnoDB: 128 rollback segments are active. +`), + want: Bootstrap{}, + wantErr: true, + }, + { + name: "invalid uuid", + bytes: []byte(`2023-06-04 8:24:23 0 [Note] WSREP: Recovered position: foo:1`), + want: Bootstrap{}, + wantErr: true, + }, + { + name: "invalid seqno", + bytes: []byte(`2023-06-04 8:24:23 0 [Note] WSREP: Recovered position: 15d9a0ef-02b1-11ee-9499-decd8e34642e:bar`), + want: Bootstrap{}, + wantErr: true, + }, + { + name: "single position", + //nolint + bytes: []byte(`2023-06-04 8:24:23 0 [Note] Starting MariaDB 10.11.3-MariaDB-1:10.11.3+maria~ubu2204 source revision 0bb31039f54bd6a0dc8f0fc7d40e6b58a51998b0 as process 86033 +2023-06-04 8:24:23 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 +2023-06-04 8:24:23 0 [Note] InnoDB: Number of transaction pools: 1 +2023-06-04 8:24:23 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions +2023-06-04 8:24:23 0 [Note] mariadbd: O_TMPFILE is not supported on /tmp (disabling future attempts) +2023-06-04 8:24:23 0 [Note] InnoDB: Using liburing +2023-06-04 8:24:23 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB +2023-06-04 8:24:23 0 [Note] InnoDB: Completed initialization of buffer pool +2023-06-04 8:24:23 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes) +2023-06-04 8:24:23 0 [Note] InnoDB: 128 rollback segments are active. +2023-06-04 8:24:23 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ... +2023-06-04 8:24:23 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB. +2023-06-04 8:24:23 0 [Note] InnoDB: log sequence number 54530; transaction id 30 +2023-06-04 8:24:23 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery. +2023-06-04 8:24:23 0 [Note] Plugin 'FEEDBACK' is disabled. +2023-06-04 8:24:23 0 [Note] Server socket created on IP: '0.0.0.0'. +2023-06-04 8:24:23 0 [Note] WSREP: Recovered position: 15d9a0ef-02b1-11ee-9499-decd8e34642e:1 +Warning: Memory not freed: 280 +`), + want: Bootstrap{ + UUID: "15d9a0ef-02b1-11ee-9499-decd8e34642e", + Seqno: 1, + }, + wantErr: false, + }, + { + name: "multiple positions", + //nolint + bytes: []byte(`2023-06-04 8:24:16 0 [Note] Starting MariaDB 10.11.3-MariaDB-1:10.11.3+maria~ubu2204 source revision 0bb31039f54bd6a0dc8f0fc7d40e6b58a51998b0 as process 84826 +2023-06-04 8:24:16 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 +2023-06-04 8:24:16 0 [Note] InnoDB: Number of transaction pools: 1 +2023-06-04 8:24:16 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions +2023-06-04 8:24:16 0 [Note] mariadbd: O_TMPFILE is not supported on /tmp (disabling future attempts) +2023-06-04 8:24:16 0 [Note] InnoDB: Using liburing +2023-06-04 8:24:16 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB +2023-06-04 8:24:16 0 [Note] InnoDB: Completed initialization of buffer pool +2023-06-04 8:24:16 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes) +2023-06-04 8:24:16 0 [Note] InnoDB: Starting crash recovery from checkpoint LSN=46590 +2023-06-04 8:24:16 0 [Note] InnoDB: Starting final batch to recover 15 pages from redo log. +2023-06-04 8:24:16 0 [Note] InnoDB: 128 rollback segments are active. +2023-06-04 8:24:16 0 [Note] InnoDB: Removed temporary tablespace data file: "./ibtmp1" +2023-06-04 8:24:16 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ... +2023-06-04 8:24:16 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB. +2023-06-04 8:24:16 0 [Note] InnoDB: log sequence number 54202; transaction id 30 +2023-06-04 8:24:16 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery. +2023-06-04 8:24:16 0 [Note] Plugin 'FEEDBACK' is disabled. +2023-06-04 8:24:16 0 [Note] Recovering after a crash using tc.log +2023-06-04 8:24:16 0 [Note] Starting table crash recovery... +2023-06-04 8:24:16 0 [Note] Crash table recovery finished. +2023-06-04 8:24:16 0 [Note] Server socket created on IP: '0.0.0.0'. +2023-06-04 8:24:16 0 [Note] WSREP: Recovered position: 15d9a0ef-02b1-11ee-9499-decd8e34642e:1 +Warning: Memory not freed: 280 +2023-06-04 8:24:17 0 [Note] Starting MariaDB 10.11.3-MariaDB-1:10.11.3+maria~ubu2204 source revision 0bb31039f54bd6a0dc8f0fc7d40e6b58a51998b0 as process 85132 +2023-06-04 8:24:17 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 +2023-06-04 8:24:17 0 [Note] InnoDB: Number of transaction pools: 1 +2023-06-04 8:24:17 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions +2023-06-04 8:24:17 0 [Note] mariadbd: O_TMPFILE is not supported on /tmp (disabling future attempts) +2023-06-04 8:24:17 0 [Note] InnoDB: Using liburing +2023-06-04 8:24:17 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB +2023-06-04 8:24:17 0 [Note] InnoDB: Completed initialization of buffer pool +2023-06-04 8:24:17 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes) +2023-06-04 8:24:17 0 [Note] InnoDB: 128 rollback segments are active. +2023-06-04 8:24:17 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ... +2023-06-04 8:24:17 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB. +2023-06-04 8:24:17 0 [Note] InnoDB: log sequence number 54530; transaction id 30 +2023-06-04 8:24:17 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery. +2023-06-04 8:24:17 0 [Note] Plugin 'FEEDBACK' is disabled. +2023-06-04 8:24:17 0 [Note] Server socket created on IP: '0.0.0.0'. +2023-06-04 8:24:17 0 [Note] WSREP: Recovered position: 0794bb7b-0614-41cd-8301-4fe1d55a1f60:2 +Warning: Memory not freed: 280 +2023-06-04 8:24:18 0 [Note] Starting MariaDB 10.11.3-MariaDB-1:10.11.3+maria~ubu2204 source revision 0bb31039f54bd6a0dc8f0fc7d40e6b58a51998b0 as process 85425 +2023-06-04 8:24:18 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 +2023-06-04 8:24:18 0 [Note] InnoDB: Number of transaction pools: 1 +2023-06-04 8:24:18 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions +2023-06-04 8:24:18 0 [Note] mariadbd: O_TMPFILE is not supported on /tmp (disabling future attempts) +2023-06-04 8:24:18 0 [Note] InnoDB: Using liburing +2023-06-04 8:24:18 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB +2023-06-04 8:24:18 0 [Note] InnoDB: Completed initialization of buffer pool +2023-06-04 8:24:18 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes) +2023-06-04 8:24:18 0 [Note] InnoDB: 128 rollback segments are active. +2023-06-04 8:24:18 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ... +2023-06-04 8:24:18 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB. +2023-06-04 8:24:18 0 [Note] InnoDB: log sequence number 54530; transaction id 30 +2023-06-04 8:24:18 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery. +2023-06-04 8:24:18 0 [Note] Plugin 'FEEDBACK' is disabled. +2023-06-04 8:24:18 0 [Note] Server socket created on IP: '0.0.0.0'. +2023-06-04 8:24:18 0 [Note] WSREP: Recovered position: 08dd3b99-ac6b-46f8-84bd-8cb8f9f949b0:3 +Warning: Memory not freed: 280 +`), + want: Bootstrap{ + UUID: "08dd3b99-ac6b-46f8-84bd-8cb8f9f949b0", + Seqno: 3, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var bootstrap Bootstrap + err := bootstrap.Unmarshal(tt.bytes) + if tt.wantErr && err == nil { + t.Fatal("error expected, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("error unexpected, got %v", err) + } + if !reflect.DeepEqual(tt.want, bootstrap) { + t.Fatalf("unexpected result:\nexpected:\n%v\ngot:\n%v\n", tt.want, bootstrap) + } + }) + } +}