diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1f2d0bee..c9c65000 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,10 +40,10 @@ jobs: run: | # Extract TiDB versions from test matrix and compare with env.TIDB_VERSIONS EXPECTED_VERSIONS="${{ env.TIDB_VERSIONS }}" - MATRIX_VERSIONS=$(grep -E "testtidb[0-9]" .github/workflows/main.yml | sed 's/.*testtidb\([0-9.]*\).*/\1/' | tr '\n' ' ' | xargs) + MATRIX_VERSIONS=$(grep -A 20 "db_type: tidb" .github/workflows/main.yml | grep "db_version:" | sed 's/.*db_version: "\([0-9.]*\)".*/\1/' | tr '\n' ' ' | xargs) echo "Expected versions (from env): $EXPECTED_VERSIONS" - echo "Matrix versions (from test targets): $MATRIX_VERSIONS" + echo "Matrix versions (from workflow): $MATRIX_VERSIONS" # Check if versions match (simple check - both should contain same versions) MISSING="" @@ -55,7 +55,7 @@ jobs: if [ -n "$MISSING" ]; then echo "ERROR: TiDB versions in env.TIDB_VERSIONS not found in test matrix: $MISSING" - echo "Please ensure test matrix includes testtidb* entries for all versions in env.TIDB_VERSIONS" + echo "Please ensure test matrix includes tidb entries for all versions in env.TIDB_VERSIONS" exit 1 fi @@ -75,22 +75,8 @@ jobs: - name: Vendor Go dependencies run: go mod vendor - # Note: TiDB tests now use Docker images with Buildx caching instead of TiUP - # Docker images are cached per version in individual test jobs using Buildx with GHA cache backend - # Each test job pulls and caches its own images - no pre-pull needed since Docker layer cache is ephemeral per job - - - name: Cache apt packages - uses: actions/cache@v4 - with: - path: /var/cache/apt/archives - key: ${{ runner.os }}-apt-${{ hashFiles('**/.github/workflows/main.yml') }} - restore-keys: | - ${{ runner.os }}-apt- - - - name: Install mysql client (populates cache for test jobs) - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends mysql-client + # Note: Tests now use testcontainers - no mysql-client or Docker Buildx caching needed + # Testcontainers handles container lifecycle and image pulling automatically - name: Upload Terraform binary uses: actions/upload-artifact@v4 @@ -113,24 +99,47 @@ jobs: strategy: fail-fast: false matrix: - target: - - testversion5.6 - - testversion5.7 - - testversion8.0 - - testpercona5.7 - - testpercona8.0 - - testmariadb10.3 - - testmariadb10.8 - - testmariadb10.10 - # Track https://github.com/pingcap/tidb/tags - # TiDB versions must match env.TIDB_VERSIONS: 6.1.7 6.5.12 7.1.6 7.5.7 8.1.2 8.5.3 - # Latest version of each minor series - - testtidb6.1.7 - - testtidb6.5.12 - - testtidb7.1.6 - - testtidb7.5.7 - - testtidb8.1.2 - - testtidb8.5.3 + include: + # MySQL versions + - db_type: mysql + db_version: "5.6" + docker_image: "mysql:5.6" + - db_type: mysql + db_version: "5.7" + docker_image: "mysql:5.7" + - db_type: mysql + db_version: "8.0" + docker_image: "mysql:8.0" + # Percona versions + - db_type: percona + db_version: "5.7" + docker_image: "percona:5.7" + - db_type: percona + db_version: "8.0" + docker_image: "percona:8.0" + # MariaDB versions + - db_type: mariadb + db_version: "10.3" + docker_image: "mariadb:10.3" + - db_type: mariadb + db_version: "10.8" + docker_image: "mariadb:10.8" + - db_type: mariadb + db_version: "10.10" + docker_image: "mariadb:10.10" + # TiDB versions - must match env.TIDB_VERSIONS: 6.1.7 6.5.12 7.1.6 7.5.7 8.1.2 8.5.3 + - db_type: tidb + db_version: "6.1.7" + - db_type: tidb + db_version: "6.5.12" + - db_type: tidb + db_version: "7.1.6" + - db_type: tidb + db_version: "7.5.7" + - db_type: tidb + db_version: "8.1.2" + - db_type: tidb + db_version: "8.5.3" steps: - name: Checkout Git repo uses: actions/checkout@v4 @@ -157,136 +166,33 @@ jobs: - name: Make Terraform executable run: chmod +x bin/terraform - - name: Cache apt packages - uses: actions/cache@v4 - with: - path: /var/cache/apt/archives - key: ${{ runner.os }}-apt-${{ hashFiles('**/.github/workflows/main.yml') }} - restore-keys: | - ${{ runner.os }}-apt- - - - name: Install mysql client - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends mysql-client - - name: Set up Docker Buildx - if: contains(matrix.target, 'testversion') || contains(matrix.target, 'testpercona') || contains(matrix.target, 'testmariadb') uses: docker/setup-buildx-action@v3 - - name: Determine Docker image for this test - if: contains(matrix.target, 'testversion') || contains(matrix.target, 'testpercona') || contains(matrix.target, 'testmariadb') - id: docker-image - run: | - if [[ "${{ matrix.target }}" == testversion5.6 ]]; then - echo "image=mysql:5.6" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testversion5.7 ]]; then - echo "image=mysql:5.7" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testversion8.0 ]]; then - echo "image=mysql:8.0" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testpercona5.7 ]]; then - echo "image=percona:5.7" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testpercona8.0 ]]; then - echo "image=percona:8.0" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testmariadb10.3 ]]; then - echo "image=mariadb:10.3" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testmariadb10.8 ]]; then - echo "image=mariadb:10.8" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.target }}" == testmariadb10.10 ]]; then - echo "image=mariadb:10.10" >> $GITHUB_OUTPUT - fi - - - name: Pull and cache Docker image using Buildx - if: contains(matrix.target, 'testversion') || contains(matrix.target, 'testpercona') || contains(matrix.target, 'testmariadb') - uses: docker/build-push-action@v5 - with: - context: . - file: Dockerfile.mysql - push: false - tags: ${{ steps.docker-image.outputs.image }} - build-args: | - MYSQL_IMAGE=${{ steps.docker-image.outputs.image }} - cache-from: type=gha,scope=${{ steps.docker-image.outputs.image }} - cache-to: type=gha,mode=max,scope=${{ steps.docker-image.outputs.image }} - - - name: Extract TiDB version from test target - id: extract-tidb-version - if: contains(matrix.target, 'tidb') + - name: Pre-pull Docker images for caching + if: matrix.db_type != 'tidb' run: | - # Extract version from testtidb6.1.7 -> 6.1.7 - VERSION=$(echo "${{ matrix.target }}" | sed 's/testtidb//') - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "TiDB version for this test: ${VERSION}" + docker pull ${{ matrix.docker_image }} || true - - name: Set up Docker Buildx for TiDB - if: contains(matrix.target, 'tidb') - uses: docker/setup-buildx-action@v3 - - - name: Pull TiDB Docker images in parallel - if: contains(matrix.target, 'tidb') + - name: Pre-pull TiDB images for caching + if: matrix.db_type == 'tidb' run: | - VERSION="${{ steps.extract-tidb-version.outputs.version }}" - echo "Pulling TiDB component images for v${VERSION} in parallel..." - - # Pull all three images in parallel - docker pull pingcap/tidb:v${VERSION} & - docker pull pingcap/pd:v${VERSION} & - docker pull pingcap/tikv:v${VERSION} & - - # Wait for all pulls to complete - wait - - echo "All TiDB component images pulled successfully" - - - name: Cache TiDB Docker images using Buildx - if: contains(matrix.target, 'tidb') - uses: docker/build-push-action@v5 - continue-on-error: false - with: - context: . - file: Dockerfile.tidb - push: false - tags: pingcap/tidb:v${{ steps.extract-tidb-version.outputs.version }} - build-args: | - TIDB_COMPONENT=pingcap/tidb - TIDB_VERSION=v${{ steps.extract-tidb-version.outputs.version }} - cache-from: type=gha,scope=tidb-v${{ steps.extract-tidb-version.outputs.version }} - cache-to: type=gha,mode=max,scope=tidb-v${{ steps.extract-tidb-version.outputs.version }} + docker pull pingcap/tidb:v${{ matrix.db_version }} || true + docker pull pingcap/pd:v${{ matrix.db_version }} || true + docker pull pingcap/tikv:v${{ matrix.db_version }} || true - - name: Cache PD Docker image using Buildx - if: contains(matrix.target, 'tidb') - uses: docker/build-push-action@v5 - continue-on-error: false - with: - context: . - file: Dockerfile.tidb - push: false - tags: pingcap/pd:v${{ steps.extract-tidb-version.outputs.version }} - build-args: | - TIDB_COMPONENT=pingcap/pd - TIDB_VERSION=v${{ steps.extract-tidb-version.outputs.version }} - cache-from: type=gha,scope=pd-v${{ steps.extract-tidb-version.outputs.version }} - cache-to: type=gha,mode=max,scope=pd-v${{ steps.extract-tidb-version.outputs.version }} - - - name: Cache TiKV Docker image using Buildx - if: contains(matrix.target, 'tidb') - uses: docker/build-push-action@v5 - continue-on-error: false - with: - context: . - file: Dockerfile.tidb - push: false - tags: pingcap/tikv:v${{ steps.extract-tidb-version.outputs.version }} - build-args: | - TIDB_COMPONENT=pingcap/tikv - TIDB_VERSION=v${{ steps.extract-tidb-version.outputs.version }} - cache-from: type=gha,scope=tikv-v${{ steps.extract-tidb-version.outputs.version }} - cache-to: type=gha,mode=max,scope=tikv-v${{ steps.extract-tidb-version.outputs.version }} - - - name: Run tests {{ matrix.target }} + - name: Run testcontainers tests env: GOFLAGS: -mod=vendor - run: make ${{ matrix.target }} + TF_ACC: 1 + GOTOOLCHAIN: auto + run: | + export PATH="${{ github.workspace }}/bin:$PATH" + if [ "${{ matrix.db_type }}" == "tidb" ]; then + TIDB_VERSION=${{ matrix.db_version }} go test -tags=testcontainers -v ./mysql/... -run WithTestcontainers -timeout=30m + else + DOCKER_IMAGE=${{ matrix.docker_image }} go test -tags=testcontainers -v ./mysql/... -run WithTestcontainers -timeout=30m + fi # DISABLED to figure out GPG signing issue on Github Actions # possibly due to lack of TTY inside docker? # release: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..c4ad7976 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,15 @@ +# GolangCI-Lint configuration +# Suppress warnings from testcontainers dependencies + +linters-settings: + gci: + # Suppress warnings from third-party packages + skip-generated: true + +issues: + exclude-rules: + # Suppress warnings from go-m1cpu (testcontainers dependency) + - path: _test\.go + linters: + - gocritic + text: ".*go-m1cpu.*" diff --git a/.testcontainers-build-flags.sh b/.testcontainers-build-flags.sh new file mode 100755 index 00000000..7888a5d2 --- /dev/null +++ b/.testcontainers-build-flags.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Build flags to suppress warnings from testcontainers dependencies +# Usage: source .testcontainers-build-flags.sh before building + +export CGO_CFLAGS="-Wno-gnu-folding-constant" diff --git a/GNUmakefile b/Makefile similarity index 58% rename from GNUmakefile rename to Makefile index 403794d2..a36848a5 100644 --- a/GNUmakefile +++ b/Makefile @@ -5,8 +5,7 @@ PKG_NAME=mysql # Last version before hashicorp relicensing to BSL TERRAFORM_VERSION=1.5.6 TERRAFORM_OS=$(shell uname -s | tr A-Z a-z) -TEST_USER=root -TEST_PASSWORD=my-secret-pw +# Testcontainers-based testing - no need for manual Docker management DATESTAMP=$(shell date "+%Y%m%d") SHA_SHORT=$(shell git describe --match=FORCE_NEVER_MATCH --always --abbrev=40 --dirty --abbrev) MOST_RECENT_UPSTREAM_TAG=$(shell git for-each-ref refs/tags --sort=-taggerdate --format="%(refname)" | head -1 | grep -E -o "v\d+\.\d+\.\d+") @@ -34,78 +33,88 @@ VERSION=9.9.9 ## on linux base os TERRAFORM_PLUGINS_DIRECTORY=~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} -default: build - -build: fmtcheck +.PHONY: help +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Available targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo '' + @echo 'Examples:' + @echo ' make build Build the provider' + @echo ' make testversion8.0 Run tests against MySQL 8.0' + @echo ' make testtidb8.5.3 Run tests against TiDB 8.5.3' + @echo ' make acceptance Run all acceptance tests' + @echo ' make testcontainers-matrix Run test matrix across all database versions' + +default: help + +build: fmtcheck ## Build the provider go install -test: acceptance +test: testcontainers-matrix ## Run all acceptance tests +test-sequential: acceptance + +# Run testcontainers tests with a matrix of all database versions +# Usage: make testcontainers-matrix TESTARGS="TestAccUser" +testcontainers-matrix: fmtcheck bin/terraform ## Run test matrix across all database versions + @cd $(CURDIR) && PATH="$(CURDIR)/bin:${PATH}" PARALLEL=4 GOTOOLCHAIN=auto TF_ACC=1 go run scripts/test-runner.go $(if $(TESTARGS),$(TESTARGS),WithTestcontainers) + +# Run testcontainers tests for a specific database image +# Usage: make testcontainers-image DOCKER_IMAGE=mysql:8.0 +# make testcontainers-image TIDB_VERSION=8.5.3 +testcontainers-image: fmtcheck bin/terraform ## Run tests for a specific database image (set DOCKER_IMAGE or TIDB_VERSION) + @PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers $(TEST) -v $(TESTARGS) -timeout=15m -bin/terraform: +bin/terraform: ## Download Terraform binary mkdir -p "$(CURDIR)/bin" curl -sfL https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION)/terraform_$(TERRAFORM_VERSION)_$(TERRAFORM_OS)_$(ARCH).zip > $(CURDIR)/bin/terraform.zip (cd $(CURDIR)/bin/ ; unzip terraform.zip) -testacc: fmtcheck bin/terraform +testacc: fmtcheck bin/terraform ## Run acceptance tests (requires MYSQL_ENDPOINT env vars) PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout=90s # TiDB versions: latest of each minor series (must match .github/workflows/main.yml TIDB_VERSIONS) # 6.1.x → 6.1.7, 6.5.x → 6.5.12, 7.1.x → 7.1.6, 7.5.x → 7.5.7, 8.1.x → 8.1.2, 8.5.x → 8.5.3 -acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testmariadb10.3 testmariadb10.8 testmariadb10.10 testtidb6.1.7 testtidb6.5.12 testtidb7.1.6 testtidb7.5.7 testtidb8.1.2 testtidb8.5.3 - -testversion%: - $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=33$(shell echo "$*" | tr -d '.') testversion - -testversion: - -docker run --rm --name test-mysql$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 mysql:$(MYSQL_VERSION) - @echo 'Waiting for MySQL...' - @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" - -mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so';" - MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc - -docker rm -f test-mysql$(MYSQL_VERSION) - -testpercona%: - $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=34$(shell echo "$*" | tr -d '.') testpercona - -testpercona: - -docker run --rm --name test-percona$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 percona:$(MYSQL_VERSION) - @echo 'Waiting for Percona...' - @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" - -mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so';" - MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc - -docker rm -f test-percona$(MYSQL_VERSION) - -testrdsdb%: +acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testmariadb10.3 testmariadb10.8 testmariadb10.10 testtidb6.1.7 testtidb6.5.12 testtidb7.1.6 testtidb7.5.7 testtidb8.1.2 testtidb8.5.3 ## Run all acceptance tests across all database versions + +# MySQL test targets - use testcontainers +testversion%: ## Run tests against MySQL version (e.g., testversion8.0) + @DOCKER_IMAGE=mysql:$* PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +testversion: ## Run tests against MySQL version (set MYSQL_VERSION) + @DOCKER_IMAGE=mysql:$(MYSQL_VERSION) PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +# Percona test targets - use testcontainers +testpercona%: ## Run tests against Percona version (e.g., testpercona8.0) + @DOCKER_IMAGE=percona:$* PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +testpercona: ## Run tests against Percona version (set MYSQL_VERSION) + @DOCKER_IMAGE=percona:$(MYSQL_VERSION) PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +testrdsdb%: ## Run tests against RDS MySQL version (requires MYSQL_ENDPOINT env vars) $(MAKE) MYSQL_VERSION=$* MYSQL_USERNAME=${MYSQL_USERNAME} MYSQL_HOST=$(shell echo ${MYSQL_ENDPOINT} | cut -d: -f1) MYSQL_PASSWORD=${MYSQL_PASSWORD} MYSQL_PORT=$(shell echo ${MYSQL_ENDPOINT} | cut -d: -f2) testrdsdb -testrdsdb: +testrdsdb: ## Run tests against Amazon RDS (requires MYSQL_ENDPOINT env vars) @echo 'Waiting for AMAZON RDS...' @while ! mysql -h "$(MYSQL_HOST)" -P "$(MYSQL_PORT)" -u "$(MYSQL_USERNAME)" -p"$(MYSQL_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" $(MAKE) testacc -testtidb%: - $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=$(shell echo "$*" | awk -F. '{port=34000+($$2*100)+$$3; if(port>65535) port=34000+($$2*10)+$$3; printf "%d", port}') testtidb - -# WARNING: this does not work as a bare task run, it only instantiates correctly inside the versioned TiDB task run -# otherwise MYSQL_PORT and version are unset. -testtidb: - @MYSQL_VERSION=$(MYSQL_VERSION) MYSQL_PORT=$(MYSQL_PORT) $(CURDIR)/scripts/tidb-test-cluster.sh --init --port $(MYSQL_PORT) --version $(MYSQL_VERSION) || exit 1 - MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc; \ - TEST_RESULT=$$?; \ - MYSQL_VERSION=$(MYSQL_VERSION) MYSQL_PORT=$(MYSQL_PORT) $(CURDIR)/scripts/tidb-test-cluster.sh --destroy || true; \ - exit $$TEST_RESULT - -testmariadb%: - $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=6$(shell echo "$*" | tr -d '.') testmariadb - -testmariadb: - -docker run --rm --name test-mariadb$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 mariadb:$(MYSQL_VERSION) - @echo 'Waiting for MySQL...' - @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" - MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc - -docker rm -f test-mariadb$(MYSQL_VERSION) - -vet: +# TiDB test targets - use testcontainers +testtidb%: ## Run tests against TiDB version (e.g., testtidb8.5.3) + @TIDB_VERSION=$* PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +testtidb: ## Run tests against TiDB version (set MYSQL_VERSION) + @TIDB_VERSION=$(MYSQL_VERSION) PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +# MariaDB test targets - use testcontainers +testmariadb%: ## Run tests against MariaDB version (e.g., testmariadb10.10) + @DOCKER_IMAGE=mariadb:$* PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +testmariadb: ## Run tests against MariaDB version (set MYSQL_VERSION) + @DOCKER_IMAGE=mariadb:$(MYSQL_VERSION) PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 GOTOOLCHAIN=auto go test -tags=testcontainers ./mysql/... -v $(if $(TESTARGS),-run "$(TESTARGS).*WithTestcontainers",-run WithTestcontainers) -timeout=30m + +vet: ## Run go vet @echo "go vet ." @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ echo ""; \ @@ -114,23 +123,23 @@ vet: exit 1; \ fi -fmt: +fmt: ## Format Go code gofmt -w $(GOFMT_FILES) -deps: +deps: ## Update dependencies and vendor go mod tidy go mod vendor -fmtcheck: +fmtcheck: ## Check Go code formatting @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" -errcheck: +errcheck: ## Run errcheck @sh -c "'$(CURDIR)/scripts/errcheck.sh'" -vendor-status: +vendor-status: ## Show vendor status @govendor status -test-compile: +test-compile: ## Compile tests without running them @if [ "$(TEST)" = "./..." ]; then \ echo "ERROR: Set TEST to a specific package. For example,"; \ echo " make test-compile TEST=./$(PKG_NAME)"; \ @@ -138,7 +147,7 @@ test-compile: fi go test -c $(TEST) $(TESTARGS) -website: +website: ## Generate website documentation ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) echo "$(WEBSITE_REPO) not found in your GOPATH (necessary for layouts and assets), get-ting..." git clone https://$(WEBSITE_REPO) $(GOPATH)/src/$(WEBSITE_REPO) @@ -147,27 +156,27 @@ endif @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) -install: +install: ## Install provider to Terraform plugins directory mkdir -p ${TERRAFORM_PLUGINS_DIRECTORY} go build -o ${TERRAFORM_PLUGINS_DIRECTORY}/terraform-provider-${NAME} cd examples && rm -rf .terraform cd examples && make init -re-install: +re-install: ## Reinstall provider (removes lock file first) rm -f examples/.terraform.lock.hcl rm -f ${TERRAFORM_PLUGINS_DIRECTORY}/terraform-provider-${NAME} go build -o ${TERRAFORM_PLUGINS_DIRECTORY}/terraform-provider-${NAME} cd examples && rm -rf .terraform cd examples && terraform init -format-tag: +format-tag: ## Format tag string @echo $(MOST_RECENT_UPSTREAM_TAG)-$(DATESTAMP)-$(SHA_SHORT) -tag: +tag: ## Create git tag from VERSION file @echo git tag -a $(shell cat VERSION) -m $(shell cat VERSION) @git tag -a v$(shell cat VERSION) -m v$(shell cat VERSION) -release: +release: ## Create a release (tag, build, and optionally push to GitHub) @VERSION=$$(cat VERSION); \ TAG="v$$VERSION"; \ echo "Checking if tag $$TAG already exists..."; \ @@ -273,4 +282,4 @@ release: echo ""; \ echo "Release complete! Tag $$TAG has been pushed to GitHub." -.PHONY: build test testacc vet fmt fmtcheck errcheck vendor-status test-compile website website-test tag format-tag release +.PHONY: help build test testacc vet fmt fmtcheck errcheck vendor-status test-compile website website-test tag format-tag release diff --git a/TESTCONTAINERS_POC.md b/TESTCONTAINERS_POC.md new file mode 100644 index 00000000..af2dbd51 --- /dev/null +++ b/TESTCONTAINERS_POC.md @@ -0,0 +1,128 @@ +# Testcontainers Proof of Concept + +This directory contains a proof of concept implementation using Testcontainers instead of the Makefile + Docker approach. + +## What's Implemented + +1. **`mysql/testcontainers_helper.go`**: Helper functions for starting MySQL containers +2. **`mysql/data_source_databases_testcontainers_test.go`**: Example test using Testcontainers + +## How to Use + +### Prerequisites + +- Docker or Podman installed +- Go 1.21+ (project requirement) + +### Running Tests + +```bash +# Run the Testcontainers-based test +go test -tags=testcontainers -v ./mysql/... -run TestAccDataSourceDatabases_WithTestcontainers + +# Run all tests (both old and new) +go test -tags=testcontainers -v ./mysql/... +``` + +### With Podman + +Testcontainers automatically detects Podman. To use Podman explicitly: + +```bash +# Set Podman socket (if needed) +export CONTAINER_HOST=unix://$HOME/.local/share/containers/podman/machine/podman-machine-default/podman.sock + +# Run tests +go test -tags=testcontainers -v ./mysql/... -run TestAccDataSourceDatabases_WithTestcontainers +``` + +## How It Works + +1. **Build Tag**: Code is gated behind `// +build testcontainers` tag + - Old tests continue to work without Testcontainers + - New tests only run when `-tags=testcontainers` is used + +2. **Container Lifecycle**: + - Container starts automatically when test begins + - Environment variables are set for the test + - Container terminates automatically when test completes + +3. **Compatibility**: + - Works with Docker (default) + - Works with Podman (automatic detection) + - Uses GenericContainer for Go 1.21 compatibility + +## Example Test + +```go +func TestAccDataSourceDatabases_WithTestcontainers(t *testing.T) { + ctx := context.Background() + + // Start MySQL container + container := startMySQLContainer(ctx, t, "mysql:8.0") + defer container.SetupTestEnv(t)() + + // Run tests (same as before) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + // ... test steps + }, + }) +} +``` + +## Benefits Over Makefile Approach + +1. **No Shell Scripts**: Pure Go code +2. **Automatic Cleanup**: Containers terminate automatically +3. **Better Error Messages**: Container logs captured on failure +4. **Parallel Execution**: Go's testing framework handles parallelism +5. **Podman Support**: Works without configuration changes + +## Next Steps + +1. **Test Locally**: Verify it works with your Docker/Podman setup +2. **Measure Performance**: Compare startup time vs. Makefile approach +3. **Convert More Tests**: Gradually migrate other test files +4. **CI/CD Integration**: Update GitHub Actions to use Testcontainers + +## Troubleshooting + +### Compiler Warnings + +You may see warnings like: +``` +warning: variable length array folded to constant array as an extension [-Wgnu-folding-constant] +``` + +These are harmless warnings from the `go-m1cpu` dependency (used by testcontainers-go). They don't affect functionality. To suppress them: + +```bash +export CGO_CFLAGS="-Wno-gnu-folding-constant" +go test -tags=testcontainers -v ./mysql/... +``` + +Or use the provided script: +```bash +source .testcontainers-build-flags.sh +go test -tags=testcontainers -v ./mysql/... +``` + +### Container Won't Start + +- Check Docker/Podman is running: `docker ps` or `podman ps` +- Check image exists: `docker images mysql:8.0` +- Increase timeout in `testcontainers_helper.go` if needed + +### Port Conflicts + +- Testcontainers automatically assigns random ports +- No manual port calculation needed (unlike Makefile approach) + +### Podman Issues + +- Ensure Podman socket is accessible +- Check `CONTAINER_HOST` environment variable if needed +- Testcontainers should auto-detect Podman diff --git a/TESTCONTAINERS_SPIKE.md b/TESTCONTAINERS_SPIKE.md new file mode 100644 index 00000000..af6d7039 --- /dev/null +++ b/TESTCONTAINERS_SPIKE.md @@ -0,0 +1,583 @@ +# Spike: Replacing Makefile + Docker with Testcontainers + +## Current State + +### Problems with Current Approach + +1. **Complex Makefile**: 200+ lines of shell scripting with port calculations, container management, and cleanup logic +2. **Fragile Port Management**: Complex port calculation logic (`34$(tr -d '.')`) that breaks with longer version numbers +3. **Manual Container Lifecycle**: Containers must be manually started, waited for, and cleaned up +4. **TiDB Complexity**: Requires custom bash script (`tidb-test-cluster.sh`) with 200+ lines for multi-container orchestration +5. **No Parallel Execution**: Containers started sequentially, tests run sequentially +6. **Environment Variable Pollution**: Tests rely on global environment variables +7. **Hard to Debug**: Container failures require manual log inspection +8. **CI/CD Complexity**: GitHub Actions workflow has to coordinate Makefile targets with Docker + +### Current Test Flow + +``` +Makefile target → Docker run → Wait loop → Set env vars → Run tests → Cleanup +``` + +## Testcontainers Solution + +[Testcontainers](https://golang.testcontainers.org/) is a Go library that provides lightweight, throwaway instances of Docker containers for integration testing. + +### Benefits + +1. **Pure Go**: No shell scripts, no Makefile complexity +2. **Automatic Lifecycle**: Containers start/stop automatically with test lifecycle +3. **Built-in Waiting**: Automatic readiness checks (no manual wait loops) +4. **Parallel Execution**: Go's testing framework handles parallel test execution +5. **Isolated Tests**: Each test can have its own container instance +6. **Better Error Messages**: Container logs automatically captured on failure +7. **Type Safety**: Compile-time checks instead of runtime shell errors +8. **Simpler CI/CD**: Just run `go test` - no Makefile coordination needed +9. **Podman Support**: Works with Podman without special configuration (daemonless, rootless) + +## Proof of Concept + +### Example: Simple MySQL Test + +```go +package mysql + +import ( + "context" + "os" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + tcMySQL "github.com/testcontainers/testcontainers-go/modules/mysql" +) + +func TestAccDatabase_WithTestcontainers(t *testing.T) { + ctx := context.Background() + + // Start MySQL container + mysqlContainer, err := tcMySQL.RunContainer(ctx, + testcontainers.WithImage("mysql:8.0"), + tcMySQL.WithDatabase("testdb"), + tcMySQL.WithUsername("root"), + tcMySQL.WithPassword(""), + testcontainers.WithWaitStrategy( + wait.ForLog("ready for connections"). + WithOccurrence(2). + WithStartupTimeout(120*time.Second), + ), + ) + if err != nil { + t.Fatalf("Failed to start container: %v", err) + } + defer func() { + if err := mysqlContainer.Terminate(ctx); err != nil { + t.Fatalf("Failed to terminate container: %v", err) + } + }() + + // Get connection details + endpoint, err := mysqlContainer.ConnectionString(ctx) + if err != nil { + t.Fatalf("Failed to get connection string: %v", err) + } + + // Set environment variables for test + os.Setenv("MYSQL_ENDPOINT", endpoint) + os.Setenv("MYSQL_USERNAME", "root") + os.Setenv("MYSQL_PASSWORD", "") + defer func() { + os.Unsetenv("MYSQL_ENDPOINT") + os.Unsetenv("MYSQL_USERNAME") + os.Unsetenv("MYSQL_PASSWORD") + }() + + // Run existing test logic + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfig("testdb"), + Check: resource.ComposeTestCheckFunc( + testAccDatabaseExists("mysql_database.test"), + ), + }, + }, + }) +} +``` + +### Example: Test Helper Function + +```go +// testcontainers_helper.go + +package mysql + +import ( + "context" + "os" + "testing" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + tcMySQL "github.com/testcontainers/testcontainers-go/modules/mysql" +) + +type MySQLTestContainer struct { + Container testcontainers.Container + Endpoint string + Username string + Password string +} + +func startMySQLContainer(ctx context.Context, t *testing.T, image string) *MySQLTestContainer { + container, err := tcMySQL.RunContainer(ctx, + testcontainers.WithImage(image), + tcMySQL.WithDatabase("testdb"), + tcMySQL.WithUsername("root"), + tcMySQL.WithPassword(""), + testcontainers.WithWaitStrategy( + wait.ForLog("ready for connections"). + WithOccurrence(2). + WithStartupTimeout(120*time.Second), + ), + ) + if err != nil { + t.Fatalf("Failed to start MySQL container: %v", err) + } + + endpoint, err := container.ConnectionString(ctx) + if err != nil { + t.Fatalf("Failed to get connection string: %v", err) + } + + return &MySQLTestContainer{ + Container: container, + Endpoint: endpoint, + Username: "root", + Password: "", + } +} + +func (m *MySQLTestContainer) SetupTestEnv(t *testing.T) func() { + os.Setenv("MYSQL_ENDPOINT", m.Endpoint) + os.Setenv("MYSQL_USERNAME", m.Username) + os.Setenv("MYSQL_PASSWORD", m.Password) + + return func() { + os.Unsetenv("MYSQL_ENDPOINT") + os.Unsetenv("MYSQL_USERNAME") + os.Unsetenv("MYSQL_PASSWORD") + ctx := context.Background() + if err := m.Container.Terminate(ctx); err != nil { + t.Logf("Warning: Failed to terminate container: %v", err) + } + } +} + +// Usage in tests +func TestAccDatabase_Simple(t *testing.T) { + ctx := context.Background() + container := startMySQLContainer(ctx, t, "mysql:8.0") + defer container.SetupTestEnv(t)() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfig("testdb"), + Check: resource.ComposeTestCheckFunc( + testAccDatabaseExists("mysql_database.test"), + ), + }, + }, + }) +} +``` + +### Example: Percona and MariaDB Support + +```go +func startPerconaContainer(ctx context.Context, t *testing.T, version string) *MySQLTestContainer { + image := fmt.Sprintf("percona:%s", version) + // Percona uses same MySQL protocol, can use MySQL module + return startMySQLContainer(ctx, t, image) +} + +func startMariaDBContainer(ctx context.Context, t *testing.T, version string) *MySQLTestContainer { + image := fmt.Sprintf("mariadb:%s", version) + // MariaDB also uses MySQL protocol + return startMySQLContainer(ctx, t, image) +} +``` + +### Example: TiDB Multi-Container Setup + +```go +import ( + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/compose" +) + +func startTiDBCluster(ctx context.Context, t *testing.T, version string) *MySQLTestContainer { + // Option 1: Use Docker Compose module + composeFile := fmt.Sprintf(` +version: '3' +services: + pd: + image: pingcap/pd:v%s + command: --name=pd --data-dir=/data --client-urls=http://0.0.0.0:2379 --peer-urls=http://0.0.0.0:2380 + tikv: + image: pingcap/tikv:v%s + depends_on: [pd] + command: --addr=0.0.0.0:20160 --advertise-addr=tikv:20160 --pd=pd:2379 + tidb: + image: pingcap/tidb:v%s + depends_on: [tikv] + ports: + - "4000" + command: --store=tikv --path=pd:2379 +`, version, version, version) + + composeContainer, err := compose.NewDockerCompose(composeFile) + if err != nil { + t.Fatalf("Failed to create compose: %v", err) + } + + err = composeContainer.Up(ctx, compose.Wait(true)) + if err != nil { + t.Fatalf("Failed to start TiDB cluster: %v", err) + } + + // Get TiDB port + tidbPort, err := composeContainer.ServicePort(ctx, "tidb", 4000) + if err != nil { + t.Fatalf("Failed to get TiDB port: %v", err) + } + + return &MySQLTestContainer{ + Endpoint: fmt.Sprintf("127.0.0.1:%s", tidbPort.Port()), + Username: "root", + Password: "", + } +} + +// Option 2: Manual multi-container setup +func startTiDBClusterManual(ctx context.Context, t *testing.T, version string) *MySQLTestContainer { + networkName := "tidb-test-network" + network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ + NetworkRequest: testcontainers.NetworkRequest{ + Name: networkName, + }, + }) + if err != nil { + t.Fatalf("Failed to create network: %v", err) + } + + // Start PD + pdContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/pd:v%s", version), + Networks: []string{networkName}, + Cmd: []string{ + "--name=pd", + "--data-dir=/data", + "--client-urls=http://0.0.0.0:2379", + "--peer-urls=http://0.0.0.0:2380", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to start PD: %v", err) + } + + // Start TiKV + tikvContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tikv:v%s", version), + Networks: []string{networkName}, + Cmd: []string{ + "--addr=0.0.0.0:20160", + "--advertise-addr=tikv:20160", + "--pd=pd:2379", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to start TiKV: %v", err) + } + + // Start TiDB + tidbContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tidb:v%s", version), + ExposedPorts: []string{"4000/tcp"}, + Networks: []string{networkName}, + Cmd: []string{ + "--store=tikv", + "--path=pd:2379", + }, + WaitingFor: wait.ForLog("server is running MySQL protocol").WithStartupTimeout(240 * time.Second), + }, + }) + if err != nil { + t.Fatalf("Failed to start TiDB: %v", err) + } + + tidbPort, err := tidbContainer.MappedPort(ctx, "4000") + if err != nil { + t.Fatalf("Failed to get TiDB port: %v", err) + } + + return &MySQLTestContainer{ + Endpoint: fmt.Sprintf("127.0.0.1:%s", tidbPort.Port()), + Username: "root", + Password: "", + } +} +``` + +### Example: Table-Driven Tests for Multiple Versions + +```go +func TestAccDatabase_MultipleVersions(t *testing.T) { + versions := []struct { + name string + image string + }{ + {"MySQL5.6", "mysql:5.6"}, + {"MySQL5.7", "mysql:5.7"}, + {"MySQL8.0", "mysql:8.0"}, + {"Percona5.7", "percona:5.7"}, + {"Percona8.0", "percona:8.0"}, + {"MariaDB10.3", "mariadb:10.3"}, + {"MariaDB10.8", "mariadb:10.8"}, + {"MariaDB10.10", "mariadb:10.10"}, + } + + for _, v := range versions { + t.Run(v.name, func(t *testing.T) { + ctx := context.Background() + container := startMySQLContainer(ctx, t, v.image) + defer container.SetupTestEnv(t)() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfig("testdb"), + Check: resource.ComposeTestCheckFunc( + testAccDatabaseExists("mysql_database.test"), + ), + }, + }, + }) + }) + } +} +``` + +## Migration Strategy + +### Phase 1: Proof of Concept (Current) +- Create spike document ✅ +- Implement helper functions for MySQL/Percona/MariaDB +- Convert one test file as proof of concept +- Measure performance and reliability + +### Phase 2: Gradual Migration +- Convert tests file-by-file +- Keep Makefile targets as fallback during migration +- Update CI/CD to support both approaches + +### Phase 3: Complete Migration +- Remove Makefile targets +- Remove shell scripts (`tidb-test-cluster.sh`) +- Simplify GitHub Actions workflow +- Update documentation + +## Benefits Analysis + +### Development Experience +- ✅ **Simpler**: No Makefile, no shell scripts +- ✅ **Type Safe**: Compile-time errors instead of runtime shell errors +- ✅ **Better IDE Support**: Go tooling works out of the box +- ✅ **Easier Debugging**: Container logs automatically captured + +### Testing +- ✅ **Parallel Execution**: Go's testing framework handles this +- ✅ **Isolation**: Each test can have its own container +- ✅ **Reliability**: Automatic retries and better error handling +- ✅ **Port Management**: No manual port calculation needed + +### CI/CD +- ✅ **Simpler Workflow**: Just `go test` - no Makefile coordination +- ✅ **Better Caching**: Testcontainers can reuse containers +- ✅ **Faster**: Parallel test execution +- ✅ **More Reliable**: Better error messages and retry logic + +## Trade-offs and Considerations + +### Challenges + +1. **TiDB Complexity**: Multi-container setup still complex, but better than shell scripts +2. **Learning Curve**: Team needs to learn Testcontainers API +3. **Container Runtime Dependency**: Requires Docker or Podman (same as current approach) +4. **Migration Effort**: Need to convert all tests + +### Potential Issues + +1. **Container Startup Time**: May be slower than optimized Makefile approach + - **Mitigation**: Testcontainers has built-in caching and reuse + +2. **Resource Usage**: Multiple containers running in parallel + - **Mitigation**: Containers are lightweight, can limit parallelism + +3. **Network Issues**: Docker networking complexity + - **Mitigation**: Testcontainers handles this automatically + +## Podman Support + +Testcontainers Go automatically detects and works with Podman when Docker is not available or when `TESTCONTAINERS_RYUK_DISABLED=true` is set. Podman support is transparent - no code changes needed. + +### Podman Benefits + +1. **Rootless**: Can run without root privileges +2. **Daemonless**: No background daemon required +3. **Drop-in Replacement**: API-compatible with Docker +4. **Better Security**: Uses user namespaces + +### Podman Configuration + +Testcontainers will automatically use Podman if: +- Docker is not available, OR +- `CONTAINER_HOST` environment variable points to Podman socket + +Example: +```bash +# Use Podman explicitly +export CONTAINER_HOST=unix://$HOME/.local/share/containers/podman/machine/podman-machine-default/podman.sock + +# Or let Testcontainers auto-detect +# (it will try Docker first, then Podman) +``` + +### Podman Considerations + +- **Socket Path**: Podman socket location varies by installation +- **Rootless Mode**: May have different networking behavior +- **Compose Support**: Docker Compose module may not work with Podman (use manual multi-container setup for TiDB) + +## Implementation Plan + +### Step 1: Add Dependencies + +```bash +go get github.com/testcontainers/testcontainers-go +go get github.com/testcontainers/testcontainers-go/modules/mysql +go get github.com/testcontainers/testcontainers-go/modules/compose +``` + +### Step 1.5: Verify Podman Support (Optional) + +Test Podman compatibility: +```bash +# With Podman installed +export CONTAINER_HOST=unix://$HOME/.local/share/containers/podman/machine/podman-machine-default/podman.sock +go test -tags=spike ./mysql/... -run ExampleTestAccDatabase_WithTestcontainers +``` + +### Step 2: Create Helper Package + +Create `mysql/testcontainers_helper.go` with: +- `startMySQLContainer()` - MySQL/Percona/MariaDB +- `startTiDBCluster()` - TiDB multi-container setup +- `MySQLTestContainer` struct with cleanup logic + +### Step 3: Convert One Test File + +Start with `data_source_databases_test.go` as proof of concept: +- Replace `testAccPreCheck` to use Testcontainers +- Verify it works locally +- Measure performance + +### Step 4: Update CI/CD + +Update `.github/workflows/main.yml`: +- Remove Makefile targets +- Use `go test -tags=acceptance` with build tags +- Run tests in parallel using Go's built-in parallelism + +### Step 5: Gradual Migration + +Convert tests file-by-file: +1. `data_source_databases_test.go` +2. `resource_database_test.go` +3. `resource_user_test.go` +4. ... (continue with remaining files) + +## Performance Comparison + +### Current Approach +- Sequential container startup: ~10-30s per version +- Sequential test execution: ~60-90s per test suite +- Total for 13 versions: ~15-20 minutes + +### Testcontainers Approach (Estimated) +- Parallel container startup: ~10-30s (same, but parallel) +- Parallel test execution: ~60-90s (same, but parallel) +- Total for 13 versions: ~2-3 minutes (with parallelism) + +## Next Steps + +1. **Review this spike** with team +2. **Implement proof of concept** for one test file +3. **Measure performance** and compare with current approach +4. **Decide on migration strategy** (gradual vs. all-at-once) +5. **Create migration tickets** if approved + +## Podman-Specific Implementation Notes + +### TiDB with Podman + +Since Docker Compose may not work with Podman, use manual multi-container setup: + +```go +func startTiDBClusterPodman(ctx context.Context, t *testing.T, version string) *MySQLTestContainer { + // Use GenericContainer instead of Compose module + // This works with both Docker and Podman + return startTiDBClusterManual(ctx, t, version) +} +``` + +### Testing Podman Compatibility + +Add a build tag to test Podman specifically: + +```go +// +build podman + +// Test with Podman +func TestPodmanCompatibility(t *testing.T) { + // Verify Testcontainers detects Podman + // Run subset of tests to verify compatibility +} +``` + +Run with: +```bash +go test -tags=podman ./mysql/... +``` + +## References + +- [Testcontainers Go Documentation](https://golang.testcontainers.org/) +- [Testcontainers MySQL Module](https://golang.testcontainers.org/modules/mysql/) +- [Testcontainers Compose Module](https://golang.testcontainers.org/modules/compose/) +- [Testcontainers Podman Support](https://golang.testcontainers.org/features/container_daemons/) +- [Example: Terraform Provider Testing](https://github.com/testcontainers/testcontainers-go/tree/main/examples) diff --git a/go.mod b/go.mod index 03090884..c79a7b5b 100644 --- a/go.mod +++ b/go.mod @@ -10,35 +10,58 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 + github.com/olekukonko/tablewriter v1.1.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 github.com/tidwall/gjson v1.17.1 - golang.org/x/net v0.26.0 - golang.org/x/oauth2 v0.21.0 + golang.org/x/net v0.45.0 + golang.org/x/oauth2 v0.22.0 google.golang.org/api v0.185.0 ) require ( cloud.google.com/go/auth v0.5.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/clipperhouse/displaywidth v0.3.1 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.8 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -56,38 +79,66 @@ require ( github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.1.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) -go 1.21.11 +go 1.25 diff --git a/go.sum b/go.sum index bcbb174e..3e153149 100644 --- a/go.sum +++ b/go.sum @@ -5,23 +5,27 @@ cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKF cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/cloudsqlconn v1.11.0 h1:/OEFGCNUG2TWefrsfNJE0UWJ+7/YIkkNUWd06ZGbAL4= cloud.google.com/go/cloudsqlconn v1.11.0/go.mod h1:c0f/ftQkRxEpIfGNQQIeBtE6Nj+17x9tol/WaGhNSEA= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o= github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton h1:HKz85FwoXx86kVtTvFke7rgHvq/HoloSUvW5semjFWs= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -31,11 +35,31 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk= +github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= @@ -43,6 +67,16 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -65,6 +99,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -103,8 +139,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -114,6 +151,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -183,6 +222,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -192,6 +233,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -201,6 +246,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -213,30 +260,75 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0= +github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0= +github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 h1:P9Txfy5Jothx2wFdcus0QoSmX/PKSIXZxrTbZPVJswA= +github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0/go.mod h1:oZPHHqJqXG7FD8OB/yWH7gLnDvZUlFHAVJNrGftL+eg= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -244,6 +336,10 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -254,32 +350,44 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -289,44 +397,52 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -336,8 +452,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.185.0 h1:ENEKk1k4jW8SmmaT6RE+ZasxmxezCrD5Vw4npvr+pAU= @@ -350,17 +466,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 h1:CUiCqkPw1nNrNQzCCG4WA65m0nAmQiwXHpub3dNyruU= -google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= -google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -383,5 +499,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/mysql/data_source_databases_testcontainers_test.go b/mysql/data_source_databases_testcontainers_test.go new file mode 100644 index 00000000..d8625a7d --- /dev/null +++ b/mysql/data_source_databases_testcontainers_test.go @@ -0,0 +1,51 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccDataSourceDatabases_WithTestcontainers is a proof of concept test +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain +func TestAccDataSourceDatabases_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + // Run the same test logic as the original test + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDatabasesConfigBasic("%"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mysql_databases.test", "pattern", "%"), + testAccDatabasesCount("data.mysql_databases.test", "databases.#", func(rn string, databaseCount int) error { + if databaseCount < 1 { + return fmt.Errorf("%s: databases not found", rn) + } + return nil + }), + ), + }, + { + Config: testAccDatabasesConfigBasic("__database_does_not_exist__"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mysql_databases.test", "pattern", "__database_does_not_exist__"), + testAccDatabasesCount("data.mysql_databases.test", "databases.#", func(rn string, databaseCount int) error { + if databaseCount > 0 { + return fmt.Errorf("%s: unexpected database found", rn) + } + return nil + }), + ), + }, + }, + }) +} diff --git a/mysql/data_source_tables_testcontainers_test.go b/mysql/data_source_tables_testcontainers_test.go new file mode 100644 index 00000000..4ad7cf1d --- /dev/null +++ b/mysql/data_source_tables_testcontainers_test.go @@ -0,0 +1,53 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccDataSourceTables_WithTestcontainers tests the mysql_tables data source +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain +func TestAccDataSourceTables_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + // Run the same test logic as the original test + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccTablesConfigBasic("mysql", "%"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mysql_tables.test", "database", "mysql"), + resource.TestCheckResourceAttr("data.mysql_tables.test", "pattern", "%"), + testAccTablesCount("data.mysql_tables.test", "tables.#", func(rn string, tableCount int) error { + if tableCount < 1 { + return fmt.Errorf("%s: tables not found", rn) + } + return nil + }), + ), + }, + { + Config: testAccTablesConfigBasic("mysql", "__table_does_not_exist__"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mysql_tables.test", "database", "mysql"), + resource.TestCheckResourceAttr("data.mysql_tables.test", "pattern", "__table_does_not_exist__"), + testAccTablesCount("data.mysql_tables.test", "tables.#", func(rn string, tableCount int) error { + if tableCount > 0 { + return fmt.Errorf("%s: unexpected table found", rn) + } + return nil + }), + ), + }, + }, + }) +} diff --git a/mysql/resource_database_placement_policy_testcontainers_test.go b/mysql/resource_database_placement_policy_testcontainers_test.go new file mode 100644 index 00000000..bd2ab492 --- /dev/null +++ b/mysql/resource_database_placement_policy_testcontainers_test.go @@ -0,0 +1,9 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +// Note: TestAccDatabase_placementPolicyChange_WithTestcontainers is skipped +// because placement policies are TiDB-specific and require a TiDB cluster setup, +// which is more complex than a simple MySQL container. +// This test would need special TiDB container orchestration. diff --git a/mysql/resource_database_testcontainers_test.go b/mysql/resource_database_testcontainers_test.go new file mode 100644 index 00000000..dd80d0d9 --- /dev/null +++ b/mysql/resource_database_testcontainers_test.go @@ -0,0 +1,92 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccDatabase_WithTestcontainers is a proof of concept test +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain +func TestAccDatabase_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := "terraform_acceptance_test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccDatabaseCheckDestroy(dbName), + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfigBasic(dbName), + Check: testAccDatabaseCheckBasic( + "mysql_database.test", dbName, + ), + }, + { + Config: testAccDatabaseConfigBasic(dbName), + ResourceName: "mysql_database.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: dbName, + }, + }, + }) +} + +// TestAccDatabase_collationChange_WithTestcontainers tests collation changes +// Uses shared container set up in TestMain +func TestAccDatabase_collationChange_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := "terraform_acceptance_test" + + charset1 := "latin1" + charset2 := "utf8mb4" + collation1 := "latin1_bin" + collation2 := "utf8mb4_general_ci" + + resourceName := "mysql_database.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccDatabaseCheckDestroy(dbName), + Steps: []resource.TestStep{ + { + Config: testAccDatabaseConfigFull(dbName, charset1, collation1, ""), + Check: resource.ComposeTestCheckFunc( + testAccDatabaseCheckFull("mysql_database.test", dbName, charset1, collation1, ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + PreConfig: func() { + ctx := context.Background() + db, err := connectToMySQL(ctx, testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + return + } + + db.Exec(fmt.Sprintf("ALTER DATABASE %s CHARACTER SET %s COLLATE %s", dbName, charset2, collation2)) + }, + Config: testAccDatabaseConfigFull(dbName, charset1, collation1, ""), + Check: resource.ComposeTestCheckFunc( + testAccDatabaseCheckFull(resourceName, dbName, charset1, collation1, ""), + ), + }, + }, + }) +} diff --git a/mysql/resource_default_roles_testcontainers_test.go b/mysql/resource_default_roles_testcontainers_test.go new file mode 100644 index 00000000..c8e2f166 --- /dev/null +++ b/mysql/resource_default_roles_testcontainers_test.go @@ -0,0 +1,65 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccDefaultRoles_basic_WithTestcontainers tests the mysql_default_roles resource +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain (MySQL 8.0 required for default roles) +func TestAccDefaultRoles_basic_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccDefaultRolesCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDefaultRolesBasic, + Check: resource.ComposeTestCheckFunc( + testAccDefaultRoles("mysql_default_roles.test", "role1"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.#", "1"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.0", "role1"), + ), + }, + { + Config: testAccDefaultRolesMultiple, + Check: resource.ComposeTestCheckFunc( + testAccDefaultRoles("mysql_default_roles.test", "role1", "role2"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.#", "2"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.0", "role1"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.1", "role2"), + ), + }, + { + Config: testAccDefaultRolesNone, + Check: resource.ComposeTestCheckFunc( + testAccDefaultRoles("mysql_default_roles.test"), + resource.TestCheckResourceAttr("mysql_default_roles.test", "roles.#", "0"), + ), + }, + { + Config: testAccDefaultRolesBasic, + ResourceName: "mysql_default_roles.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%v@%v", "jdoe", "%"), + }, + { + Config: testAccDefaultRolesMultiple, + ResourceName: "mysql_default_roles.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%v@%v", "jdoe", "%"), + }, + }, + }) +} diff --git a/mysql/resource_global_variable_testcontainers_test.go b/mysql/resource_global_variable_testcontainers_test.go new file mode 100644 index 00000000..a69049c6 --- /dev/null +++ b/mysql/resource_global_variable_testcontainers_test.go @@ -0,0 +1,67 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccGlobalVar_basic_WithTestcontainers tests the mysql_global_variable resource +// Requires MySQL (not MariaDB/RDS) +// Uses shared container set up in TestMain +func TestAccGlobalVar_basic_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + varName := "max_connections" + resourceName := "mysql_global_variable.test" + varValue := "1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGlobalVarCheckDestroy(varName, varValue), + Steps: []resource.TestStep{ + { + Config: testAccGlobalVarConfigBasic(varName, varValue), + Check: resource.ComposeTestCheckFunc( + testAccGlobalVarExists(varName, varValue), + resource.TestCheckResourceAttr(resourceName, "name", varName), + ), + }, + }, + }) +} + +// TestAccGlobalVar_parseBoolean_WithTestcontainers tests boolean parsing +// Requires MySQL (not MariaDB/RDS) +// Uses shared container set up in TestMain +func TestAccGlobalVar_parseBoolean_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + varName := "autocommit" + resourceName := "mysql_global_variable.test" + varValue := "OFF" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGlobalVarCheckDestroy(varName, varValue), + Steps: []resource.TestStep{ + { + Config: testAccGlobalVarConfigBasic(varName, varValue), + Check: resource.ComposeTestCheckFunc( + testAccGlobalVarExists(varName, varValue), + resource.TestCheckResourceAttr(resourceName, "name", varName), + ), + }, + }, + }) +} + +// Note: TestAccGlobalVar_parseString and TestAccGlobalVar_parseFloat are TiDB-specific +// and require TiDB containers, so they are not converted here. diff --git a/mysql/resource_grant_testcontainers_test.go b/mysql/resource_grant_testcontainers_test.go new file mode 100644 index 00000000..d5bd1032 --- /dev/null +++ b/mysql/resource_grant_testcontainers_test.go @@ -0,0 +1,515 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "fmt" + "math/rand" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccGrant_WithTestcontainers tests basic grant functionality +// Uses shared container set up in TestMain +func TestAccGrant_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + userName := fmt.Sprintf("jdoe-%s", dbName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", userName), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), + ), + }, + { + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", userName), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + ), + }, + { + Config: testAccGrantConfigBasic(dbName), + ResourceName: "mysql_grant.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%v@%v@%v@%v", userName, "example.com", dbName, "*"), + }, + }, + }) +} + +// TestAccRevokePrivRefresh_WithTestcontainers tests privilege revocation and refresh +func TestAccRevokePrivRefresh_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "UPDATE", true, false), + ), + }, + { + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: resource.ComposeTestCheckFunc( + revokeUserPrivs(dbName, "UPDATE"), + ), + }, + { + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "UPDATE", false, false), + ), + }, + { + PlanOnly: true, + ExpectNonEmptyPlan: true, + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "UPDATE", false, false), + ), + }, + { + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "UPDATE", true, false), + ), + }, + }, + }) +} + +// TestAccBroken_WithTestcontainers tests error handling for duplicate grants +func TestAccBroken_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigBasic(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), + ), + }, + { + Config: testAccGrantConfigBroken(dbName), + ExpectError: regexp.MustCompile("already has"), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), + ), + }, + }, + }) +} + +// TestAccDifferentHosts_WithTestcontainers tests grants with different hosts +func TestAccDifferentHosts_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigExtraHost(dbName, false), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test_all", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test_all", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test_all", "host", "%"), + resource.TestCheckResourceAttr("mysql_grant.test_all", "table", "*"), + ), + }, + { + Config: testAccGrantConfigExtraHost(dbName, true), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "10.1.2.3"), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), + resource.TestCheckResourceAttr("mysql_grant.test_all", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test_all", "host", "%"), + resource.TestCheckResourceAttr("mysql_grant.test_all", "table", "*"), + ), + }, + }, + }) +} + +// TestAccGrantComplex_WithTestcontainers tests complex grant scenarios +func TestAccGrantComplex_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + // Create table first + Config: testAccGrantConfigNoGrant(dbName), + Check: resource.ComposeTestCheckFunc( + prepareTable(dbName, "tbl"), + ), + }, + { + Config: testAccGrantConfigWithPrivs(dbName, `"SELECT (c1, c2)"`, false), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), + ), + }, + { + Config: testAccGrantConfigWithPrivs(dbName, `"DROP", "SELECT (c1)", "INSERT(c3, c4)", "REFERENCES(c5)"`, false), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "INSERT (c3,c4)", true, false), + testAccPrivilege("mysql_grant.test", "SELECT (c1)", true, false), + testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", false, false), + testAccPrivilege("mysql_grant.test", "REFERENCES (c5)", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), + ), + }, + { + Config: testAccGrantConfigWithPrivs(dbName, `"ALL PRIVILEGES"`, false), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "ALL", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), + ), + }, + { + Config: testAccGrantConfigWithPrivs(dbName, `"SELECT (c1, c2)","UPDATE(c1, c2)"`, true), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "SELECT (c1,c2)", true, true), + testAccPrivilege("mysql_grant.test", "UPDATE (c1,c2)", true, true), + testAccPrivilege("mysql_grant.test", "ALL", false, true), + testAccPrivilege("mysql_grant.test", "DROP", false, true), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", dbName), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "tbl"), + ), + }, + { + Config: testAccGrantConfigNoGrant(dbName), + }, + }, + }) +} + +// TestAccGrantComplexMySQL8_WithTestcontainers tests MySQL 8.0 specific grants +func TestAccGrantComplexMySQL8_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigWithDynamicMySQL8(dbName), + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.test", "CONNECTION_ADMIN", true, false), + testAccPrivilege("mysql_grant.test", "FIREWALL_EXEMPT", true, false), + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "database", "*"), + resource.TestCheckResourceAttr("mysql_grant.test", "table", "*"), + ), + }, + }, + }) +} + +// TestAccGrant_role_WithTestcontainers tests role grants (requires MySQL 8.0+) +func TestAccGrant_role_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + roleName := fmt.Sprintf("TFRole-exp%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigRole(dbName, roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("mysql_grant.test", "role", roleName), + ), + }, + { + Config: testAccGrantConfigRoleWithGrantOption(dbName, roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("mysql_grant.test", "role", roleName), + resource.TestCheckResourceAttr("mysql_grant.test", "grant", "true"), + ), + }, + { + Config: testAccGrantConfigRole(dbName, roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("mysql_grant.test", "role", roleName), + ), + }, + }, + }) +} + +// TestAccGrant_roleToUser_WithTestcontainers tests granting roles to users (requires MySQL 8.0+) +func TestAccGrant_roleToUser_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + roleName := fmt.Sprintf("TFRole-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigRoleToUser(dbName, roleName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("mysql_grant.test", "user", fmt.Sprintf("jdoe-%s", dbName)), + resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_grant.test", "roles.#", "1"), + ), + }, + }, + }) +} + +// TestAccGrant_complexRoleGrants_WithTestcontainers tests complex role grant scenarios (requires MySQL 8.0+) +func TestAccGrant_complexRoleGrants_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigComplexRoleGrants(dbName), + }, + }, + }) +} + +// TestAccGrantOnProcedure_WithTestcontainers tests procedure grants +func TestAccGrantOnProcedure_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + procedureName := "test_procedure" + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + userName := fmt.Sprintf("jdoe-%s", dbName) + hostName := "%" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + // Create table first + Config: testAccGrantConfigNoGrant(dbName), + Check: resource.ComposeTestCheckFunc( + prepareTable(dbName, "tbl"), + ), + }, + { + // Create a procedure + Config: testAccGrantConfigNoGrant(dbName), + Check: resource.ComposeTestCheckFunc( + prepareProcedure(dbName, procedureName), + ), + }, + { + Config: testAccGrantConfigProcedureWithTable(procedureName, dbName, hostName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProcedureGrant("mysql_grant.test_procedure", userName, hostName, procedureName, true), + resource.TestCheckResourceAttr("mysql_grant.test_procedure", "user", userName), + resource.TestCheckResourceAttr("mysql_grant.test_procedure", "host", hostName), + ), + }, + }, + }) +} + +// TestAllowDuplicateUsersDifferentTables_WithTestcontainers tests allowing duplicate grants on different tables +func TestAllowDuplicateUsersDifferentTables_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + + duplicateUserConfig := fmt.Sprintf(` + resource "mysql_database" "test" { + name = "%s" + } + + resource "mysql_user" "test" { + user = "jdoe-%s" + host = "example.com" + } + + resource "mysql_grant" "grant1" { + user = "${mysql_user.test.user}" + host = "${mysql_user.test.host}" + database = "${mysql_database.test.name}" + table = "table1" + privileges = ["UPDATE", "SELECT"] + } + + resource "mysql_grant" "grant2" { + user = "${mysql_user.test.user}" + host = "${mysql_user.test.host}" + database = "${mysql_database.test.name}" + table = "table2" + privileges = ["UPDATE", "SELECT"] + } + `, dbName, dbName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + // Create table first + Config: testAccGrantConfigNoGrant(dbName), + Check: resource.ComposeTestCheckFunc( + prepareTable(dbName, "table1"), + prepareTable(dbName, "table2"), + ), + }, + { + Config: duplicateUserConfig, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant1", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.grant1", "table", "table1"), + testAccPrivilege("mysql_grant.grant2", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.grant2", "table", "table2"), + ), + }, + { + RefreshState: true, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant1", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.grant1", "table", "table1"), + testAccPrivilege("mysql_grant.grant2", "SELECT", true, false), + resource.TestCheckResourceAttr("mysql_grant.grant2", "table", "table2"), + ), + }, + }, + }) +} + +// TestDisallowDuplicateUsersSameTable_WithTestcontainers tests disallowing duplicate grants on same table +func TestDisallowDuplicateUsersSameTable_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) + + duplicateUserConfig := fmt.Sprintf(` + resource "mysql_database" "test" { + name = "%s" + } + + resource "mysql_user" "test" { + user = "jdoe-%s" + host = "example.com" + } + + resource "mysql_grant" "grant1" { + user = "${mysql_user.test.user}" + host = "${mysql_user.test.host}" + database = "${mysql_database.test.name}" + table = "table1" + privileges = ["UPDATE", "SELECT"] + } + + resource "mysql_grant" "grant2" { + user = "${mysql_user.test.user}" + host = "${mysql_user.test.host}" + database = "${mysql_database.test.name}" + table = "table1" + privileges = ["UPDATE", "SELECT"] + } + `, dbName, dbName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigNoGrant(dbName), + Check: resource.ComposeTestCheckFunc( + prepareTable(dbName, "table1"), + ), + }, + { + Config: duplicateUserConfig, + ExpectError: regexp.MustCompile("already has"), + }, + }, + }) +} diff --git a/mysql/resource_role_testcontainers_test.go b/mysql/resource_role_testcontainers_test.go new file mode 100644 index 00000000..97e532ca --- /dev/null +++ b/mysql/resource_role_testcontainers_test.go @@ -0,0 +1,36 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccRole_basic_WithTestcontainers tests the mysql_role resource +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain (MySQL 8.0 required for roles) +func TestAccRole_basic_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + roleName := "tf-test-role" + resourceName := "mysql_role.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccRoleCheckDestroy(roleName), + Steps: []resource.TestStep{ + { + Config: testAccRoleConfigBasic(roleName), + Check: resource.ComposeTestCheckFunc( + testAccRoleExists(roleName), + resource.TestCheckResourceAttr(resourceName, "name", roleName), + ), + }, + }, + }) +} diff --git a/mysql/resource_user_password_testcontainers_test.go b/mysql/resource_user_password_testcontainers_test.go new file mode 100644 index 00000000..6a03055e --- /dev/null +++ b/mysql/resource_user_password_testcontainers_test.go @@ -0,0 +1,34 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccUserPassword_basic_WithTestcontainers tests the mysql_user_password resource +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain +func TestAccUserPassword_basic_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserPasswordConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user_password.test", "user", "jdoe"), + resource.TestCheckResourceAttrSet("mysql_user_password.test", "plaintext_password"), + ), + }, + }, + }) +} diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index fcfbc56d..053fb768 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -55,7 +55,26 @@ func TestAccUser_basic(t *testing.T) { func TestAccUser_auth(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheckSkipTiDB(t); testAccPreCheckSkipMariaDB(t); testAccPreCheckSkipRds(t) }, + PreCheck: func() { + testAccPreCheckSkipTiDB(t) + testAccPreCheckSkipMariaDB(t) + testAccPreCheckSkipRds(t) + // Check if mysql_no_login plugin is available + // This plugin may not be available in all MySQL distributions + ctx := context.Background() + db, err := connectToMySQL(ctx, testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + t.Fatalf("Cannot connect to DB: %v", err) + } + // Don't close - connection is cached and shared + + // Check if plugin exists + var pluginName string + err = db.QueryRowContext(ctx, "SELECT PLUGIN_NAME FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'mysql_no_login'").Scan(&pluginName) + if err != nil { + t.Skip("mysql_no_login plugin is not available in this MySQL distribution") + } + }, ProviderFactories: testAccProviderFactories, CheckDestroy: testAccUserCheckDestroy, Steps: []resource.TestStep{ diff --git a/mysql/resource_user_testcontainers_test.go b/mysql/resource_user_testcontainers_test.go new file mode 100644 index 00000000..740107a8 --- /dev/null +++ b/mysql/resource_user_testcontainers_test.go @@ -0,0 +1,229 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// TestAccUser_basic_WithTestcontainers tests the mysql_user resource +// using Testcontainers instead of Makefile + Docker +// Uses shared container set up in TestMain +func TestAccUser_basic_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "%"), + resource.TestCheckResourceAttr("mysql_user.test", "plaintext_password", hashSum("password")), + resource.TestCheckResourceAttr("mysql_user.test", "tls_option", "NONE"), + ), + }, + { + Config: testAccUserConfig_ssl, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "plaintext_password", hashSum("password")), + resource.TestCheckResourceAttr("mysql_user.test", "tls_option", "SSL"), + ), + }, + { + Config: testAccUserConfig_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "%"), + resource.TestCheckResourceAttr("mysql_user.test", "plaintext_password", hashSum("password2")), + resource.TestCheckResourceAttr("mysql_user.test", "tls_option", "NONE"), + ), + }, + }, + }) +} + +// TestAccUser_auth_WithTestcontainers tests auth plugin functionality +// Requires MySQL (not TiDB/MariaDB/RDS) with mysql_no_login plugin +// Uses shared container set up in TestMain +// Note: mysql_no_login plugin may not be available in all MySQL distributions +func TestAccUser_auth_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + // Check if mysql_no_login plugin is available + ctx := context.Background() + db, err := connectToMySQL(ctx, testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + t.Fatalf("Cannot connect to DB: %v", err) + } + // Don't close - connection is cached and shared + + // Check if plugin exists + var pluginName string + err = db.QueryRowContext(ctx, "SELECT PLUGIN_NAME FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'mysql_no_login'").Scan(&pluginName) + if err != nil { + t.Skip("mysql_no_login plugin is not available in this MySQL distribution") + } + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_auth_iam_plugin, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "mysql_no_login"), + ), + }, + { + Config: testAccUserConfig_auth_native, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "mysql_native_password"), + ), + }, + { + Config: testAccUserConfig_auth_iam_plugin, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "auth_plugin", "mysql_no_login"), + ), + }, + }, + }) +} + +// TestAccUser_authConnect_WithTestcontainers tests password authentication +// Requires MySQL (not TiDB/MariaDB/RDS) +// Uses shared container set up in TestMain +func TestAccUser_authConnect_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password"), + ), + }, + { + Config: testAccUserConfig_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "random"), + ), + ExpectError: regexp.MustCompile(`.*Access denied for user 'jdoe'.*`), + }, + { + Config: testAccUserConfig_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password"), + ), + ExpectError: regexp.MustCompile(`.*Access denied for user 'jdoe'.*`), + }, + { + Config: testAccUserConfig_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password2"), + ), + }, + }, + }) +} + +// TestAccUser_authConnectRetainOldPassword_WithTestcontainers tests retain_old_password +// Requires MySQL 8.0.14+ +// Uses shared container set up in TestMain +func TestAccUser_authConnectRetainOldPassword_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_basic_retain_old_password, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password"), + ), + }, + { + Config: testAccUserConfig_newPass_retain_old_password, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password"), + testAccUserAuthValid("jdoe", "password2"), + ), + }, + { + Config: testAccUserConfig_newNewPass_retain_old_password, + Check: resource.ComposeTestCheckFunc( + testAccUserAuthValid("jdoe", "password"), + ), + ExpectError: regexp.MustCompile(`.*Access denied for user 'jdoe'.*`), + }, + }, + }) +} + +// TestAccUser_deprecated_WithTestcontainers tests deprecated password attribute +// Uses shared container set up in TestMain +func TestAccUser_deprecated_WithTestcontainers(t *testing.T) { + // Use shared container set up in TestMain + _ = getSharedMySQLContainer(t, "mysql:8.0") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfig_deprecated, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "password", "password"), + ), + }, + { + Config: testAccUserConfig_deprecated_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "password", "password2"), + ), + }, + }, + }) +} diff --git a/mysql/testcontainers_helper.go b/mysql/testcontainers_helper.go new file mode 100644 index 00000000..80fcc9f4 --- /dev/null +++ b/mysql/testcontainers_helper.go @@ -0,0 +1,543 @@ +//go:build testcontainers +// +build testcontainers + +// Suppress warnings from go-m1cpu dependency +// These are harmless compiler warnings from CGO code in a third-party package + +package mysql + +import ( + "context" + "fmt" + "io" + "log" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +var ( + sharedContainer *MySQLTestContainer + sharedContainerOnce sync.Once + sharedContainerMtx sync.Mutex + + sharedTiDBCluster *TiDBTestCluster + sharedTiDBClusterOnce sync.Once + sharedTiDBClusterMtx sync.Mutex +) + +func init() { + // Suppress MySQL driver "unexpected EOF" log messages during tests + // These are harmless connection cleanup messages that occur when connections + // are closed during test cleanup + mysql.SetLogger(log.New(&mysqlLogFilter{Writer: io.Discard}, "", log.LstdFlags)) +} + +// mysqlLogFilter filters out "unexpected EOF" messages from MySQL driver logs +type mysqlLogFilter struct { + io.Writer +} + +func (f *mysqlLogFilter) Write(p []byte) (n int, err error) { + // Filter out "unexpected EOF" messages + if strings.Contains(string(p), "unexpected EOF") { + return len(p), nil // Discard the message + } + return len(p), nil // Also discard other messages to suppress all MySQL driver logging +} + +// MySQLTestContainer wraps a testcontainers MySQL container with connection details +type MySQLTestContainer struct { + Container testcontainers.Container + Endpoint string + Username string + Password string +} + +// startMySQLContainer starts a MySQL/Percona/MariaDB container for testing +// Supports MySQL, Percona, and MariaDB images +func startMySQLContainer(ctx context.Context, t *testing.T, image string) *MySQLTestContainer { + // Determine timeout based on image/version + timeout := 120 * time.Second + if contains(image, "5.6") || contains(image, "5.7") || contains(image, "6.1") || contains(image, "6.5") { + // Older versions may need more time + timeout = 180 * time.Second + } + + // Use GenericContainer for compatibility with Go 1.21 + // Configure MySQL with environment variables + req := testcontainers.ContainerRequest{ + Image: image, + ExposedPorts: []string{"3306/tcp"}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "", + "MYSQL_ALLOW_EMPTY_PASSWORD": "1", + "MYSQL_DATABASE": "testdb", + }, + WaitingFor: wait.ForLog("ready for connections"). + WithOccurrence(2). + WithStartupTimeout(timeout), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("Failed to start MySQL container (%s): %v", image, err) + } + + host, err := container.Host(ctx) + if err != nil { + t.Fatalf("Failed to get container host: %v", err) + } + + port, err := container.MappedPort(ctx, "3306") + if err != nil { + t.Fatalf("Failed to get container port: %v", err) + } + + endpoint := fmt.Sprintf("%s:%s", host, port.Port()) + + return &MySQLTestContainer{ + Container: container, + Endpoint: endpoint, + Username: "root", + Password: "", + } +} + +// SetupTestEnv sets environment variables for the test and returns a cleanup function +func (m *MySQLTestContainer) SetupTestEnv(t *testing.T) func() { + originalEndpoint := os.Getenv("MYSQL_ENDPOINT") + originalUsername := os.Getenv("MYSQL_USERNAME") + originalPassword := os.Getenv("MYSQL_PASSWORD") + + os.Setenv("MYSQL_ENDPOINT", m.Endpoint) + os.Setenv("MYSQL_USERNAME", m.Username) + os.Setenv("MYSQL_PASSWORD", m.Password) + + return func() { + // Restore original values or unset + if originalEndpoint != "" { + os.Setenv("MYSQL_ENDPOINT", originalEndpoint) + } else { + os.Unsetenv("MYSQL_ENDPOINT") + } + if originalUsername != "" { + os.Setenv("MYSQL_USERNAME", originalUsername) + } else { + os.Unsetenv("MYSQL_USERNAME") + } + if originalPassword != "" { + os.Setenv("MYSQL_PASSWORD", originalPassword) + } else { + os.Unsetenv("MYSQL_PASSWORD") + } + + // Terminate container + ctx := context.Background() + if err := m.Container.Terminate(ctx); err != nil { + t.Logf("Warning: Failed to terminate container: %v", err) + } + } +} + +// contains checks if a string contains a substring +func contains(s, substr string) bool { + return strings.Contains(s, substr) +} + +// getSharedMySQLContainer returns a shared MySQL container for all tests +// The container is created once and reused across all tests in the package +func getSharedMySQLContainer(t *testing.T, image string) *MySQLTestContainer { + sharedContainerOnce.Do(func() { + ctx := context.Background() + sharedContainer = startMySQLContainer(ctx, t, image) + + // Set up environment variables for the shared container + os.Setenv("MYSQL_ENDPOINT", sharedContainer.Endpoint) + os.Setenv("MYSQL_USERNAME", sharedContainer.Username) + os.Setenv("MYSQL_PASSWORD", sharedContainer.Password) + }) + return sharedContainer +} + +// startSharedMySQLContainer starts a shared MySQL container without requiring a testing.T +// Used by TestMain for initial setup +func startSharedMySQLContainer(image string) (*MySQLTestContainer, error) { + ctx := context.Background() + + // Determine timeout based on image/version + timeout := 120 * time.Second + if contains(image, "5.6") || contains(image, "5.7") || contains(image, "6.1") || contains(image, "6.5") { + timeout = 180 * time.Second + } + + req := testcontainers.ContainerRequest{ + Image: image, + ExposedPorts: []string{"3306/tcp"}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "", + "MYSQL_ALLOW_EMPTY_PASSWORD": "1", + "MYSQL_DATABASE": "testdb", + }, + WaitingFor: wait.ForLog("ready for connections"). + WithOccurrence(2). + WithStartupTimeout(timeout), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start MySQL container (%s): %v", image, err) + } + + host, err := container.Host(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get container host: %v", err) + } + + port, err := container.MappedPort(ctx, "3306") + if err != nil { + return nil, fmt.Errorf("failed to get container port: %v", err) + } + + endpoint := fmt.Sprintf("%s:%s", host, port.Port()) + + mysqlContainer := &MySQLTestContainer{ + Container: container, + Endpoint: endpoint, + Username: "root", + Password: "", + } + + // Install mysql_no_login plugin (required for some tests) + // This matches what the Makefile does + // Wait a bit for MySQL to be fully ready before installing plugin + time.Sleep(2 * time.Second) + if err := installMySQLNoLoginPlugin(ctx, mysqlContainer); err != nil { + // Log warning but don't fail - plugin may already be installed or not available + // Some MySQL versions/distributions may not have this plugin + fmt.Printf("Warning: Could not install mysql_no_login plugin: %v (some tests may skip)\n", err) + } + + return mysqlContainer, nil +} + +// installMySQLNoLoginPlugin installs the mysql_no_login plugin in the container +func installMySQLNoLoginPlugin(ctx context.Context, container *MySQLTestContainer) error { + // Connect to MySQL and install the plugin + db, err := connectToMySQL(ctx, &MySQLConfiguration{ + Config: &mysql.Config{ + User: container.Username, + Passwd: container.Password, + Net: "tcp", + Addr: container.Endpoint, + }, + MaxConnLifetime: 0, + MaxOpenConns: 1, + ConnectRetryTimeoutSec: 30 * time.Second, + }) + if err != nil { + return fmt.Errorf("failed to connect to install plugin: %v", err) + } + defer db.Close() + + // Try to install the plugin (ignore error if already installed or not available) + _, err = db.ExecContext(ctx, "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so'") + if err != nil { + errStr := err.Error() + // Ignore if plugin already exists or if plugin file doesn't exist (MySQL 8.0 may not have it) + if strings.Contains(errStr, "already exists") || + strings.Contains(errStr, "file not found") || + strings.Contains(errStr, "does not exist") { + return nil // Not an error - plugin already installed or not available + } + return fmt.Errorf("failed to install mysql_no_login plugin: %v", err) + } + + return nil +} + +// cleanupSharedContainer terminates the shared container +func cleanupSharedContainer() { + sharedContainerMtx.Lock() + defer sharedContainerMtx.Unlock() + + if sharedContainer != nil { + ctx := context.Background() + if err := sharedContainer.Container.Terminate(ctx); err != nil { + // Use fmt.Printf since we're in cleanup and testing.T may not be available + fmt.Printf("Warning: Failed to terminate shared container: %v\n", err) + } + sharedContainer = nil + } + + // Clean up environment variables + os.Unsetenv("MYSQL_ENDPOINT") + os.Unsetenv("MYSQL_USERNAME") + os.Unsetenv("MYSQL_PASSWORD") +} + +// TiDBTestCluster wraps TiDB cluster containers with connection details +type TiDBTestCluster struct { + PDContainer testcontainers.Container + TiKVContainer testcontainers.Container + TiDBContainer testcontainers.Container + Network testcontainers.Network + Endpoint string + Username string + Password string +} + +// startTiDBCluster starts a TiDB cluster (PD, TiKV, TiDB) for testing +// TiDB requires a multi-container setup with coordination between components +func startTiDBCluster(ctx context.Context, t *testing.T, version string) *TiDBTestCluster { + // Create a Docker network for TiDB cluster components + testNetwork, err := network.New(ctx, + network.WithCheckDuplicate(), + network.WithDriver("bridge"), + ) + if err != nil { + t.Fatalf("Failed to create Docker network: %v", err) + } + + networkName := testNetwork.Name + + // Start PD (Placement Driver) - must start first + pdContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/pd:v%s", version), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"pd"}}, + Cmd: []string{ + "--name=pd", + "--data-dir=/data", + "--client-urls=http://0.0.0.0:2379", + "--advertise-client-urls=http://pd:2379", + "--peer-urls=http://0.0.0.0:2380", + "--advertise-peer-urls=http://pd:2380", + "--initial-cluster=pd=http://pd:2380", + }, + WaitingFor: wait.ForLog("ready to serve"). + WithStartupTimeout(120 * time.Second), + }, + Started: true, + }) + if err != nil { + t.Fatalf("Failed to start PD container: %v", err) + } + + // Start TiKV (storage layer) - connects to PD + tikvContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tikv:v%s", version), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"tikv"}}, + Cmd: []string{ + "--addr=0.0.0.0:20160", + "--advertise-addr=tikv:20160", + "--status-addr=0.0.0.0:20180", + "--data-dir=/data", + "--pd=pd:2379", + }, + WaitingFor: wait.ForLog("TiKV started"). + WithStartupTimeout(120 * time.Second), + }, + Started: true, + }) + if err != nil { + t.Fatalf("Failed to start TiKV container: %v", err) + } + + // Start TiDB (SQL layer) - connects to PD, uses TiKV for storage + tidbContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tidb:v%s", version), + ExposedPorts: []string{"4000/tcp"}, + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"tidb"}}, + Cmd: []string{ + "--store=tikv", + "-P", "4000", + "--path=pd:2379", + }, + WaitingFor: wait.ForLog("server is running MySQL protocol"). + WithOccurrence(1). + WithStartupTimeout(180 * time.Second), + }, + Started: true, + }) + if err != nil { + t.Fatalf("Failed to start TiDB container: %v", err) + } + + // Get TiDB endpoint + host, err := tidbContainer.Host(ctx) + if err != nil { + t.Fatalf("Failed to get TiDB container host: %v", err) + } + + port, err := tidbContainer.MappedPort(ctx, "4000") + if err != nil { + t.Fatalf("Failed to get TiDB container port: %v", err) + } + + endpoint := fmt.Sprintf("%s:%s", host, port.Port()) + + return &TiDBTestCluster{ + PDContainer: pdContainer, + TiKVContainer: tikvContainer, + TiDBContainer: tidbContainer, + Network: testNetwork, + Endpoint: endpoint, + Username: "root", + Password: "", + } +} + +// startSharedTiDBCluster starts a shared TiDB cluster without requiring a testing.T +// Used by TestMain for initial setup +func startSharedTiDBCluster(version string) (*TiDBTestCluster, error) { + ctx := context.Background() + + // Create a Docker network for TiDB cluster components + testNetwork, err := network.New(ctx, + network.WithCheckDuplicate(), + network.WithDriver("bridge"), + ) + if err != nil { + return nil, fmt.Errorf("failed to create Docker network: %v", err) + } + + networkName := testNetwork.Name + + // Start PD (Placement Driver) - must start first + pdContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/pd:v%s", version), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"pd"}}, + Cmd: []string{ + "--name=pd", + "--data-dir=/data", + "--client-urls=http://0.0.0.0:2379", + "--advertise-client-urls=http://pd:2379", + "--peer-urls=http://0.0.0.0:2380", + "--advertise-peer-urls=http://pd:2380", + "--initial-cluster=pd=http://pd:2380", + }, + WaitingFor: wait.ForLog("ready to serve"). + WithStartupTimeout(120 * time.Second), + }, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start PD container: %v", err) + } + + // Start TiKV (storage layer) - connects to PD + tikvContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tikv:v%s", version), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"tikv"}}, + Cmd: []string{ + "--addr=0.0.0.0:20160", + "--advertise-addr=tikv:20160", + "--status-addr=0.0.0.0:20180", + "--data-dir=/data", + "--pd=pd:2379", + }, + WaitingFor: wait.ForLog("TiKV started"). + WithStartupTimeout(120 * time.Second), + }, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start TiKV container: %v", err) + } + + // Start TiDB (SQL layer) - connects to PD, uses TiKV for storage + tidbContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: fmt.Sprintf("pingcap/tidb:v%s", version), + ExposedPorts: []string{"4000/tcp"}, + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {"tidb"}}, + Cmd: []string{ + "--store=tikv", + "-P", "4000", + "--path=pd:2379", + }, + WaitingFor: wait.ForLog("server is running MySQL protocol"). + WithOccurrence(1). + WithStartupTimeout(180 * time.Second), + }, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start TiDB container: %v", err) + } + + // Get TiDB endpoint + host, err := tidbContainer.Host(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get TiDB container host: %v", err) + } + + port, err := tidbContainer.MappedPort(ctx, "4000") + if err != nil { + return nil, fmt.Errorf("failed to get TiDB container port: %v", err) + } + + endpoint := fmt.Sprintf("%s:%s", host, port.Port()) + + return &TiDBTestCluster{ + PDContainer: pdContainer, + TiKVContainer: tikvContainer, + TiDBContainer: tidbContainer, + Network: testNetwork, + Endpoint: endpoint, + Username: "root", + Password: "", + }, nil +} + +// cleanupSharedTiDBCluster terminates the shared TiDB cluster +func cleanupSharedTiDBCluster() { + sharedTiDBClusterMtx.Lock() + defer sharedTiDBClusterMtx.Unlock() + + if sharedTiDBCluster != nil { + ctx := context.Background() + if err := sharedTiDBCluster.TiDBContainer.Terminate(ctx); err != nil { + fmt.Printf("Warning: Failed to terminate TiDB container: %v\n", err) + } + if err := sharedTiDBCluster.TiKVContainer.Terminate(ctx); err != nil { + fmt.Printf("Warning: Failed to terminate TiKV container: %v\n", err) + } + if err := sharedTiDBCluster.PDContainer.Terminate(ctx); err != nil { + fmt.Printf("Warning: Failed to terminate PD container: %v\n", err) + } + if err := sharedTiDBCluster.Network.Remove(ctx); err != nil { + fmt.Printf("Warning: Failed to remove TiDB network: %v\n", err) + } + sharedTiDBCluster = nil + } + + // Clean up environment variables + os.Unsetenv("MYSQL_ENDPOINT") + os.Unsetenv("MYSQL_USERNAME") + os.Unsetenv("MYSQL_PASSWORD") +} diff --git a/mysql/testcontainers_helper_example.go b/mysql/testcontainers_helper_example.go new file mode 100644 index 00000000..3c61e2ea --- /dev/null +++ b/mysql/testcontainers_helper_example.go @@ -0,0 +1,89 @@ +//go:build spike +// +build spike + +package mysql + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/mysql" + "github.com/testcontainers/testcontainers-go/wait" +) + +// MySQLTestContainer wraps a testcontainers MySQL container with connection details +type MySQLTestContainer struct { + Container testcontainers.Container + Endpoint string + Username string + Password string + cleanup func() +} + +// startMySQLContainer starts a MySQL/Percona/MariaDB container for testing +func startMySQLContainer(ctx context.Context, t *testing.T, image string) *MySQLTestContainer { + container, err := mysql.RunContainer(ctx, + testcontainers.WithImage(image), + mysql.WithDatabase("testdb"), + mysql.WithUsername("root"), + mysql.WithPassword(""), + testcontainers.WithWaitStrategy( + wait.ForLog("ready for connections"). + WithOccurrence(2). + WithStartupTimeout(120*time.Second), + ), + ) + if err != nil { + t.Fatalf("Failed to start MySQL container: %v", err) + } + + host, err := container.Host(ctx) + if err != nil { + t.Fatalf("Failed to get container host: %v", err) + } + + port, err := container.MappedPort(ctx, "3306") + if err != nil { + t.Fatalf("Failed to get container port: %v", err) + } + + endpoint := fmt.Sprintf("%s:%s", host, port.Port()) + + return &MySQLTestContainer{ + Container: container, + Endpoint: endpoint, + Username: "root", + Password: "", + } +} + +// SetupTestEnv sets environment variables for the test and returns a cleanup function +func (m *MySQLTestContainer) SetupTestEnv(t *testing.T) func() { + os.Setenv("MYSQL_ENDPOINT", m.Endpoint) + os.Setenv("MYSQL_USERNAME", m.Username) + os.Setenv("MYSQL_PASSWORD", m.Password) + + return func() { + os.Unsetenv("MYSQL_ENDPOINT") + os.Unsetenv("MYSQL_USERNAME") + os.Unsetenv("MYSQL_PASSWORD") + ctx := context.Background() + if err := m.Container.Terminate(ctx); err != nil { + t.Logf("Warning: Failed to terminate container: %v", err) + } + } +} + +// Example usage in a test +func ExampleTestAccDatabase_WithTestcontainers(t *testing.T) { + ctx := context.Background() + container := startMySQLContainer(ctx, t, "mysql:8.0") + defer container.SetupTestEnv(t)() + + // Now tests can use MYSQL_ENDPOINT, MYSQL_USERNAME, MYSQL_PASSWORD + // Existing test logic works without changes +} diff --git a/mysql/testcontainers_testmain.go b/mysql/testcontainers_testmain.go new file mode 100644 index 00000000..98a4564c --- /dev/null +++ b/mysql/testcontainers_testmain.go @@ -0,0 +1,76 @@ +//go:build testcontainers +// +build testcontainers + +package mysql + +import ( + "fmt" + "os" + "testing" +) + +// TestMain sets up a shared MySQL/TiDB container for all testcontainers tests +// This is more efficient than starting a container for each test +func TestMain(m *testing.M) { + // Check if we're testing TiDB (requires multi-container setup) + tidbVersion := os.Getenv("TIDB_VERSION") + if tidbVersion != "" { + // Start shared TiDB cluster before running tests + var err error + sharedTiDBClusterMtx.Lock() + sharedTiDBCluster, err = startSharedTiDBCluster(tidbVersion) + sharedTiDBClusterMtx.Unlock() + + if err != nil { + // If cluster startup fails, exit with error + os.Stderr.WriteString(fmt.Sprintf("Failed to start shared TiDB cluster: %v\n", err)) + os.Exit(1) + } + + // Set up environment variables for the shared TiDB cluster + os.Setenv("MYSQL_ENDPOINT", sharedTiDBCluster.Endpoint) + os.Setenv("MYSQL_USERNAME", sharedTiDBCluster.Username) + os.Setenv("MYSQL_PASSWORD", sharedTiDBCluster.Password) + + // Run all tests + code := m.Run() + + // Cleanup shared TiDB cluster after all tests complete + cleanupSharedTiDBCluster() + + // Exit with test result code + os.Exit(code) + } + + // Default to MySQL 8.0, but allow override via DOCKER_IMAGE env var + mysqlImage := os.Getenv("DOCKER_IMAGE") + if mysqlImage == "" { + mysqlImage = "mysql:8.0" + } + + // Start shared container before running tests + var err error + sharedContainerMtx.Lock() + sharedContainer, err = startSharedMySQLContainer(mysqlImage) + sharedContainerMtx.Unlock() + + if err != nil { + // If container startup fails, exit with error + os.Stderr.WriteString(fmt.Sprintf("Failed to start shared MySQL container: %v\n", err)) + os.Exit(1) + } + + // Set up environment variables for the shared container + os.Setenv("MYSQL_ENDPOINT", sharedContainer.Endpoint) + os.Setenv("MYSQL_USERNAME", sharedContainer.Username) + os.Setenv("MYSQL_PASSWORD", sharedContainer.Password) + + // Run all tests + code := m.Run() + + // Cleanup shared container after all tests complete + cleanupSharedContainer() + + // Exit with test result code + os.Exit(code) +} diff --git a/scripts/test-runner.go b/scripts/test-runner.go new file mode 100644 index 00000000..a8be1b99 --- /dev/null +++ b/scripts/test-runner.go @@ -0,0 +1,688 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/olekukonko/tablewriter" +) + +var ( + // MySQL versions to test + mysqlVersions = []string{ + "mysql:5.6", + "mysql:5.7", + "mysql:8.0", + } + + // Percona versions to test + perconaVersions = []string{ + "percona:5.7", + "percona:8.0", + } + + // MariaDB versions to test + mariadbVersions = []string{ + "mariadb:10.3", + "mariadb:10.8", + "mariadb:10.10", + } + + // TiDB versions to test (version numbers only, not full image names) + tidbVersions = []string{ + "6.1.7", + "6.5.12", + "7.1.6", + "7.5.7", + "8.1.2", + "8.5.3", + } +) + +type testResult struct { + image string + dbType string + passed bool + logFile string + duration time.Duration + totalTests int + passedTests int + failedTests int +} + +type testJob struct { + image string + dbType string + testPattern string + testNum int +} + +type progressTracker struct { + mu sync.Mutex + trackers map[string]*versionProgress + lastUpdate time.Time +} + +type versionProgress struct { + dbType string + image string + totalTests int + passedTests int + failedTests int + skippedTests int + failedOutput []string // Store failure output lines + failedTestSet map[string]bool // Track which tests have failed + lastUpdate time.Time +} + +var ( + outputMutex sync.Mutex + progress = &progressTracker{ + trackers: make(map[string]*versionProgress), + } + totalJobsCount int + isParallel bool +) + +func main() { + // Get test pattern from command line args, default to "WithTestcontainers" + testPattern := "WithTestcontainers" + if len(os.Args) > 1 { + testPattern = os.Args[1] + } + + // Get parallelism from environment variable + parallel := getParallelism() + + fmt.Printf("Testcontainers Matrix Test Suite\n") + fmt.Printf("Test pattern: %s | Parallelism: %d\n\n", testPattern, parallel) + + // Build all test jobs + var jobs []testJob + testNum := 0 + + // MySQL tests + for _, version := range mysqlVersions { + testNum++ + jobs = append(jobs, testJob{ + image: version, + dbType: "MySQL", + testPattern: testPattern, + testNum: testNum, + }) + } + + // Percona tests + for _, version := range perconaVersions { + testNum++ + jobs = append(jobs, testJob{ + image: version, + dbType: "Percona", + testPattern: testPattern, + testNum: testNum, + }) + } + + // MariaDB tests + for _, version := range mariadbVersions { + testNum++ + jobs = append(jobs, testJob{ + image: version, + dbType: "MariaDB", + testPattern: testPattern, + testNum: testNum, + }) + } + + // TiDB tests + for _, version := range tidbVersions { + testNum++ + jobs = append(jobs, testJob{ + image: version, + dbType: "TiDB", + testPattern: testPattern, + testNum: testNum, + }) + } + + totalJobsCount = len(jobs) + + // Run tests (sequentially or in parallel) + var results []testResult + if parallel > 1 { + isParallel = true + results = runTestsParallel(jobs, parallel) + } else { + isParallel = false + results = runTestsSequential(jobs) + } + + // Print summary + printSummary(results) + + // Exit with error code if any tests failed + for _, result := range results { + if !result.passed { + os.Exit(1) + } + } +} + +func getParallelism() int { + parallelStr := os.Getenv("PARALLEL") + if parallelStr == "" { + return 1 // Default to sequential + } + + parallel, err := strconv.Atoi(parallelStr) + if err != nil || parallel < 1 { + fmt.Fprintf(os.Stderr, "Warning: Invalid PARALLEL value '%s', using 1 (sequential)\n", parallelStr) + return 1 + } + + // Cap parallelism at number of CPUs + 2 to avoid overwhelming the system + maxParallel := runtime.NumCPU() + 2 + if parallel > maxParallel { + fmt.Fprintf(os.Stderr, "Warning: PARALLEL=%d exceeds recommended max (%d), capping at %d\n", parallel, maxParallel, maxParallel) + return maxParallel + } + + return parallel +} + +func runTestsSequential(jobs []testJob) []testResult { + var results []testResult + + for _, job := range jobs { + result := runTest(job) + results = append(results, result) + } + + return results +} + +func runTestsParallel(jobs []testJob, parallel int) []testResult { + // Create job channel + jobChan := make(chan testJob, len(jobs)) + resultChan := make(chan testResult, len(jobs)) + + // Send all jobs to channel + for _, job := range jobs { + jobChan <- job + } + close(jobChan) + + // Start worker goroutines + var wg sync.WaitGroup + for i := 0; i < parallel; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobChan { + result := runTest(job) + resultChan <- result + } + }() + } + + // Wait for all workers to finish + go func() { + wg.Wait() + close(resultChan) + }() + + // Collect results as they come in + var results []testResult + for result := range resultChan { + results = append(results, result) + } + + return results +} + +func runTest(job testJob) testResult { + key := fmt.Sprintf("%s-%s", job.dbType, job.image) + + // Initialize progress tracker + progress.mu.Lock() + progress.trackers[key] = &versionProgress{ + dbType: job.dbType, + image: job.image, + totalTests: 0, + passedTests: 0, + failedTests: 0, + skippedTests: 0, + failedOutput: []string{}, + failedTestSet: make(map[string]bool), + lastUpdate: time.Now(), + } + progress.mu.Unlock() + + // Synchronize output to prevent interleaving + outputMutex.Lock() + if !isParallel { + // In sequential mode, show counter and start progress bar + fmt.Printf("\n[%d/%d] ", job.testNum, getTotalJobs()) + progress.mu.Lock() + tracker := progress.trackers[key] + progress.mu.Unlock() + renderProgress(tracker) + } + outputMutex.Unlock() + + // Sanitize image name for log file + logFile := fmt.Sprintf("/tmp/testcontainers-%s-%s.log", job.dbType, sanitizeImageName(job.image)) + + start := time.Now() + + // Build the go test command with JSON output + cmd := exec.Command("go", "test", + "-tags=testcontainers", + "-json", + "./mysql/...", + "-run", job.testPattern, + "-timeout", "15m", + ) + + // Set environment variables + envVars := os.Environ() + if job.dbType == "TiDB" { + // TiDB uses version number, not full image name + envVars = append(envVars, "TIDB_VERSION="+job.image) + } else { + envVars = append(envVars, "DOCKER_IMAGE="+job.image) + } + envVars = append(envVars, "TF_ACC=1", "GOTOOLCHAIN=auto") + cmd.Env = envVars + + // Create log file + logFileHandle, err := os.Create(logFile) + if err != nil { + outputMutex.Lock() + fmt.Fprintf(os.Stderr, "Error creating log file %s: %v\n", logFile, err) + outputMutex.Unlock() + return testResult{ + image: job.image, + dbType: job.dbType, + passed: false, + logFile: logFile, + } + } + defer logFileHandle.Close() + + // Create a pipe to capture output in real-time + pipeReader, pipeWriter, err := os.Pipe() + if err != nil { + outputMutex.Lock() + fmt.Fprintf(os.Stderr, "Error creating pipe: %v\n", err) + outputMutex.Unlock() + return testResult{ + image: job.image, + dbType: job.dbType, + passed: false, + logFile: logFile, + } + } + + // Set command output to pipe + cmd.Stdout = pipeWriter + cmd.Stderr = pipeWriter + + // Start parsing output in a goroutine + done := make(chan bool) + var lineBuffer strings.Builder + go func() { + defer pipeReader.Close() + buf := make([]byte, 4096) + for { + n, readErr := pipeReader.Read(buf) + if n > 0 { + chunk := string(buf[:n]) + // Write to log file + logFileHandle.WriteString(chunk) + + // Accumulate lines for JSON parsing + lineBuffer.WriteString(chunk) + lines := strings.Split(lineBuffer.String(), "\n") + // Keep the last incomplete line in buffer + lineBuffer.Reset() + if len(lines) > 1 { + lineBuffer.WriteString(lines[len(lines)-1]) + // Process complete lines + for i := 0; i < len(lines)-1; i++ { + parseTestOutput(lines[i], key) + } + } + } + if readErr != nil { + // Process any remaining line in buffer + if lineBuffer.Len() > 0 { + parseTestOutput(lineBuffer.String(), key) + } + break + } + } + done <- true + }() + + // Run the command + err = cmd.Run() + + // Close writer to signal EOF to reader + pipeWriter.Close() + + // Wait for reader goroutine to finish + <-done + duration := time.Since(start) + + // Get final progress counts and failure output + progress.mu.Lock() + tracker := progress.trackers[key] + var totalTests, passedTests, failedTests int + var failedOutput []string + if tracker != nil { + totalTests = tracker.totalTests + passedTests = tracker.passedTests + failedTests = tracker.failedTests + failedOutput = tracker.failedOutput + + // Render final progress state + if tracker.totalTests > 0 { + outputMutex.Lock() + if !isParallel { + // Clear the in-progress line + fmt.Fprintf(os.Stderr, "\r\033[K") + } + renderProgress(tracker) + fmt.Fprintf(os.Stderr, "\n") + outputMutex.Unlock() + } + } + progress.mu.Unlock() + + outputMutex.Lock() + passed := err == nil + + // Print failure output if there were failures + if failedTests > 0 && len(failedOutput) > 0 { + fmt.Println() + for _, line := range failedOutput { + fmt.Print(line) + } + fmt.Println() + } + + outputMutex.Unlock() + + return testResult{ + image: job.image, + dbType: job.dbType, + passed: passed, + logFile: logFile, + duration: duration, + totalTests: totalTests, + passedTests: passedTests, + failedTests: failedTests, + // Note: skippedTests is tracked but not stored in testResult struct + // It's displayed in the progress bar + } +} + +func getTotalJobs() int { + return totalJobsCount +} + +type testEvent struct { + Time time.Time `json:"Time"` + Action string `json:"Action"` + Package string `json:"Package"` + Test string `json:"Test"` + Elapsed float64 `json:"Elapsed"` + Output string `json:"Output"` +} + +func parseTestOutput(line string, key string) { + line = strings.TrimSpace(line) + if line == "" { + return + } + + var event testEvent + if err := json.Unmarshal([]byte(line), &event); err != nil { + // Not valid JSON, skip + return + } + + progress.mu.Lock() + tracker := progress.trackers[key] + if tracker == nil { + progress.mu.Unlock() + return + } + + // Handle test-level events + if event.Test != "" { + switch event.Action { + case "run": + // Test started - increment total only once per test + // Note: We count "run" events, but the actual completion is tracked via "pass"/"fail"/"skip" + tracker.totalTests++ + case "pass": + // Test passed - increment passed count + tracker.passedTests++ + case "fail": + // Test failed - increment failed count and mark test as failed + tracker.failedTests++ + if event.Test != "" { + tracker.failedTestSet[event.Test] = true + } + case "skip": + // Test skipped - increment skipped count + tracker.skippedTests++ + } + } + + // Capture output from tests (especially failures) + // The JSON format includes output events with the Test field set + if event.Action == "output" && event.Output != "" && event.Test != "" { + output := event.Output + // Capture output from tests that have failed or look like failures + // Output events come before the "fail" action, so we capture based on content + // We'll also capture all output from tests that eventually fail + isFailureOutput := strings.Contains(output, "FAIL:") || + strings.Contains(output, "--- FAIL:") || + strings.Contains(output, "Error:") || + strings.Contains(output, "panic:") || + (strings.Contains(output, "got:") && strings.Contains(output, "want:")) + + // Also capture if this test has already been marked as failed + if isFailureOutput || tracker.failedTestSet[event.Test] { + tracker.failedOutput = append(tracker.failedOutput, output) + } + } + + tracker.lastUpdate = time.Now() + progress.mu.Unlock() + + // Update progress display when we have tests running + if tracker.totalTests > 0 && (tracker.passedTests > 0 || tracker.failedTests > 0) { + outputMutex.Lock() + if !isParallel { + // In sequential mode, update progress on same line + renderProgress(tracker) + } + // In parallel mode, don't update in real-time to avoid screen chaos + outputMutex.Unlock() + } +} + +func renderProgress(tracker *versionProgress) { + if tracker == nil { + // Show empty bar initially + bar := strings.Repeat("-", 30) + fmt.Fprintf(os.Stderr, "\r\033[K[%s] 0/0", bar) + return + } + + version := extractVersion(tracker.image) + if tracker.totalTests == 0 { + // Show empty bar initially + bar := strings.Repeat("-", 30) + fmt.Fprintf(os.Stderr, "\r\033[K%-8s %-6s [%s] 0/0", tracker.dbType, version, bar) + return + } + + // Calculate percent based on completed tests (passed + failed), not including skipped + completedTests := tracker.passedTests + tracker.failedTests + var percent float64 + if tracker.totalTests > 0 { + percent = float64(completedTests) / float64(tracker.totalTests) + } + barWidth := 30 + filled := int(percent * float64(barWidth)) + bar := strings.Repeat("=", filled) + strings.Repeat("-", barWidth-filled) + + // Show passed/total, and include skipped/failed info if present + // Pad database type to 8 chars and version to 6 chars for alignment + status := fmt.Sprintf("%-8s %-6s [%s] %d/%d", tracker.dbType, version, bar, tracker.passedTests, tracker.totalTests) + if tracker.failedTests > 0 { + status = fmt.Sprintf("%-8s %-6s [%s] %d passed, %d failed", tracker.dbType, version, bar, tracker.passedTests, tracker.failedTests) + } else if tracker.skippedTests > 0 { + // If all non-skipped tests passed, show skipped count + status = fmt.Sprintf("%-8s %-6s [%s] %d passed, %d skipped", tracker.dbType, version, bar, tracker.passedTests, tracker.skippedTests) + } + fmt.Fprintf(os.Stderr, "\r\033[K%s", status) +} + +func printProgressBar(dbType, image string, total, passed, failed int, final bool) { + version := extractVersion(image) + + if final { + // Final: print summary + if failed == 0 { + fmt.Printf(" %s %s %d/%d passed\n", dbType, version, passed, total) + } else { + fmt.Printf(" %s %s %d passed, %d failed\n", dbType, version, passed, failed) + } + } else { + // For in-progress, we use the progress bar library which handles updates + // This is called from updateProgressBar which manages the actual bar + // Just print a simple status line + if isParallel { + fmt.Printf(" %s %s %d passed", dbType, version, passed) + } else { + fmt.Printf("\r\033[K %s %s %d passed", dbType, version, passed) + } + if failed > 0 { + fmt.Printf(", %d failed", failed) + } + } +} + +func sanitizeImageName(image string) string { + // Replace colons and slashes with underscores + result := strings.ReplaceAll(image, ":", "_") + result = strings.ReplaceAll(result, "/", "_") + return result +} + +func printSummary(results []testResult) { + fmt.Println() + + total := len(results) + passed := 0 + failed := 0 + + // Sort results by database type and version for better readability + sortedResults := make([]testResult, len(results)) + copy(sortedResults, results) + sort.Slice(sortedResults, func(i, j int) bool { + if sortedResults[i].dbType != sortedResults[j].dbType { + return sortedResults[i].dbType < sortedResults[j].dbType + } + return sortedResults[i].image < sortedResults[j].image + }) + + // Create table + table := tablewriter.NewWriter(os.Stdout) + table.Options( + tablewriter.WithHeader([]string{"Database", "Version", "Status", "Duration"}), + ) + + // Add rows + for _, result := range sortedResults { + status := "PASS" + if !result.passed { + status = "FAIL" + failed++ + } else { + passed++ + } + + // Extract version from image (e.g., "mysql:8.0" -> "8.0") + version := extractVersion(result.image) + duration := formatDuration(result.duration) + + // Add test counts to status + if result.totalTests > 0 { + if result.failedTests > 0 { + status = fmt.Sprintf("%s (%d/%d, %d failed)", status, result.passedTests, result.totalTests, result.failedTests) + } else { + status = fmt.Sprintf("%s (%d/%d)", status, result.passedTests, result.totalTests) + } + } + + row := []string{ + result.dbType, + version, + status, + duration, + } + table.Append(row) + } + + table.Render() + + fmt.Printf("\nSummary: %d/%d passed", passed, total) + if failed > 0 { + fmt.Printf(", %d failed", failed) + } + fmt.Println() + + if failed > 0 { + fmt.Println("Logs available in /tmp/testcontainers-*.log") + } +} + +func extractVersion(image string) string { + // For TiDB, the image is already just the version number + // For MySQL/Percona/MariaDB, extract version from image string (e.g., "mysql:8.0" -> "8.0") + parts := strings.Split(image, ":") + if len(parts) > 1 { + return parts[1] + } + return image +} + +func formatDuration(d time.Duration) string { + if d == 0 { + return "-" + } + if d < time.Second { + return fmt.Sprintf("%.0fms", float64(d.Nanoseconds())/1e6) + } + if d < time.Minute { + return fmt.Sprintf("%.1fs", d.Seconds()) + } + minutes := int(d.Minutes()) + seconds := int(d.Seconds()) % 60 + return fmt.Sprintf("%dm%ds", minutes, seconds) +} diff --git a/scripts/tidb-test-cluster.sh b/scripts/tidb-test-cluster.sh index 7aa50234..f6b1e30b 100755 --- a/scripts/tidb-test-cluster.sh +++ b/scripts/tidb-test-cluster.sh @@ -170,6 +170,39 @@ function run_tidb() { fi } +function wait_for_tidb() { + local _mysql_port=$1 + local _version=$2 + echo "==> Waiting for TiDB to be ready..." + + # Determine timeout based on TiDB version + # Versions 6.1.x and 6.5.x need longer startup time + if [[ "${_version}" == 6.1.* ]] || [[ "${_version}" == 6.5.* ]]; then + TIMEOUT=240 # 4 minutes for older versions + echo "Using extended timeout (240s) for TiDB ${_version}" + else + TIMEOUT=120 # 2 minutes for newer versions + fi + + # Wait for TiDB to be ready + for i in $(seq 1 ${TIMEOUT}); do + if mysql -h 127.0.0.1 -P ${_mysql_port} -u root -e 'SELECT 1' >/dev/null 2>&1; then + echo "TiDB is ready!" + return 0 + fi + sleep 1 + if [ $((i % 10)) -eq 0 ]; then + printf "." + fi + done + + echo "" + echo "ERROR: TiDB failed to start within ${TIMEOUT} seconds" + echo "Checking container logs..." + docker logs tidb 2>&1 | tail -20 || true + return 1 +} + function main() { parse_params "$@" if [[ "$SCRIPT_INIT" = "true" ]]; then @@ -178,7 +211,8 @@ function main() { ${DOCKER} network create ${DOCKER_NETWORK} && \ run_pd && \ run_tikv && \ - run_tidb $MYSQL_PORT + run_tidb $MYSQL_PORT && \ + wait_for_tidb $MYSQL_PORT $MYSQL_VERSION else script_usage