pax_global_header00006660000000000000000000000064150755335300014520gustar00rootroot0000000000000052 comment=97f6539e11575977097054f0769d021ac605fc91 wait4x-wait4x-7fb9e45/000077500000000000000000000000001507553353000146055ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/.editorconfig000066400000000000000000000005371507553353000172670ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = false max_line_length = 120 tab_width = 2 [{*.go,*.go2}] indent_size = 4 indent_style = tab tab_width = 4 [{*.markdown,*.md}] indent_size = 4 tab_width = 4 [{*.mk,GNUmakefile,GNUmakefile.inc,Makefile,makefile,makefile.inc}] tab_width = 4 wait4x-wait4x-7fb9e45/.gitattributes000066400000000000000000000000451507553353000174770ustar00rootroot00000000000000internal/cmd/version.go export-subst wait4x-wait4x-7fb9e45/.github/000077500000000000000000000000001507553353000161455ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/.github/dependabot.yml000066400000000000000000000014711507553353000210000ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: docker directory: / reviewers: - "atkrad" assignees: - "atkrad" schedule: interval: "daily" groups: docker: patterns: - "*" - package-ecosystem: "gomod" open-pull-requests-limit: 10 directory: "/" schedule: interval: "daily" reviewers: - "atkrad" assignees: - "atkrad" labels: - "dependencies" groups: testcontainers: patterns: - github.com/testcontainers/* - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" reviewers: - "atkrad" assignees: - "atkrad" labels: - "actions" - "dependencies" groups: github-actions: patterns: - "*" wait4x-wait4x-7fb9e45/.github/release.yml000066400000000000000000000007561507553353000203200ustar00rootroot00000000000000changelog: categories: - title: Exciting New Features 🎉 labels: - feature - title: Enhancements 🚀 labels: - enhancement - title: Bug Fixes 🐛 labels: - bug - title: Deprecations ❌ labels: - deprecation - title: Dependency Updates ⬆️ labels: - dependencies - title: Documentation improvements labels: - documentation - title: Other Changes labels: - "*"wait4x-wait4x-7fb9e45/.github/workflows/000077500000000000000000000000001507553353000202025ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/.github/workflows/ci.yaml000066400000000000000000000127201507553353000214630ustar00rootroot00000000000000name: Wait4X CI on: push: branches: - 'main' - 'release/*' tags: - 'v*' pull_request: branches: - '*' permissions: contents: read packages: write jobs: check: name: Check runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v5 - name: Set up Go 1.23.x uses: actions/setup-go@v5 with: go-version: 1.23.x - name: go-fmt run: make check-gofmt - name: go-vet run: make check-govet - name: revive run: | go install github.com/mgechev/revive@v1.1.4 make check-revive test: name: Test needs: check runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v5 - name: Set up Go 1.23.x uses: actions/setup-go@v5 with: go-version: 1.23.x - name: Test Wait4X run: make test - name: Convert coverage to lcov uses: jandelgado/gcov2lcov-action@v1.2.0 with: infile: coverage.out outfile: coverage.lcov - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov build: name: Build needs: test runs-on: ubuntu-latest permissions: contents: write packages: write statuses: write steps: - name: Checkout Code uses: actions/checkout@v5 with: fetch-depth: 0 - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: # Maintaining "atkrad/wait4x" for backward compatibility # Note: This will be removed in v4.0.0. Please use the "wait4x/wait4x" image going forward. images: | atkrad/wait4x wait4x/wait4x ghcr.io/${{ github.repository }} ### versioning strategy ### push semver tag v3.2.1 on the default branch # wait4x/wait4x:2.2.0 # wait4x/wait4x:2.2 # wait4x/wait4x:2 # wait4x/wait4x:latest # ghcr.io/wait4x/wait4x:2.2.0 # ghcr.io/wait4x/wait4x:2.2 # ghcr.io/wait4x/wait4x:2 # ghcr.io/wait4x/wait4x:latest ### push semver pre-release tag v3.0.0-beta.1 on the default branch # wait4x/wait4x:3.0.0-beta.1 # ghcr.io/wait4x/wait4x:3.0.0-beta.1 ### push on the default branch # wait4x/wait4x:edge # ghcr.io/wait4x/wait4x:edge tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=ref,event=pr type=edge,branch=${{ github.event.repository.default_branch }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: buildkitd-config-inline: | [worker.oci] max-parallelism = 10 - name: Login to Docker Hub (docker.io) if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry (ghcr.io) if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build artifacts uses: docker/bake-action@v6 with: targets: artifact provenance: false set: | *.args.COMMIT_HASH=${{ github.sha }} *.args.COMMIT_REF_SLUG=${{ github.ref_name }} - name: Move artifacts run: | # Move all files except SHA256SUMS to the main dist directory find ./dist -type f -not -name "SHA256SUMS" -exec mv {} ./dist/ \; # Combine all SHA256SUMS files into one find ./dist -name "SHA256SUMS" -exec cat {} \; | sort -u > ./dist/SHA256SUMS.combined mv ./dist/SHA256SUMS.combined ./dist/SHA256SUMS # Remove empty directories find ./dist -type d -empty -delete - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: wait4x-artifacts path: ./dist/* if-no-files-found: error - name: Build images uses: docker/bake-action@v6 with: targets: image push: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }} sbom: true provenance: true files: | ./docker-bake.hcl cwd://${{ steps.meta.outputs.bake-file }} - name: Docker Hub Description uses: peter-evans/dockerhub-description@v4 if: ${{ github.event_name == 'push' && github.ref_name == github.event.repository.default_branch }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} short-description: ${{ github.event.repository.description }} - name: GitHub Release uses: softprops/action-gh-release@v2 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: true generate_release_notes: true files: | dist/*.tar.gz dist/*.sha256sum dist/SHA256SUMS env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} wait4x-wait4x-7fb9e45/.gitignore000066400000000000000000000000711507553353000165730ustar00rootroot00000000000000bin dist vendor coverage.out # Nix build results result wait4x-wait4x-7fb9e45/.mailmap000066400000000000000000000003511507553353000162250ustar00rootroot00000000000000Mohammad Abdolirad Mohammad Abdolirad Morteza NourelahiAlamdari Morteza NourelahiAlamdari wait4x-wait4x-7fb9e45/.revive.toml000066400000000000000000000011161507553353000170570ustar00rootroot00000000000000ignoreGeneratedHeader = false severity = "warning" confidence = 0.8 errorCode = 1 warningCode = 1 [rule.blank-imports] [rule.context-as-argument] [rule.dot-imports] [rule.error-naming] [rule.error-return] [rule.error-strings] [rule.exported] [rule.if-return] [rule.increment-decrement] [rule.var-naming] [rule.var-declaration] [rule.package-comments] [rule.range] [rule.receiver-naming] [rule.time-naming] [rule.unexported-return] [rule.indent-error-flow] [rule.errorf] [rule.empty-block] [rule.superfluous-else] [rule.unused-parameter] [rule.unreachable-code] [rule.redefines-builtin-id] wait4x-wait4x-7fb9e45/CODE_OF_CONDUCT.md000066400000000000000000000121441507553353000174060ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at conduct@wait4x.dev. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. wait4x-wait4x-7fb9e45/Dockerfile000066400000000000000000000032321507553353000165770ustar00rootroot00000000000000# syntax=docker/dockerfile:1.5.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx FROM --platform=$BUILDPLATFORM golang:1.24-alpine3.22 AS base ENV GO111MODULE=auto ENV CGO_ENABLED=0 COPY --from=xx / / RUN apk add --update --no-cache build-base coreutils git WORKDIR /src FROM base AS build ARG TARGETPLATFORM ARG TARGETOS ARG COMMIT_HASH ARG COMMIT_REF_SLUG RUN --mount=type=bind,target=/src,rw \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GO_BINARY=xx-go WAIT4X_BUILD_OUTPUT=/usr/bin WAIT4X_COMMIT_HASH=${COMMIT_HASH} WAIT4X_COMMIT_REF_SLUG=${COMMIT_REF_SLUG} make build \ && xx-verify --static /usr/bin/wait4x* FROM scratch AS binary COPY --from=build /usr/bin/wait4x* / FROM base AS releaser ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT WORKDIR /work RUN --mount=from=binary,target=/build \ --mount=type=bind,target=/src \ mkdir -p /out \ && cp /build/wait4x* /src/README.md /src/LICENSE . \ && tar -czvf "/out/wait4x-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.tar.gz" * \ # Change dir to "/out" to prevent adding "/out" in the sha256sum command output. && cd /out \ # Note: This will be removed in v4.0.0. Please use the SHA256SUMS file going forward. && sha256sum "wait4x-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.tar.gz" > "wait4x-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.tar.gz.sha256sum" \ && sha256sum "wait4x-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.tar.gz" >> "SHA256SUMS" FROM scratch AS artifact COPY --from=releaser /out / FROM alpine:3.22 RUN apk add --update --no-cache ca-certificates tzdata COPY --from=binary /wait4x /usr/bin/wait4x ENTRYPOINT ["wait4x"] CMD ["help"] wait4x-wait4x-7fb9e45/LICENSE000066400000000000000000000261351507553353000156210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. wait4x-wait4x-7fb9e45/Makefile000066400000000000000000000305531507553353000162530ustar00rootroot00000000000000# Copyright 2019-2025 The Wait4X Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================= # Configuration # ============================================================================= # Go configuration GO_BINARY ?= $(shell which go) GO_ENVIRONMENTS ?= GO_VERSION ?= $(shell $(GO_BINARY) version | cut -d' ' -f3 | sed 's/go//') # Wait4X configuration WAIT4X_BINARY_NAME ?= wait4x WAIT4X_MODULE_NAME ?= wait4x.dev/v3 WAIT4X_MAIN_PATH ?= cmd/wait4x/main.go # Test configuration WAIT4X_COVERAGE_IGNORE_PACKAGES ?= ${WAIT4X_MODULE_NAME}/examples ${WAIT4X_MODULE_NAME}/cmd # Build configuration WAIT4X_BUILD_OUTPUT ?= ${CURDIR}/dist WAIT4X_BUILD_OS ?= $(shell go env GOOS) WAIT4X_BUILD_ARCH ?= $(shell go env GOARCH) WAIT4X_BUILD_CGO ?= 0 # Version information WAIT4X_COMMIT_REF_SLUG ?= $(shell [ -d ./.git ] && (git symbolic-ref -q --short HEAD || git describe --tags --always 2>/dev/null || echo "unknown")) WAIT4X_COMMIT_HASH ?= $(shell [ -d ./.git ] && git rev-parse HEAD 2>/dev/null || echo "unknown") WAIT4X_BUILD_TIME ?= $(shell date -u '+%FT%TZ') # Change output to .exe for windows ifeq (windows,$(filter windows,$(WAIT4X_BUILD_OS) $(TARGETOS))) WAIT4X_BINARY_NAME := ${WAIT4X_BINARY_NAME}.exe endif # ============================================================================= # Build flags and LDFLAGS # ============================================================================= # Base LDFLAGS for reproducible builds and metadata WAIT4X_BUILD_LDFLAGS ?= -buildid= -w \ -X $(WAIT4X_MODULE_NAME)/internal/cmd.BuildTime=$(WAIT4X_BUILD_TIME) # Add version information if available ifneq ($(WAIT4X_COMMIT_REF_SLUG),) WAIT4X_BUILD_LDFLAGS += -X $(WAIT4X_MODULE_NAME)/internal/cmd.AppVersion=$(WAIT4X_COMMIT_REF_SLUG) endif ifneq ($(WAIT4X_COMMIT_HASH),) WAIT4X_BUILD_LDFLAGS += -X $(WAIT4X_MODULE_NAME)/internal/cmd.GitCommit=$(WAIT4X_COMMIT_HASH) endif # Build flags for reproducible builds WAIT4X_BUILD_FLAGS ?= -trimpath -ldflags="$(WAIT4X_BUILD_LDFLAGS)" WAIT4X_RUN_FLAGS ?= -ldflags="$(WAIT4X_BUILD_LDFLAGS)" # Runtime flags WAIT4X_FLAGS ?= # ============================================================================= # Utility functions # ============================================================================= # Check if a command exists check_cmd = $(shell command -v $(1) 2> /dev/null) # Filter coverage output to exclude ignored packages filter_coverage = @grep $(foreach pkg,$(WAIT4X_COVERAGE_IGNORE_PACKAGES),-v -e "$(pkg)") coverage.out.tmp > coverage.out # Common build function define build_binary @mkdir -p $(WAIT4X_BUILD_OUTPUT) CGO_ENABLED=$(WAIT4X_BUILD_CGO) GOOS=$(WAIT4X_BUILD_OS) GOARCH=$(WAIT4X_BUILD_ARCH) \ $(GO_ENVIRONMENTS) $(GO_BINARY) build -v $(1) \ -o $(WAIT4X_BUILD_OUTPUT)/$(2) $(WAIT4X_MAIN_PATH) endef # ============================================================================= # Targets # ============================================================================= .PHONY: help help: ## Show this help message @echo " __ __ .__ __ _________ ___" @echo "/ \ / \_____ |__|/ |_ / | \ \/ /" @echo "\ \/\/ /\__ \ | \ __\/ | |\ / " @echo " \ / / __ \| || | / ^ / \ " @echo " \__/\ / (____ /__||__| \____ /___/\ \\" @echo " \/ \/ |__| \_/" @echo "" @echo "Available targets:" @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @echo "" @echo "Environment variables:" @echo " GO_BINARY Go binary to use (default: $(GO_BINARY))" @echo " GO_ENVIRONMENTS Additional environment variables for Go (default: $(if $(GO_ENVIRONMENTS),$(GO_ENVIRONMENTS),none))" @echo " WAIT4X_BUILD_OUTPUT Build output directory (default: ${WAIT4X_BUILD_OUTPUT})" @echo " WAIT4X_BUILD_OS Target OS for cross-compilation (default: $(WAIT4X_BUILD_OS))" @echo " WAIT4X_BUILD_ARCH Target architecture for cross-compilation (default: $(WAIT4X_BUILD_ARCH))" @echo " WAIT4X_FLAGS Additional flags for wait4x binary (default: $(if $(WAIT4X_FLAGS),$(WAIT4X_FLAGS),none))" @echo " WAIT4X_COVERAGE_IGNORE_PACKAGES Space-separated list of packages to exclude from coverage (default: ${WAIT4X_COVERAGE_IGNORE_PACKAGES})" .PHONY: version version: ## Show version information @echo "Go version: $(GO_VERSION)" @echo "Build OS: $(WAIT4X_BUILD_OS)" @echo "Build ARCH: $(WAIT4X_BUILD_ARCH)" @echo "Commit ref: $(WAIT4X_COMMIT_REF_SLUG)" @echo "Commit hash: $(WAIT4X_COMMIT_HASH)" @echo "Build time: $(WAIT4X_BUILD_TIME)" @echo "Coverage ignore packages: $(WAIT4X_COVERAGE_IGNORE_PACKAGES)" .PHONY: show-coverage-config show-coverage-config: ## Show current coverage configuration @echo "Coverage ignore packages: $(WAIT4X_COVERAGE_IGNORE_PACKAGES)" @echo "To modify, set WAIT4X_COVERAGE_IGNORE_PACKAGES environment variable" @echo "Example: WAIT4X_COVERAGE_IGNORE_PACKAGES='pkg1 pkg2' make test" .PHONY: clean clean: ## Clean build artifacts @echo "Cleaning build artifacts..." @rm -rf $(WAIT4X_BUILD_OUTPUT) @go clean -cache -testcache -modcache @echo "Clean complete!" .PHONY: deps deps: ## Download and tidy dependencies @echo "Downloading dependencies..." $(GO_ENVIRONMENTS) $(GO_BINARY) mod download $(GO_ENVIRONMENTS) $(GO_BINARY) mod tidy @echo "Dependencies ready!" .PHONY: check-deps check-deps: ## Check for outdated dependencies @echo "Checking for outdated dependencies..." @$(GO_ENVIRONMENTS) $(GO_BINARY) list -u -m all | grep -v "go:" || echo "All dependencies are up to date" .PHONY: test test: ## Run tests with coverage @echo "Running tests..." $(GO_ENVIRONMENTS) $(GO_BINARY) test -v -race -covermode=atomic -coverprofile=coverage.out.tmp ./... @$(filter_coverage) > coverage.out @rm coverage.out.tmp @echo "Test coverage report generated: coverage.out" .PHONY: test-short test-short: ## Run tests without race detection and integration tests @echo "Running tests (short mode)..." $(GO_ENVIRONMENTS) $(GO_BINARY) test -v -short -covermode=atomic -coverprofile=coverage.out.tmp ./... @$(filter_coverage) > coverage.out @rm coverage.out.tmp .PHONY: test-coverage test-coverage: ## Run tests and show coverage report @echo "Running tests with coverage..." $(GO_ENVIRONMENTS) $(GO_BINARY) test -v -race -covermode=atomic -coverprofile=coverage.out.tmp ./... @$(filter_coverage) > coverage.out @rm coverage.out.tmp @echo "Coverage report:" $(GO_ENVIRONMENTS) $(GO_BINARY) tool cover -func=coverage.out @echo "" @echo "HTML coverage report:" $(GO_ENVIRONMENTS) $(GO_BINARY) tool cover -html=coverage.out -o coverage.html @echo "Coverage report saved to coverage.html" .PHONY: lint lint: ## Run all linting checks @echo "Running linting checks..." @$(MAKE) check-gofmt @$(MAKE) check-revive @$(MAKE) check-govet @echo "All linting checks passed!" .PHONY: check-gofmt check-gofmt: ## Check Go code formatting @echo "Checking Go code formatting..." @if [ -n "$(shell gofmt -s -l .)" ]; then \ echo "❌ Go code is not formatted. Run 'make fmt' to fix."; \ exit 1; \ fi @echo "✅ Go code formatting is correct" .PHONY: fmt fmt: ## Format Go code @echo "Formatting Go code..." $(GO_ENVIRONMENTS) $(GO_BINARY) fmt ./... gofmt -s -w . @echo "✅ Go code formatted" .PHONY: check-revive check-revive: ## Run revive linter @echo "Running revive linter..." @if [ -z "$(call check_cmd,revive)" ]; then \ echo "❌ revive not found. Install with: go install github.com/mgechev/revive@latest (or run 'nix develop' to install)"; \ exit 1; \ fi revive -config .revive.toml -formatter friendly ./... .PHONY: check-govet check-govet: ## Run go vet @echo "Running go vet..." $(GO_ENVIRONMENTS) $(GO_BINARY) vet ./... .PHONY: security security: ## Run security checks @echo "Running security checks..." @if [ -n "$(call check_cmd,gosec)" ]; then \ gosec ./...; \ else \ echo "⚠️ gosec not found. Install with: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest (or run 'nix develop' to install)"; \ fi .PHONY: build build: ## Build Wait4X binary @echo "Building Wait4X..." $(call build_binary,$(WAIT4X_BUILD_FLAGS),$(WAIT4X_BINARY_NAME)) @echo "✅ Binary built: $(WAIT4X_BUILD_OUTPUT)/$(WAIT4X_BINARY_NAME)" .PHONY: build-debug build-debug: ## Build Wait4X binary with debug information @echo "Building Wait4X (debug mode)..." $(call build_binary,-gcflags="all=-N -l",$(WAIT4X_BINARY_NAME)-debug) @echo "✅ Debug binary built: $(WAIT4X_BUILD_OUTPUT)/$(WAIT4X_BINARY_NAME)-debug" .PHONY: build-cross build-cross: ## Build Wait4X for multiple platforms @echo "Building Wait4X for multiple platforms..." @mkdir -p $(WAIT4X_BUILD_OUTPUT) @for os in linux darwin windows; do \ for arch in amd64 arm64; do \ if [ "$$os" = "windows" ]; then \ binary_name="$(WAIT4X_BINARY_NAME).exe"; \ else \ binary_name="$(WAIT4X_BINARY_NAME)"; \ fi; \ echo "Building for $$os/$$arch..."; \ CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \ $(GO_ENVIRONMENTS) $(GO_BINARY) build -v $(WAIT4X_BUILD_FLAGS) \ -o $(WAIT4X_BUILD_OUTPUT)/$(WAIT4X_BINARY_NAME)-$$os-$$arch/$$binary_name $(WAIT4X_MAIN_PATH); \ done; \ done @echo "✅ Cross-platform builds complete!" .PHONY: install install: build ## Install Wait4X to system @echo "Installing Wait4X..." $(GO_ENVIRONMENTS) $(GO_BINARY) install $(WAIT4X_BUILD_FLAGS) $(WAIT4X_MAIN_PATH) @echo "✅ Wait4X installed to $(shell go env GOPATH)/bin/" .PHONY: run run: ## Run Wait4X @echo "Running Wait4X..." $(GO_ENVIRONMENTS) $(GO_BINARY) run $(WAIT4X_RUN_FLAGS) $(WAIT4X_MAIN_PATH) $(WAIT4X_FLAGS) .PHONY: run-examples run-examples: ## Run example configurations @echo "Running examples..." @if [ -d "examples" ]; then \ for example in examples/*/; do \ if [ -f "$$example/main.go" ]; then \ echo "Running example: $$(basename $$example)"; \ cd $$example && go run main.go; \ cd - > /dev/null; \ fi; \ done; \ else \ echo "No examples directory found"; \ fi .PHONY: docker-build docker-build: ## Build Docker image @echo "Building Docker image..." docker build -t wait4x:latest . @echo "✅ Docker image built: wait4x:latest" .PHONY: docker-run docker-run: ## Run Wait4X in Docker @echo "Running Wait4X in Docker..." docker run --rm wait4x:latest $(WAIT4X_FLAGS) .PHONY: release release: clean deps lint test build-cross ## Prepare release (clean, lint, test, build) @echo "✅ Release preparation complete!" .PHONY: ci ci: deps lint test ## Run CI pipeline @echo "✅ CI pipeline completed successfully!" # ============================================================================= # Development helpers # ============================================================================= .PHONY: dev-setup dev-setup: ## Setup development environment @echo "Setting up development environment..." @echo "Using Nix for development environment setup..." @echo "Run 'nix develop' to enter the development shell with all tools pre-installed" @echo "Or run 'nix build' to build the project" @echo "✅ Development environment ready! (Managed by Nix)" # ============================================================================= # Documentation # ============================================================================= .PHONY: docs docs: ## Generate documentation @echo "Generating documentation..." @if [ -z "$(call check_cmd,godoc)" ]; then \ echo "⚠️ godoc not found. Install with: go install golang.org/x/tools/cmd/godoc@latest (or run 'nix develop' to install)"; \ exit 1; \ else \ echo "Running 'godoc -http=:6060' to start the documentation server"; \ echo "Open your browser to http://localhost:6060/pkg/$(WAIT4X_MODULE_NAME)/"; \ echo "Press Ctrl+C to stop the server"; \ godoc -http=:6060; \ fi # ============================================================================= # Default target # ============================================================================= .DEFAULT_GOAL := help wait4x-wait4x-7fb9e45/README.md000066400000000000000000000540361507553353000160740ustar00rootroot00000000000000
Wait4X Logo

Wait4X

Wait4X is a lightweight, zero-dependency tool to wait for services to be ready. Perfect for CI/CD, containers, and local development.

CI Status Coverage Go Report Docker Pulls Downloads Packaging Go Reference
--- ## 📑 Table of Contents - [Overview](#overview) - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Usage Examples](#usage-examples) - [Advanced Features](#advanced-features) - [Go Package Usage](#go-package-usage) - [CLI Reference](#cli-reference) - [Contributing](#contributing) - [License](#license) --- ## Overview **Wait4X** helps you wait for services (databases, APIs, message queues, etc.) to be ready before your app or script continues. It's ideal for: - **CI/CD pipelines**: Ensure dependencies are up before tests run - **Containers & orchestration**: Health check services before startup - **Deployments**: Verify readiness before rollout - **Local development**: Simplify service readiness checks ## Features | Feature | Description | | ----------------------------- | ---------------------------------------------------------------------- | | **Multi-Protocol** | TCP, HTTP, DNS, and more | | **Service Integrations** | Redis, MySQL, PostgreSQL, MongoDB, Kafka, RabbitMQ, InfluxDB, Temporal | | **Reverse/Parallel Checking** | Invert checks or check multiple services at once | | **Exponential Backoff** | Smarter retries | | **Cross-Platform** | Single binary for Linux, macOS, Windows | | **Go Package** | Use as a Go library | | **Command Execution** | Run commands after checks | ## 📥 Installation *After installing, jump to [Quick Start](#quick-start) to try it out!*
🐳 With Docker Wait4X provides automatically updated Docker images within Docker Hub: ```bash # Pull the image docker pull wait4x/wait4x:latest # Run the container docker run --rm wait4x/wait4x:latest --help ```
📦 From Package Managers **macOS:** ```bash brew install wait4x ``` **Alpine Linux:** ```bash apk add wait4x ``` **Arch Linux (AUR):** ```bash yay -S wait4x-bin ``` **NixOS:** ```bash nix-env -iA nixpkgs.wait4x ``` **Windows (Scoop):** ```bash scoop install wait4x ``` [![Packaging status](https://repology.org/badge/vertical-allrepos/wait4x.svg?exclude_unsupported=1)](https://repology.org/project/wait4x/versions)
📦 From Binary Download the appropriate version for your platform from the [releases page](https://github.com/wait4x/wait4x/releases): **Linux:** ```bash curl -LO https://github.com/wait4x/wait4x/releases/latest/download/wait4x-linux-amd64.tar.gz tar -xf wait4x-linux-amd64.tar.gz -C /tmp sudo mv /tmp/wait4x-linux-amd64/wait4x /usr/local/bin/ ``` **macOS:** ```bash curl -LO https://github.com/wait4x/wait4x/releases/latest/download/wait4x-darwin-amd64.tar.gz tar -xf wait4x-darwin-amd64.tar.gz -C /tmp sudo mv /tmp/wait4x-darwin-amd64/wait4x /usr/local/bin/ ``` **Windows:** ```bash curl -LO https://github.com/wait4x/wait4x/releases/latest/download/wait4x-windows-amd64.tar.gz tar -xf wait4x-windows-amd64.tar.gz # Move to a directory in your PATH ``` **Verify checksums:** ```bash curl -LO https://github.com/wait4x/wait4x/releases/latest/download/wait4x-linux-amd64.tar.gz.sha256sum sha256sum --check wait4x-linux-amd64.tar.gz.sha256sum ```
🐹 Go Install (for Go users) You can install Wait4X directly from source using Go (requires Go 1.16+): ```bash go install wait4x.dev/v3/cmd/wait4x@latest ``` This will place the `wait4x` binary in your `$GOPATH/bin` or `$HOME/go/bin` directory.
## 🚀 Quick Start Get started in seconds! After [installing](#installation), try these common checks: ### Wait for a TCP Port ```bash wait4x tcp localhost:3306 ``` ### HTTP Health Check ```bash wait4x http https://example.com/health --expect-status-code 200 ``` ### Wait for Multiple Services (Parallel) ```bash wait4x tcp 127.0.0.1:5432 127.0.0.1:6379 127.0.0.1:27017 ``` ### Database Readiness ```bash wait4x postgresql 'postgres://user:pass@localhost:5432/mydb?sslmode=disable' ``` For more, see [Usage Examples](#usage-examples) or [Detailed Usage](#detailed-usage). ## Usage Examples Here are some of the most useful Wait4X commands. Click the links for more details! - **TCP:** Wait for a port to be available [`wait4x tcp localhost:8080`](#main-commands) - **HTTP:** Wait for a web endpoint with status code and body check [`wait4x http https://api.example.com/health --expect-status-code 200 --expect-body-regex '"status":"UP"'`](#main-commands) - **DNS:** Wait for DNS A record [`wait4x dns A example.com`](#main-commands) - **MySQL:** Wait for MySQL DB [`wait4x mysql 'user:password@tcp(localhost:3306)/mydb'`](#main-commands) - **Redis:** Wait for Redis and check for a key [`wait4x redis redis://localhost:6379 --expect-key "session:active"`](#main-commands) - **Run a command after check:** [`wait4x tcp localhost:8080 -- ./start-app.sh`](#main-commands) - **Reverse check (wait for port to be free):** [`wait4x tcp localhost:8080 --invert-check`](#main-commands) - **Parallel check:** [`wait4x tcp localhost:3306 localhost:6379 localhost:27017`](#main-commands) See [Detailed Usage](#detailed-usage) for advanced options and more protocols. ## 📖 Detailed Usage Jump to: - [HTTP Checking](#http-checking) - [DNS Checking](#dns-checking) - [Database Checking](#database-checking) - [Message Queue Checking](#message-queue-checking) - [Shell Command](#shell-command) --- ### HTTP Checking Wait for an HTTP(S) endpoint to be ready, with flexible validation options. - **Status code check:** ```bash wait4x http https://api.example.com/health --expect-status-code 200 ``` - **Response body regex:** ```bash wait4x http https://api.example.com/status --expect-body-regex '"status":\s*"healthy"' ``` - **JSON path check:** ```bash wait4x http https://api.example.com/status --expect-body-json "services.database.status" ``` Uses [GJSON Path Syntax](https://github.com/tidwall/gjson#path-syntax). - **XPath check:** ```bash wait4x http https://example.com --expect-body-xpath "//div[@id='status']" ``` - **Custom request headers:** ```bash wait4x http https://api.example.com \ --request-header "Authorization: Bearer token123" \ --request-header "Content-Type: application/json" ``` - **Response header check:** ```bash wait4x http https://api.example.com --expect-header "Content-Type=application/json" ``` - **TLS options:** ```bash wait4x http https://www.wait4x.dev --cert-file /path/to/certfile --key-file /path/to/keyfile wait4x http https://www.wait4x.dev --ca-file /path/to/cafile ``` --- ### DNS Checking Check for various DNS record types and values. - **A record:** ```bash wait4x dns A example.com wait4x dns A example.com --expected-ip 93.184.216.34 wait4x dns A example.com --expected-ip 93.184.216.34 -n 8.8.8.8 ``` - **AAAA record (IPv6):** ```bash wait4x dns AAAA example.com --expected-ip "2606:2800:220:1:248:1893:25c8:1946" ``` - **CNAME record:** ```bash wait4x dns CNAME www.example.com --expected-domain example.com ``` - **MX record:** ```bash wait4x dns MX example.com --expected-domain "mail.example.com" ``` - **NS record:** ```bash wait4x dns NS example.com --expected-nameserver "ns1.example.com" ``` - **TXT record:** ```bash wait4x dns TXT example.com --expected-value "v=spf1 include:_spf.example.com ~all" ``` --- ### Database Checking Check readiness for popular databases. #### MySQL - **TCP connection:** ```bash wait4x mysql 'user:password@tcp(localhost:3306)/mydb' ``` - **Unix socket:** ```bash wait4x mysql 'user:password@unix(/var/run/mysqld/mysqld.sock)/mydb' ``` - **Check if a table exists:** ```bash wait4x mysql 'user:password@tcp(localhost:3306)/mydb' --expect-table my_table ``` #### PostgreSQL - **TCP connection:** ```bash wait4x postgresql 'postgres://user:password@localhost:5432/mydb?sslmode=disable' ``` - **Unix socket:** ```bash wait4x postgresql 'postgres://user:password@/mydb?host=/var/run/postgresql' ``` - **Check if a table exists:** ```bash wait4x postgresql 'postgres://user:password@localhost:5432/mydb?sslmode=disable' --expect-table my_table ``` If you need to specify a schema for the table existence check, you can use the `currentSchema=myschema` connection string parameter, for example: ```bash wait4x postgresql 'postgres://user:password@localhost:5432/mydb?sslmode=disable¤tSchema=myschema' --expect-table my_table ``` #### MongoDB ```bash wait4x mongodb 'mongodb://user:password@localhost:27017/mydb?maxPoolSize=20' ``` #### Redis - **Basic connection:** ```bash wait4x redis redis://localhost:6379 ``` - **With authentication and DB selection:** ```bash wait4x redis redis://user:password@localhost:6379/0 ``` - **Check for key existence:** ```bash wait4x redis redis://localhost:6379 --expect-key "session:active" ``` - **Check for key with value (regex):** ```bash wait4x redis redis://localhost:6379 --expect-key "status=^ready$" ``` #### InfluxDB ```bash wait4x influxdb http://localhost:8086 ``` --- ### Message Queue Checking #### RabbitMQ ```bash wait4x rabbitmq 'amqp://guest:guest@localhost:5672/myvhost' ``` #### Temporal - **Server check:** ```bash wait4x temporal server localhost:7233 ``` - **Worker check (namespace & task queue):** ```bash wait4x temporal worker localhost:7233 \ --namespace my-namespace \ --task-queue my-queue ``` - **Check for specific worker identity:** ```bash wait4x temporal worker localhost:7233 \ --namespace my-namespace \ --task-queue my-queue \ --expect-worker-identity-regex "worker-.*" ``` #### Kafka - **Basic Kafka broker readiness check:** ```bash wait4x kafka kafka://localhost:9092 ``` - **Check Kafka broker with SCRAM authentication:** ```bash wait4x kafka kafka://user:pass@localhost:9092?authMechanism=scram-sha-256 ``` - **Wait for multiple Kafka brokers (cluster) to be ready:** ```bash wait4x kafka kafka://broker1:9092 kafka://broker2:9092 kafka://broker3:9092 ``` > **Notes:** > - The connection string format is: kafka://[user:pass@]host:port[?option=value&...] > - Supported options: authMechanism (scram-sha-256, scram-sha-512) --- ### Shell Command Wait for a shell command to succeed or return a specific exit code. - **Check connection:** ```bash wait4x exec 'ping wait4x.dev -c 2' ``` - **Check file existence:** ```bash wait4x exec 'ls target/debug/main' --exit-code 2 ``` --- See [Advanced Features](#advanced-features) for timeout, retry, backoff, and parallel/reverse checking options. ## ⚙️ Advanced Features Jump to: - [Timeout & Retry Control](#timeout--retry-control) - [Exponential Backoff](#exponential-backoff) - [Reverse Checking](#reverse-checking) - [Command Execution](#command-execution) - [Parallel Checking](#parallel-checking) --- ### Timeout & Retry Control Control how long Wait4X waits and how often it checks. - **Set a timeout:** ```bash wait4x tcp localhost:8080 --timeout 30s ``` *Waits up to 30 seconds before giving up.* - **Set check interval:** ```bash wait4x tcp localhost:8080 --interval 2s ``` *Checks every 2 seconds (default: 1s).* --- ### Exponential Backoff Retry with increasing delays for more efficient waiting (useful for slow-starting services). - **Enable exponential backoff:** ```bash wait4x http https://api.example.com \ --backoff-policy exponential \ --backoff-exponential-coefficient 2.0 \ --backoff-exponential-max-interval 30s ``` *Doubles the wait time between retries, up to 30 seconds.* --- ### Reverse Checking Wait for a service to become unavailable (e.g., port to be free, service to stop). - **Wait for a port to become free:** ```bash wait4x tcp localhost:8080 --invert-check ``` - **Wait for a service to stop:** ```bash wait4x http https://service.local/health --expect-status-code 200 --invert-check ``` *Use for shutdown/cleanup workflows or to ensure a port is not in use.* --- ### Command Execution Run a command after a successful check (great for CI/CD or startup scripts). - **Run a script after waiting:** ```bash wait4x tcp localhost:3306 -- ./deploy.sh ``` - **Chain multiple commands:** ```bash wait4x redis redis://localhost:6379 -- echo "Redis is ready" && ./init-redis.sh ``` *Automate your workflow after dependencies are ready.* --- ### Parallel Checking Wait for multiple services at once (all must be ready to continue). - **Check several services in parallel:** ```bash wait4x tcp localhost:3306 localhost:6379 localhost:27017 ``` *Use for microservices, integration tests, or complex startup dependencies.* --- See [CLI Reference](#cli-reference) for all available flags and options. ## 📦 Go Package Usage
🔌 Installing as a Go Package Add Wait4X to your Go project: ```bash go get wait4x.dev/v3 ``` Import the packages you need: ```go import ( "context" "time" "wait4x.dev/v3/checker/tcp" // TCP checker "wait4x.dev/v3/checker/http" // HTTP checker "wait4x.dev/v3/checker/redis" // Redis checker "wait4x.dev/v3/waiter" // Waiter functionality ) ```
🌟 Example: TCP Checking ```go // Create a context with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Create a TCP checker tcpChecker := tcp.New("localhost:6379", tcp.WithTimeout(5*time.Second)) // Wait for the TCP port to be available err := waiter.WaitContext( ctx, tcpChecker, waiter.WithTimeout(time.Minute), waiter.WithInterval(2*time.Second), waiter.WithBackoffPolicy("exponential"), ) if err != nil { log.Fatalf("Failed to connect: %v", err) } fmt.Println("Service is ready!") ```
🌟 Example: HTTP with Advanced Options ```go // Create HTTP headers headers := http.Header{} headers.Add("Authorization", "Bearer token123") headers.Add("Content-Type", "application/json") // Create an HTTP checker with validation checker := http.New( "https://api.example.com/health", http.WithTimeout(5*time.Second), http.WithExpectStatusCode(200), http.WithExpectBodyJSON("status"), http.WithExpectBodyRegex(`"healthy":\s*true`), http.WithExpectHeader("Content-Type=application/json"), http.WithRequestHeaders(headers), ) // Wait for the API to be ready err := waiter.WaitContext(ctx, checker, options...) ```
🌟 Example: Parallel Service Checking ```go // Create checkers for multiple services checkers := []checker.Checker{ redis.New("redis://localhost:6379"), postgresql.New("postgres://user:pass@localhost:5432/db"), http.New("http://localhost:8080/health"), } // Wait for all services in parallel err := waiter.WaitParallelContext( ctx, checkers, waiter.WithTimeout(time.Minute), waiter.WithBackoffPolicy(waiter.BackoffPolicyExponential), ) ```
🌟 Example: Custom Checker Implementation ```go // Define your custom checker type FileChecker struct { filePath string minSize int64 } // Implement Checker interface func (f *FileChecker) Identity() (string, error) { return fmt.Sprintf("file(%s)", f.filePath), nil } func (f *FileChecker) Check(ctx context.Context) error { // Check if context is done select { case <-ctx.Done(): return ctx.Err() default: // Continue checking } fileInfo, err := os.Stat(f.filePath) if err != nil { if os.IsNotExist(err) { return checker.NewExpectedError( "file does not exist", err, "path", f.filePath, ) } return err } if fileInfo.Size() < f.minSize { return checker.NewExpectedError( "file is smaller than expected", nil, "path", f.filePath, "actual_size", fileInfo.Size(), "expected_min_size", f.minSize, ) } return nil } ```
For more detailed examples with complete code, see the [examples/pkg](examples/pkg) directory. Each example is in its own directory with a runnable `main.go` file. ## 📝 CLI Reference Wait4X provides a flexible CLI with many commands and options. Here is a summary of the main commands and global flags. For the most up-to-date and detailed information, use the built-in help: ```bash wait4x --help wait4x --help ``` ### Main Commands | Command | Description | | ------------ | ------------------------------------------------- | | `tcp` | Wait for a TCP port to become available | | `http` | Wait for an HTTP(S) endpoint with advanced checks | | `dns` | Wait for DNS records (A, AAAA, CNAME, MX, etc.) | | `kafka` | Wait for Kafka server | | `mysql` | Wait for a MySQL database to be ready | | `postgresql` | Wait for a PostgreSQL database to be ready | | `mongodb` | Wait for a MongoDB database to be ready | | `redis` | Wait for a Redis server or key | | `influxdb` | Wait for an InfluxDB server | | `rabbitmq` | Wait for a RabbitMQ server | | `temporal` | Wait for a Temporal server or worker | | `exec` | Wait for a shell command to succeed | Each command supports its own set of flags. See examples above or run `wait4x --help` for details. ### Global Flags | Flag | Description | | ------------------------------------ | --------------------------------------------- | | `--timeout`, `-t` | Set the maximum wait time (e.g., `30s`, `2m`) | | `--interval`, `-i` | Set the interval between checks (default: 1s) | | `--invert-check` | Invert the check (wait for NOT ready) | | `--backoff-policy` | Retry policy: `linear` or `exponential` | | `--backoff-exponential-coefficient` | Exponential backoff multiplier (default: 2.0) | | `--backoff-exponential-max-interval` | Max interval for exponential backoff | | `--quiet` | Suppress output except errors | | `--no-color` | Disable colored output | ### Getting Help For a full list of commands and options, run: ```bash wait4x --help wait4x --help ``` ## 🤝 Contributing We welcome contributions of all kinds! Whether you want to fix a bug, add a feature, improve documentation, or help others, you're in the right place. **How to contribute:** 1. [Fork the repository](https://github.com/wait4x/wait4x/fork) 2. Create a feature branch: `git checkout -b feature/your-feature-name` 3. Make your changes (add tests if possible) 4. Run tests: `make test` 5. Commit: `git commit -am 'Describe your change'` 6. Push: `git push origin feature/your-feature-name` 7. [Open a Pull Request](https://github.com/wait4x/wait4x/pulls) **Found a bug or have a feature request?** - [Report an issue](https://github.com/wait4x/wait4x/issues/new/choose) For more details, see [CONTRIBUTING.md](CONTRIBUTING.md) (if available). ## 💬 Community & Support - 💡 **Questions or ideas?** Use [GitHub Discussions](https://github.com/wait4x/wait4x/discussions) - 🐞 **Bugs or feature requests?** [Open an issue](https://github.com/wait4x/wait4x/issues/new/choose) - ⭐ **Star the repo** to support the project! ## 📄 License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ``` Copyright 2019-2025 The Wait4X Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ### Credits The project logo is based on the "Waiting Man" character (Zhdun) and is used with attribution to the original creator. wait4x-wait4x-7fb9e45/checker/000077500000000000000000000000001507553353000162115ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/checker.go000066400000000000000000000015511507553353000201460ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package checker provides the Checker interface for the Wait4X application. package checker import ( "context" ) // Checker is the interface that wraps the basic checker methods type Checker interface { Identity() (string, error) Check(ctx context.Context) error } wait4x-wait4x-7fb9e45/checker/dns/000077500000000000000000000000001507553353000167755ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/a/000077500000000000000000000000001507553353000172155ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/a/a.go000066400000000000000000000045271507553353000177740ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package a provides the A checker for the Wait4X application. package a import ( "context" "net" "wait4x.dev/v3/checker" ) // Option configures an DNS A records type Option func(d *A) // A is a DNS A checker type A struct { nameserver string address string expectedIPs []string resolver *net.Resolver } // New creates a new DNS A checker for the given address func New(address string, opts ...Option) checker.Checker { d := &A{ address: address, resolver: net.DefaultResolver, } // Apply the list of options to A for _, opt := range opts { opt(d) } // Nameserver settings. if d.nameserver != "" { d.resolver = &net.Resolver{ Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { dialer := net.Dialer{} return dialer.DialContext(ctx, network, d.nameserver) }, } } return d } // WithNameServer overrides the default nameserver for the DNS A checker func WithNameServer(nameserver string) Option { return func(d *A) { d.nameserver = nameserver } } // WithExpectedIPV4s sets expected IPv4s for the DNS A checker func WithExpectedIPV4s(ips []string) Option { return func(d *A) { d.expectedIPs = ips } } // Identity returns the identity of the DNS A checker func (d *A) Identity() (string, error) { return d.address, nil } // Check checks the DNS A records func (d *A) Check(ctx context.Context) (err error) { ips, err := d.resolver.LookupIP(ctx, "ip4", d.address) if err != nil { return err } for _, ip := range ips { if len(d.expectedIPs) == 0 { return nil } for _, expectedIP := range d.expectedIPs { if expectedIP == ip.String() { return nil } } } return checker.NewExpectedError( "the A record value doesn't expect", nil, "actual", ips, "expect", d.expectedIPs, ) } wait4x-wait4x-7fb9e45/checker/dns/a/a_test.go000066400000000000000000000043101507553353000210210ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package a provides the A checker for the Wait4X application. package a import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // server is the server to use for the tests const server = "wait4x.dev" // TestSuite is a test suite for the A checker type TestSuite struct { suite.Suite } // TestCheckExistenceA tests that the A checker correctly checks for the existence of an A record func (s *TestSuite) TestCheckExistenceA() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectA tests that the A checker correctly checks for the existence of an A record with the expected IP addresses. func (s *TestSuite) TestCorrectA() { d := New(server, WithExpectedIPV4s([]string{"172.67.154.180", "127.0.0.1"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectA tests that the A checker correctly checks for the existence of an A record with an unexpected IP address. func (s *TestSuite) TestIncorrectA() { var expectedError *checker.ExpectedError d := New(server, WithExpectedIPV4s([]string{"127.0.0.1"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectA tests that the A checker correctly checks for the existence of an A record // with the expected IP addresses using a custom name server. func (s *TestSuite) TestCustomNSCorrectA() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedIPV4s([]string{"172.67.154.180"})) s.Assert().Nil(d.Check(context.Background())) } // TestA is a test function that runs the TestSuite for the A checker. func TestA(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/dns/aaaa/000077500000000000000000000000001507553353000176605ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/aaaa/aaaa.go000066400000000000000000000053001507553353000210700ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package aaaa provides functionality for checking the AAAA records of a domain. package aaaa import ( "context" "fmt" "github.com/miekg/dns" dns2 "wait4x.dev/v3/checker/dns" "wait4x.dev/v3/checker" ) // Option configures an DNS AAAA records type Option func(d *AAAA) // AAAA represents DNS AAAA data structure type AAAA struct { nameserver string address string expectedIPs []string } // New creates a new AAAA checker with the given address and optional configuration options. func New(address string, opts ...Option) checker.Checker { d := &AAAA{ address: address, } // apply the list of options to AAAA for _, opt := range opts { opt(d) } return d } // WithNameServer overrides the default nameserver func WithNameServer(nameserver string) Option { return func(d *AAAA) { d.nameserver = nameserver } } // WithExpectedIPV6s sets expected IPv6s func WithExpectedIPV6s(ips []string) Option { return func(d *AAAA) { d.expectedIPs = ips } } // Identity returns the identity of the checker func (d *AAAA) Identity() (string, error) { return d.address, nil } // Check checks DNS records func (d *AAAA) Check(ctx context.Context) (err error) { c := new(dns.Client) c.Timeout = dns2.DefaultTimeout m := new(dns.Msg) m.SetQuestion(dns.Fqdn(d.address), dns.TypeAAAA) m.RecursionDesired = true r, _, err := c.ExchangeContext(ctx, m, dns2.RR(d.nameserver)) if err != nil { return err } if r.Rcode != dns.RcodeSuccess { return fmt.Errorf("response code is not successful, %d", r.Rcode) } if len(r.Answer) == 0 { return checker.NewExpectedError("no AAAA record found", nil) } if len(d.expectedIPs) == 0 { return nil } actualRecords := make([]string, 0) for _, answer := range r.Answer { if aaaa, ok := answer.(*dns.AAAA); ok { actualRecord := aaaa.AAAA.String() actualRecords = append(actualRecords, actualRecord) for _, expectedIP := range d.expectedIPs { if expectedIP == actualRecord { return nil } } } } return checker.NewExpectedError( "the AAAA record value doesn't match expected", nil, "actual", actualRecords, "expect", d.expectedIPs, ) } wait4x-wait4x-7fb9e45/checker/dns/aaaa/aaaa_test.go000066400000000000000000000045001507553353000221300ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package aaaa provides functionality for checking the AAAA records of a domain. package aaaa import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) const server = "wait4x.dev" // TestSuite is a test suite for the AAAA DNS checker. type TestSuite struct { suite.Suite } // TestCheckExistenceAAAA tests that the AAAA DNS checker correctly checks for the // existence of the expected AAAA record for the given server. func (s *TestSuite) TestCheckExistenceAAAA() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectAAAA tests that the AAAA DNS checker correctly checks for the // existence of the expected AAAA record for the given server. func (s *TestSuite) TestCorrectAAAA() { d := New(server, WithExpectedIPV6s([]string{"2606:4700:3034::6815:591"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectAAAA tests that the AAAA DNS checker correctly handles the case where // the expected AAAA record does not match the actual AAAA record for the given server. func (s *TestSuite) TestIncorrectAAAA() { var expectedError *checker.ExpectedError d := New(server, WithExpectedIPV6s([]string{"127.0.0.1"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectAAAA tests that the AAAA DNS checker correctly checks for the // existence of the expected AAAA record for the given server using a custom name server. func (s *TestSuite) TestCustomNSCorrectAAAA() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedIPV6s([]string{"2606:4700:3034::6815:591"})) s.Assert().Nil(d.Check(context.Background())) } // TestAAAA runs the test suite for the AAAA DNS checker. func TestAAAA(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/dns/cname/000077500000000000000000000000001507553353000200605ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/cname/cname.go000066400000000000000000000055211507553353000214750ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cname provides the CNAME checker for the Wait4X application. package cname import ( "context" "fmt" "regexp" "strings" "github.com/miekg/dns" "wait4x.dev/v3/checker" dns2 "wait4x.dev/v3/checker/dns" ) // Option configures an DNS CNAME record type Option func(d *CNAME) // CNAME is a DNS CNAME checker type CNAME struct { nameserver string address string expectedDomains []string } // New creates a new DNS CNAME checker func New(address string, opts ...Option) checker.Checker { d := &CNAME{ address: address, } // apply the list of options to CNAME for _, opt := range opts { opt(d) } return d } // WithNameServer overrides the default nameserver for the DNS CNAME checker func WithNameServer(nameserver string) Option { return func(d *CNAME) { d.nameserver = nameserver } } // WithExpectedDomains sets expected domains for the DNS CNAME checker func WithExpectedDomains(doamins []string) Option { return func(d *CNAME) { d.expectedDomains = doamins } } // Identity returns the identity of the DNS CNAME checker func (d *CNAME) Identity() (string, error) { return d.address, nil } // Check checks the DNS CNAME records func (d *CNAME) Check(ctx context.Context) (err error) { c := new(dns.Client) c.Timeout = dns2.DefaultTimeout m := new(dns.Msg) m.SetQuestion(dns.Fqdn(d.address), dns.TypeCNAME) m.RecursionDesired = true r, _, err := c.ExchangeContext(ctx, m, dns2.RR(d.nameserver)) if err != nil { return err } if r.Rcode != dns.RcodeSuccess { return fmt.Errorf("response code is not successful, %d", r.Rcode) } if len(r.Answer) == 0 { return checker.NewExpectedError("no CNAME record found", nil) } if len(d.expectedDomains) == 0 { return nil } actualRecords := make([]string, 0) for _, answer := range r.Answer { if cname, ok := answer.(*dns.CNAME); ok { actualRecord := strings.TrimSuffix(cname.Target, ".") actualRecords = append(actualRecords, actualRecord) for _, expectedDomain := range d.expectedDomains { matched, _ := regexp.MatchString(expectedDomain, actualRecord) if matched { return nil } } } } return checker.NewExpectedError( "the CNAME record value doesn't match expected", nil, "actual", actualRecords, "expect", d.expectedDomains, ) } wait4x-wait4x-7fb9e45/checker/dns/cname/cname_test.go000066400000000000000000000047101507553353000225330ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cname provides the CNAME checker for the Wait4X application. package cname import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // server is the server to use for the tests const server = "www.company.info" // TestSuite is a test suite for CNAME DNS checks type TestSuite struct { suite.Suite } // TestCheckExistenceCNAME tests that the CNAME DNS check passes when the expected CNAME domain is present. func (s *TestSuite) TestCheckExistenceCNAME() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectCNAME tests that the CNAME DNS check passes when the expected CNAME domain is present. func (s *TestSuite) TestCorrectCNAME() { d := New(server, WithExpectedDomains([]string{"company.info"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectCNAME tests that the CNAME DNS check fails when the expected CNAME domain is not present. func (s *TestSuite) TestIncorrectCNAME() { var expectedError *checker.ExpectedError d := New(server, WithExpectedDomains([]string{"something wrong"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectCNAME tests that the CNAME DNS check passes when the expected CNAME domain is present // and a custom name server is used. func (s *TestSuite) TestCustomNSCorrectCNAME() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedDomains([]string{"company.info"})) s.Assert().Nil(d.Check(context.Background())) } // TestRegexCorrectCNAME tests that the CNAME DNS check passes when the expected CNAME domain matches a regular expression. func (s *TestSuite) TestRegexCorrectCNAME() { d := New(server, WithExpectedDomains([]string{"company.*"})) s.Assert().Nil(d.Check(context.Background())) } // TestCNAME runs the test suite for CNAME DNS checks. func TestCNAME(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/dns/dns.go000066400000000000000000000027051507553353000201140ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS checker for the Wait4X application. package dns import ( "net" "os" "time" "github.com/miekg/dns" ) // DefaultTimeout is the default timeout for DNS requests var DefaultTimeout = 5 * time.Second // defaultRR is the default DNS resolver address var defaultRR = "1.1.1.1:53" // Cloudflare DNS resolver // RR returns the default DNS resolver address, or the first resolver address // from the system's resolv.conf file if it exists and is readable func RR(nameserver string) string { if nameserver != "" { return nameserver } // Check if resolv.conf exists and is readable if _, err := os.Stat("/etc/resolv.conf"); err != nil { return defaultRR } conf, err := dns.ClientConfigFromFile("/etc/resolv.conf") if err != nil || len(conf.Servers) == 0 { return defaultRR } return net.JoinHostPort(conf.Servers[0], conf.Port) } wait4x-wait4x-7fb9e45/checker/dns/mx/000077500000000000000000000000001507553353000174215ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/mx/mx.go000066400000000000000000000047611507553353000204040ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mx provides the MX checker for the Wait4X application. package mx import ( "context" "fmt" "net" "regexp" "wait4x.dev/v3/checker" ) // Option configures an DNS MX records type Option func(d *MX) // MX is a DNS MX checker type MX struct { nameserver string address string expectedDomains []string resolver *net.Resolver } // New creates a new DNS MX checker func New(address string, opts ...Option) checker.Checker { d := &MX{ address: address, resolver: net.DefaultResolver, } // apply the list of options to MX for _, opt := range opts { opt(d) } // Nameserver settings. if d.nameserver != "" { d.resolver = &net.Resolver{ Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { dialer := net.Dialer{} return dialer.DialContext(ctx, network, d.nameserver) }, } } return d } // WithNameServer overrides the default nameserver for the DNS MX checker func WithNameServer(nameserver string) Option { return func(d *MX) { d.nameserver = nameserver } } // WithExpectedDomains sets expected domains for the DNS MX checker func WithExpectedDomains(domains []string) Option { return func(d *MX) { d.expectedDomains = domains } } // Identity returns the identity of the DNS MX checker func (d *MX) Identity() (string, error) { return fmt.Sprintf("MX %s %s", d.address, d.expectedDomains), nil } // Check checks the DNS MX records func (d *MX) Check(ctx context.Context) (err error) { values, err := d.resolver.LookupMX(ctx, d.address) if err != nil { return err } for _, mx := range values { if len(d.expectedDomains) == 0 { return nil } for _, expectedDomain := range d.expectedDomains { matched, _ := regexp.MatchString(expectedDomain, mx.Host) if matched { return nil } } } return checker.NewExpectedError( "the MX record value doesn't expect", nil, "actual", values, "expect", d.expectedDomains, ) } wait4x-wait4x-7fb9e45/checker/dns/mx/mx_test.go000066400000000000000000000051171507553353000214370ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mx provides the MX checker for the Wait4X application. package mx import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // server is the server to use for the tests const server = "wait4x.dev" // TestSuite is a test suite for the MX checker type TestSuite struct { suite.Suite } // TestCheckExistenceMX tests that the MX checker correctly checks the existence of MX records for the given server. func (s *TestSuite) TestCheckExistenceMX() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectMX tests that the MX checker correctly checks the existence of MX records for the given server with the expected domains. func (s *TestSuite) TestCorrectMX() { d := New(server, WithExpectedDomains([]string{"route1.mx.cloudflare.net", "route2.mx.cloudflare.net"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectMX tests that the MX checker correctly identifies when the expected MX records do not exist for the given server. func (s *TestSuite) TestIncorrectMX() { var expectedError *checker.ExpectedError d := New(server, WithExpectedDomains([]string{"127.0.0.1"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectA tests that the MX checker correctly checks the existence of MX records for the given server // using a custom name server. func (s *TestSuite) TestCustomNSCorrectA() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedDomains([]string{"route1.mx.cloudflare.net"})) s.Assert().Nil(d.Check(context.Background())) } // TestRegexCorrectA tests that the MX checker correctly checks the existence of MX records for the given server // using a regular expression to match the expected domains. func (s *TestSuite) TestRegexCorrectA() { d := New(server, WithExpectedDomains([]string{".*.mx.cloudflare.net"})) s.Assert().Nil(d.Check(context.Background())) } // TestMX runs the test suite for the MX checker. func TestMX(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/dns/ns/000077500000000000000000000000001507553353000174155ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/ns/ns.go000066400000000000000000000047721507553353000203760ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ns provides the NS checker for the Wait4X application. package ns import ( "context" "net" "regexp" "wait4x.dev/v3/checker" ) // Option configures an DNS NS records type Option func(d *NS) // NS is a DNS NS checker type NS struct { nameserver string address string expectedNameservers []string resolver *net.Resolver } // New creates a new DNS NS checker func New(address string, opts ...Option) checker.Checker { d := &NS{ address: address, resolver: net.DefaultResolver, } // apply the list of options to NS for _, opt := range opts { opt(d) } // Nameserver settings. if d.nameserver != "" { d.resolver = &net.Resolver{ Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { dialer := net.Dialer{} return dialer.DialContext(ctx, network, d.nameserver) }, } } return d } // WithNameServer overrides the default nameserver for the DNS NS checker func WithNameServer(nameserver string) Option { return func(d *NS) { d.nameserver = nameserver } } // WithExpectedNameservers sets expected nameservers for the DNS NS checker func WithExpectedNameservers(nameservers []string) Option { return func(d *NS) { d.expectedNameservers = nameservers } } // Identity returns the identity of the DNS NS checker func (d *NS) Identity() (string, error) { return d.address, nil } // Check checks the DNS NS records func (d *NS) Check(ctx context.Context) (err error) { values, err := d.resolver.LookupNS(ctx, d.address) if err != nil { return err } for _, ns := range values { if len(d.expectedNameservers) == 0 { return nil } for _, expectedNameserver := range d.expectedNameservers { matched, _ := regexp.MatchString(expectedNameserver, ns.Host) if matched { return nil } } } return checker.NewExpectedError( "the NS record value doesn't expect", nil, "actual", values, "expect", d.expectedNameservers, ) } wait4x-wait4x-7fb9e45/checker/dns/ns/ns_test.go000066400000000000000000000053051507553353000214260ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ns provides the NS checker for the Wait4X application. package ns import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // server is the server to use for the tests const server = "wait4x.dev" // TestSuite is a test suite for the DNS nameserver checker type TestSuite struct { suite.Suite } // TestCheckExistenceNS tests that the DNS nameserver checker correctly checks the existence of the nameservers for the given domain. func (s *TestSuite) TestCheckExistenceNS() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectNS tests that the DNS nameserver checker correctly checks the existence of the expected nameservers for the given domain. func (s *TestSuite) TestCorrectNS() { d := New(server, WithExpectedNameservers([]string{"gordon.ns.cloudflare.com.", "emma.ns.cloudflare.com"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectNS tests that the DNS nameserver checker correctly identifies when the expected nameservers // do not match the actual nameservers for the given domain. func (s *TestSuite) TestIncorrectNS() { var expectedError *checker.ExpectedError d := New(server, WithExpectedNameservers([]string{"127.0.0.1"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectNS tests that the DNS nameserver checker correctly checks the existence of the expected // nameservers for the given domain using a custom nameserver. func (s *TestSuite) TestCustomNSCorrectNS() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedNameservers([]string{"gordon.ns.cloudflare.com."})) s.Assert().Nil(d.Check(context.Background())) } // TestRegexCorrectNS tests that the DNS nameserver checker correctly checks the existence of the expected // nameservers for the given domain using a regular expression. func (s *TestSuite) TestRegexCorrectNS() { d := New(server, WithExpectedNameservers([]string{".*.cloudflare.com"})) s.Assert().Nil(d.Check(context.Background())) } // TestNS runs the test suite for the DNS nameserver checker. func TestNS(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/dns/txt/000077500000000000000000000000001507553353000176145ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/dns/txt/txt.go000066400000000000000000000047011507553353000207640ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package txt provides the TXT checker for the Wait4X application. package txt import ( "context" "net" "regexp" "wait4x.dev/v3/checker" ) // Option configures an DNS TXT records type Option func(d *TXT) // TXT is a DNS TXT checker type TXT struct { nameserver string address string expectedValues []string resolver *net.Resolver } // New creates a new DNS TXT checker func New(address string, opts ...Option) checker.Checker { d := &TXT{ address: address, resolver: net.DefaultResolver, } // apply the list of options to TXT for _, opt := range opts { opt(d) } // Nameserver settings. if d.nameserver != "" { d.resolver = &net.Resolver{ Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { dialer := net.Dialer{} return dialer.DialContext(ctx, network, d.nameserver) }, } } return d } // WithNameServer overrides the default nameserver for the DNS TXT checker func WithNameServer(nameserver string) Option { return func(d *TXT) { d.nameserver = nameserver } } // WithExpectedValues sets expected values for the DNS TXT checker func WithExpectedValues(values []string) Option { return func(d *TXT) { d.expectedValues = values } } // Identity returns the identity of the DNS TXT checker func (d *TXT) Identity() (string, error) { return d.address, nil } // Check checks the DNS TXT records func (d *TXT) Check(ctx context.Context) (err error) { values, err := d.resolver.LookupTXT(ctx, d.address) if err != nil { return err } for _, txt := range values { if len(d.expectedValues) == 0 { return nil } for _, expectedValue := range d.expectedValues { matched, _ := regexp.MatchString(expectedValue, txt) if matched { return nil } } } return checker.NewExpectedError( "the TXT record value doesn't expect", nil, "actual", values, "expect", d.expectedValues, ) } wait4x-wait4x-7fb9e45/checker/dns/txt/txt_test.go000066400000000000000000000047511507553353000220300ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package txt provides the TXT checker for the Wait4X application. package txt import ( "context" "testing" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // server is the server to use for the tests const server = "wait4x.dev" // TestSuite is a test suite for the TXT record checker type TestSuite struct { suite.Suite } // TestCheckExistenceTXT checks that the TXT record for the specified server exists func (s *TestSuite) TestCheckExistenceTXT() { d := New(server) s.Assert().Nil(d.Check(context.Background())) } // TestCorrectTXT checks that the TXT record for the specified server has the expected value func (s *TestSuite) TestCorrectTXT() { d := New(server, WithExpectedValues([]string{"v=spf1 include:_spf.mx.cloudflare.net ~all"})) s.Assert().Nil(d.Check(context.Background())) } // TestIncorrectTXT checks that the TXT record for the specified server has an incorrect value, and that the expected error is returned. func (s *TestSuite) TestIncorrectTXT() { var expectedError *checker.ExpectedError d := New(server, WithExpectedValues([]string{"127.0.0.1"})) s.Assert().ErrorAs(d.Check(context.Background()), &expectedError) } // TestCustomNSCorrectTXT checks that the TXT record for the specified server has the expected value, using a custom name server. func (s *TestSuite) TestCustomNSCorrectTXT() { d := New(server, WithNameServer("8.8.8.8:53"), WithExpectedValues([]string{"v=spf1 include:_spf.mx.cloudflare.net ~all"})) s.Assert().Nil(d.Check(context.Background())) } // TestRegexCorrectTXT checks that the TXT record for the specified server has a value that matches the expected regular expression. func (s *TestSuite) TestRegexCorrectTXT() { d := New(server, WithExpectedValues([]string{".* include:.*"})) s.Assert().Nil(d.Check(context.Background())) } // TestTXT runs the test suite for the TXT record checker. func TestTXT(t *testing.T) { suite.Run(t, new(TestSuite)) } wait4x-wait4x-7fb9e45/checker/error.go000066400000000000000000000024051507553353000176720ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package checker import "fmt" // ExpectedError defines the expectation error type ExpectedError struct { msg string cause error details []any } // NewExpectedError creates the ExpectedError func NewExpectedError(msg string, cause error, details ...any) error { ee := &ExpectedError{ msg: msg, cause: cause, details: details, } return ee } // Details returns the error details func (ee *ExpectedError) Details() []any { return ee.details } func (ee *ExpectedError) Unwrap() error { return ee.cause } func (ee *ExpectedError) Error() string { if ee.cause != nil { return fmt.Sprintf("%s, caused by: %s", ee.msg, ee.cause.Error()) } return ee.msg } wait4x-wait4x-7fb9e45/checker/exec/000077500000000000000000000000001507553353000171355ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/exec/exec.go000066400000000000000000000054331507553353000204150ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package exec provides the Exec checker for the Wait4X application. package exec import ( "context" "fmt" "os/exec" "strings" "wait4x.dev/v3/checker" ) // Option configures an Exec checker type Option func(e *Exec) // Exec is a command execution checker type Exec struct { command string args []string expectExitCode int } // New creates a new Exec checker func New(command string, opts ...Option) checker.Checker { e := &Exec{ command: command, expectExitCode: 0, // Default to expecting exit code 0 } // apply the list of options to Exec for _, opt := range opts { opt(e) } return e } // WithArgs configures command arguments func WithArgs(args []string) Option { return func(e *Exec) { e.args = args } } // WithExpectExitCode configures expected exit code func WithExpectExitCode(code int) Option { return func(e *Exec) { e.expectExitCode = code } } // Identity returns the identity of the checker func (e *Exec) Identity() (string, error) { if len(e.args) > 0 { return fmt.Sprintf("%s %s", e.command, strings.Join(e.args, " ")), nil } return e.command, nil } // Check executes the command and checks if it returns the expected exit code func (e *Exec) Check(ctx context.Context) error { // Create command with context for cancellation support cmd := exec.CommandContext(ctx, e.command, e.args...) err := cmd.Run() // Check if context was canceled select { case <-ctx.Done(): return ctx.Err() default: // Continue with exit code checking } // Handle the command execution result exitCode := 0 if err != nil { // If there's an error, try to get the exit code if exitErr, ok := err.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() } else { // This is not an exit error but some other error (like command not found) return checker.NewExpectedError( "failed to execute command", err, "command", e.command, "args", e.args, ) } } // Check if the exit code matches the expected one if exitCode != e.expectExitCode { return checker.NewExpectedError( "command exited with unexpected code", nil, "command", e.command, "args", e.args, "actual", exitCode, "expect", e.expectExitCode, ) } return nil } wait4x-wait4x-7fb9e45/checker/http/000077500000000000000000000000001507553353000171705ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/http/http.go000066400000000000000000000245451507553353000205100ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package http provides the HTTP checker for the Wait4X application. package http import ( "context" "crypto/tls" "crypto/x509" "errors" "io" "net" "net/http" "net/url" "os" "regexp" "strings" "time" "github.com/antchfx/htmlquery" "github.com/tidwall/gjson" "golang.org/x/net/http2" "wait4x.dev/v3/checker" ) // Option configures an HTTP. type Option func(h *HTTP) const ( // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 3 * time.Second // DefaultInsecureSkipTLSVerify is the default insecure skip tls verify DefaultInsecureSkipTLSVerify = false // DefaultNoRedirect is the default auto redirect DefaultNoRedirect = false ) // HTTP is an HTTP checker type HTTP struct { address string timeout time.Duration expectBodyRegex string expectBodyJSON string expectBodyXPath string expectHeader string requestHeaders http.Header requestBody io.Reader expectStatusCode int insecureSkipTLSVerify bool noRedirect bool caFile string certFile string keyFile string h2c bool } // New creates the HTTP checker func New(address string, opts ...Option) checker.Checker { h := &HTTP{ address: address, timeout: DefaultConnectionTimeout, insecureSkipTLSVerify: DefaultInsecureSkipTLSVerify, noRedirect: DefaultNoRedirect, } // apply the list of options to HTTP for _, opt := range opts { opt(h) } return h } // WithTimeout configures a time limit for requests made by the HTTP client func WithTimeout(timeout time.Duration) Option { return func(h *HTTP) { h.timeout = timeout } } // WithExpectBodyRegex configures response body expectation func WithExpectBodyRegex(regex string) Option { return func(h *HTTP) { h.expectBodyRegex = regex } } // WithExpectBodyJSON configures response json expectation func WithExpectBodyJSON(json string) Option { return func(h *HTTP) { h.expectBodyJSON = json } } // WithExpectBodyXPath configures response xpath expectation func WithExpectBodyXPath(xpath string) Option { return func(h *HTTP) { h.expectBodyXPath = xpath } } // WithExpectHeader configures response header expectation func WithExpectHeader(header string) Option { return func(h *HTTP) { h.expectHeader = header } } // WithRequestHeaders configures request header func WithRequestHeaders(headers http.Header) Option { return func(h *HTTP) { h.requestHeaders = headers } } // WithRequestBody configures request body func WithRequestBody(body io.Reader) Option { return func(h *HTTP) { h.requestBody = body } } // WithRequestHeader configures request header func WithRequestHeader(key string, value []string) Option { return func(h *HTTP) { if h.requestHeaders == nil { h.requestHeaders = http.Header{} } h.requestHeaders[key] = value } } // WithExpectStatusCode configures response status code expectation func WithExpectStatusCode(code int) Option { return func(h *HTTP) { h.expectStatusCode = code } } // WithInsecureSkipTLSVerify configures insecure skip tls verify func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option { return func(h *HTTP) { h.insecureSkipTLSVerify = insecureSkipTLSVerify } } // WithNoRedirect configures auto redirect func WithNoRedirect(noRedirect bool) Option { return func(h *HTTP) { h.noRedirect = noRedirect } } // WithCAFile configures CA file func WithCAFile(path string) Option { return func(h *HTTP) { h.caFile = path } } // WithCertFile configures Cert file func WithCertFile(path string) Option { return func(h *HTTP) { h.certFile = path } } // WithKeyFile configures key file func WithKeyFile(path string) Option { return func(h *HTTP) { h.keyFile = path } } // WithH2C enables prior-knowledge HTTP/2 over cleartext (h2c) for http:// URLs. func WithH2C(enable bool) Option { return func(h *HTTP) { h.h2c = enable } } // Identity returns the identity of the checker func (h *HTTP) Identity() (string, error) { return h.address, nil } // Check checks HTTP connection func (h *HTTP) Check(ctx context.Context) (err error) { tlsConfig, err := h.getTLSConfig() if err != nil { return } // Base transport (also used for HTTPS and for HTTP when h2c is not applicable). baseTransport := &http.Transport{ TLSClientConfig: tlsConfig, Proxy: http.ProxyFromEnvironment, } transport := http.RoundTripper(baseTransport) // Opt-in h2c (prior-knowledge) for cleartext HTTP when: // - explicitly enabled, // - scheme is http, // - no proxy is configured for this URL, // - noRedirect is true (avoid redirect cross-scheme issues with a single transport). if h.h2c && h.noRedirect { if u, perr := url.Parse(h.address); perr == nil && strings.EqualFold(u.Scheme, "http") { if p, _ := http.ProxyFromEnvironment(&http.Request{URL: u}); p == nil { transport = &http2.Transport{ AllowHTTP: true, DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { return net.DialTimeout(network, addr, h.timeout) }, } } } } httpClient := &http.Client{ Timeout: h.timeout, Transport: transport, } if h.noRedirect { httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse } } method := http.MethodGet if h.requestBody != nil { method = http.MethodPost } req, err := http.NewRequestWithContext(ctx, method, h.address, h.requestBody) if err != nil { return err } req.Header = h.requestHeaders resp, err := httpClient.Do(req) if err != nil { if os.IsTimeout(err) { return checker.NewExpectedError( "timed out while making an http call", err, "timeout", h.timeout, ) } else if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish an http connection", err, "address", h.address, ) } return err } defer func(body io.ReadCloser) { if cerr := body.Close(); cerr != nil { err = cerr } }(resp.Body) if h.expectStatusCode != 0 { err := h.checkingStatusCodeExpectation(resp) if err != nil { return err } } if h.expectBodyRegex != "" { err := h.checkingBodyExpectation(resp) if err != nil { return err } } if h.expectBodyJSON != "" { err := h.checkingJSONExpectation(resp) if err != nil { return err } } if h.expectBodyXPath != "" { err := h.checkingXPathExpectation(resp) if err != nil { return err } } if h.expectHeader != "" { return h.checkingHeaderExpectation(resp) } return nil } // getTLSConfig prepares TLS config func (h *HTTP) getTLSConfig() (*tls.Config, error) { cfg := tls.Config{ InsecureSkipVerify: h.insecureSkipTLSVerify, } if h.insecureSkipTLSVerify { return &cfg, nil } // Cert and key files. if h.certFile != "" || h.keyFile != "" { cert, err := tls.LoadX509KeyPair(h.certFile, h.keyFile) if err != nil { return nil, err } cfg.Certificates = []tls.Certificate{cert} } // CA file. if h.caFile != "" { ca, err := os.ReadFile(h.caFile) if err != nil { return nil, err } certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(ca) { return nil, errors.New("can't append the CA file") } cfg.RootCAs = certPool } return &cfg, nil } func (h *HTTP) checkingStatusCodeExpectation(resp *http.Response) error { if h.expectStatusCode != resp.StatusCode { return checker.NewExpectedError( "the status code doesn't expect", nil, "actual", resp.StatusCode, "expect", h.expectStatusCode, ) } return nil } func (h *HTTP) checkingBodyExpectation(resp *http.Response) error { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } bodyString := string(bodyBytes) matched, _ := regexp.MatchString(h.expectBodyRegex, bodyString) if !matched { return checker.NewExpectedError( "the body doesn't expect", nil, "actual", h.truncateString(bodyString, 50), "expect", h.expectBodyRegex, ) } return nil } func (h *HTTP) checkingJSONExpectation(resp *http.Response) error { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } bodyString := string(bodyBytes) value := gjson.Get(bodyString, h.expectBodyJSON) if !value.Exists() { return checker.NewExpectedError( "the JSON doesn't match", nil, "actual", h.truncateString(bodyString, 50), "expect", h.expectBodyJSON, ) } return nil } func (h *HTTP) checkingXPathExpectation(resp *http.Response) error { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } bodyString := string(bodyBytes) doc, err := htmlquery.Parse(strings.NewReader(bodyString)) if err != nil { return err } node, err := htmlquery.Query(doc, h.expectBodyXPath) if err != nil { return err } if node == nil { return checker.NewExpectedError( "the XPath doesn't match", nil, "actual", h.truncateString(bodyString, 50), "expect", h.expectBodyXPath, ) } return nil } func (h *HTTP) checkingHeaderExpectation(resp *http.Response) error { // Key value. e.g. Content-Type=application/json expectedHeaderParsed := strings.SplitN(h.expectHeader, "=", 2) if len(expectedHeaderParsed) == 2 { headerValue := resp.Header.Get(expectedHeaderParsed[0]) matched, _ := regexp.MatchString(expectedHeaderParsed[1], headerValue) if !matched { return checker.NewExpectedError( "the http header key and value doesn't expect", nil, "actual", headerValue, "expect", h.expectHeader, ) } } // Only key. if _, ok := resp.Header[expectedHeaderParsed[0]]; !ok { return checker.NewExpectedError( "the http header key doesn't expect", nil, "actual", resp.Header, "expect", h.expectHeader, ) } return nil } func (h *HTTP) truncateString(str string, num int) string { truncatedStr := str if len(str) > num { if num > 3 { num -= 3 } truncatedStr = str[0:num] + "..." } return truncatedStr } wait4x-wait4x-7fb9e45/checker/http/http_test.go000066400000000000000000000300741507553353000215410ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package http provides the HTTP checker for the Wait4X application. package http import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "wait4x.dev/v3/checker" ) // TestMain is the main function for the HTTP checker. func TestMain(m *testing.M) { os.Exit(m.Run()) } // TestHttpInvalidAddress tests the HTTP checker with an invalid address. func TestHttpInvalidAddress(t *testing.T) { hc := New("http://not-exists.tld", WithTimeout(time.Second)) assert.Error(t, hc.Check(context.TODO())) } // TestHttpValidAddress tests the HTTP checker with a valid address. func TestHttpValidAddress(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer ts.Close() hc := New(ts.URL) identity, err := hc.Identity() assert.Nil(t, err) assert.Nil(t, hc.Check(context.TODO())) assert.Equal(t, ts.URL, identity) } // TestHttpInvalidStatusCode tests the HTTP checker with an invalid status code. func TestHttpInvalidStatusCode(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer ts.Close() hc := New(ts.URL, WithExpectStatusCode(http.StatusCreated)) var expectedError *checker.ExpectedError assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) } // TestHttpValidStatusCode tests the HTTP checker with a valid status code. func TestHttpValidStatusCode(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer ts.Close() hc := New(ts.URL, WithExpectStatusCode(http.StatusOK)) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpInvalidTLS tests the HTTP checker with an invalid TLS certificate. func TestHttpInvalidTLS(t *testing.T) { hc := New("https://expired.badssl.com", WithInsecureSkipTLSVerify(true)) assert.Nil(t, hc.Check(context.TODO())) } func TestHttpH2CEnabled_Succeeds(t *testing.T) { h2cOnly := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor != 2 { http.Error(w, "h2c required", http.StatusHTTPVersionNotSupported) return } w.WriteHeader(http.StatusOK) }) srv := httptest.NewUnstartedServer(h2c.NewHandler(h2cOnly, &http2.Server{})) srv.Start() defer srv.Close() hc := New(srv.URL, WithTimeout(2*time.Second), WithNoRedirect(true), WithH2C(true), WithExpectStatusCode(http.StatusOK)) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpNoRedirect tests the HTTP checker with no redirect. func TestHttpNoRedirect(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", "https://wait4x.dev") w.WriteHeader(http.StatusTemporaryRedirect) })) defer ts.Close() hc := New(ts.URL, WithExpectStatusCode(http.StatusTemporaryRedirect), WithNoRedirect(true)) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpRedirect tests the HTTP checker with a redirect. func TestHttpRedirect(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", "https://wait4x.dev") w.WriteHeader(http.StatusTemporaryRedirect) })) defer ts.Close() hc := New(ts.URL, WithExpectStatusCode(http.StatusOK)) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpInvalidBody tests the HTTP checker with an invalid body. func TestHttpInvalidBody(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Wait4X")) })) defer ts.Close() hc := New(ts.URL, WithExpectBodyRegex("FooBar")) var expectedError *checker.ExpectedError assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) } // TestHttpValidBody tests the HTTP checker with a valid body. func TestHttpValidBody(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Wait4X is the best CLI tools. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla volutpat arcu malesuada lacus vulputate feugiat. Etiam vitae sem quis ligula consequat euismod. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus fringilla sapien non lacus volutpat sollicitudin. Donec sollicitudin sit amet purus ac rutrum. Nam nunc orci, luctus a sagittis.")) })) defer ts.Close() hc := New(ts.URL, WithExpectBodyRegex("Wait4X.+best.+tools")) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpValidBodyJSON tests the HTTP checker with a valid body JSON. func TestHttpValidBodyJSON(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"user": {"name": "test"}, "is_active": true}`)) })) defer ts.Close() hc := New(ts.URL, WithExpectBodyJSON("user")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectBodyJSON("user.name")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectBodyJSON("is_active")) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpInvalidBodyJSON tests the HTTP checker with an invalid body JSON. func TestHttpInvalidBodyJSON(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"user": {"name": "test"}, "is_active": true}`)) })) defer ts.Close() hc := New(ts.URL, WithExpectBodyJSON("test")) var expectedError *checker.ExpectedError assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) } // TestHttpInvalidBodyXPath tests the HTTP checker with an invalid body XPath. func TestHttpInvalidBodyXPath(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("
127.0.0.1
")) })) defer ts.Close() var expectedError *checker.ExpectedError hc := New(ts.URL, WithExpectBodyXPath("//hello")) assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) hc = New(ts.URL, WithExpectBodyXPath("//code[@id='test']")) assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) } // TestHttpValidBodyXPath tests the HTTP checker with a valid body XPath. func TestHttpValidBodyXPath(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("
127.0.0.1
")) })) defer ts.Close() hc := New(ts.URL, WithExpectBodyXPath("//div/code")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectBodyXPath("//code[@id='ip']")) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpValidHeader tests the HTTP checker with a valid header. func TestHttpValidHeader(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Test-Header", "test-value") w.Header().Add("Test-Header-New", "test-value-new") w.Header().Add("Authorization", "Token 1234") w.Header().Add("X-Foo", "") })) defer ts.Close() hc := New(ts.URL, WithExpectHeader("Test-Header")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectHeader("X-Foo")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectHeader("X-Foo=.*")) assert.Nil(t, hc.Check(context.TODO())) // Regex. hc = New(ts.URL, WithExpectHeader("Test-Header=test-.+")) assert.Nil(t, hc.Check(context.TODO())) hc = New(ts.URL, WithExpectHeader("Authorization=^Token\\s.+")) assert.Nil(t, hc.Check(context.TODO())) // Key value. hc = New(ts.URL, WithExpectHeader("Test-Header=test-value")) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpInvalidHeader tests the HTTP checker with an invalid header. func TestHttpInvalidHeader(t *testing.T) { var expectedError *checker.ExpectedError ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Test-Header", "test-value") })) defer ts.Close() hc := New(ts.URL, WithExpectHeader("Test-Header-New")) assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) hc = New(ts.URL, WithExpectHeader("Test-.+=test-value")) assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) hc = New(ts.URL, WithExpectHeader("Test-Header=[A-Z]")) assert.ErrorAs(t, hc.Check(context.TODO()), &expectedError) } // TestHttpRequestHeaders tests the HTTP checker with request headers. func TestHttpRequestHeaders(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) resp := new(bytes.Buffer) for key, value := range r.Header { fmt.Fprintf(resp, "%s=%s,", key, value) } w.Write(resp.Bytes()) })) defer ts.Close() hc := New( ts.URL, WithRequestHeaders(http.Header{"Authorization": []string{"Token 123"}}), WithRequestHeader("Foo", []string{"test1 test2"}), WithExpectBodyRegex("(.*Authorization=\\[Token 123\\].*Foo=\\[test1 test2\\].*)|(.*Foo=\\[test1 test2\\].*Authorization=\\[Token 123\\].*)"), ) assert.Nil(t, hc.Check(context.TODO())) } // TestHttpInvalidCombinationFeatures tests the HTTP checker with invalid combination features. func TestHttpInvalidCombinationFeatures(t *testing.T) { var expectedError *checker.ExpectedError ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) w.Header().Set("Test-Header", "test-value") w.Write([]byte("Wait4X")) })) defer ts.Close() hc := New(ts.URL, WithExpectStatusCode(http.StatusCreated), WithExpectBodyRegex("FooBar")) err := hc.Check(context.TODO()) assert.ErrorAs(t, err, &expectedError) assert.Equal(t, "the body doesn't expect", err.Error()) hc = New(ts.URL, WithExpectStatusCode(http.StatusCreated), WithExpectBodyRegex("Wait4X"), WithExpectHeader("X-Foo")) err = hc.Check(context.TODO()) assert.ErrorAs(t, err, &expectedError) assert.Equal(t, "the http header key doesn't expect", err.Error()) hc = New(ts.URL, WithExpectStatusCode(http.StatusOK), WithExpectBodyRegex("Wait4X"), WithExpectHeader("Test-Header")) err = hc.Check(context.TODO()) assert.ErrorAs(t, err, &expectedError) assert.Equal(t, "the status code doesn't expect", err.Error()) } // TestHttpRequestBody tests the HTTP checker with a request body. func TestHttpRequestBody(t *testing.T) { var expectedError *checker.ExpectedError ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) buf := new(bytes.Buffer) buf.ReadFrom(r.Body) w.Write(buf.Bytes()) })) defer ts.Close() hc := New( ts.URL, WithRequestBody(strings.NewReader("name=test&score=1")), WithExpectBodyRegex("something"), ) err := hc.Check(context.TODO()) assert.ErrorAs(t, err, &expectedError) hc = New( ts.URL, WithRequestBody(strings.NewReader("name=test&score=1")), WithExpectBodyRegex("name=test&score=1"), ) err = hc.Check(context.TODO()) assert.Nil(t, err) } func TestHttpRequestHeaderWithoutInit(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) resp := new(bytes.Buffer) for key, value := range r.Header { fmt.Fprintf(resp, "%s=%s,", key, value) } w.Write(resp.Bytes()) })) defer ts.Close() hc := New( ts.URL, WithRequestHeader("Foo", []string{"Bar"}), WithExpectBodyRegex("Foo=\\[Bar\\]"), ) assert.Nil(t, hc.Check(context.TODO())) } wait4x-wait4x-7fb9e45/checker/influxdb/000077500000000000000000000000001507553353000200245ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/influxdb/influxdb.go000066400000000000000000000031531507553353000221700ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package influxdb provides the InfluxDB checker for the Wait4X application. package influxdb import ( "context" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "wait4x.dev/v3/checker" ) // InfluxDB is an InfluxDB checker type InfluxDB struct { serverURL string } // New creates a new InfluxDB checker func New(serverURL string) checker.Checker { i := &InfluxDB{ serverURL: serverURL, } return i } // Identity returns the identity of the InfluxDB checker func (i *InfluxDB) Identity() (string, error) { return i.serverURL, nil } // Check checks the InfluxDB connection func (i *InfluxDB) Check(ctx context.Context) error { // InfluxDB doesn't validate authentication params on Ping and Health requests. ic := influxdb2.NewClient(i.serverURL, "") defer ic.Close() res, err := ic.Ping(ctx) if !res { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the influxdb server", err, "address", i.serverURL, ) } return err } return nil } wait4x-wait4x-7fb9e45/checker/kafka/000077500000000000000000000000001507553353000172665ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/kafka/kafka.go000066400000000000000000000077241507553353000207040ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package kafka provides Kafka checker. package kafka import ( "context" "fmt" "net/url" "strings" "time" "github.com/segmentio/kafka-go" "github.com/segmentio/kafka-go/sasl" "github.com/segmentio/kafka-go/sasl/scram" "wait4x.dev/v3/checker" ) const ( // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 100 * time.Millisecond ) // Kafka represents Kafka checker type Kafka struct { dsn string } // New creates the Kafka checker func New(dsn string) checker.Checker { i := &Kafka{ dsn: dsn, } return i } // Identity returns the identity of the checker func (k *Kafka) Identity() (string, error) { _, _, _, broker, err := parseDSN(k.dsn) if err != nil { return "", fmt.Errorf("failed to parse DSN: %w", err) } return broker, nil } // parseDSN parses the DSN and returns the authentication mechanism, password, username, and broker address. // The DSN format is expected to be: // kafka://username:password@broker:port?authMechanism=scram-sha-512 func parseDSN(dsn string) (authMechanism, password, username, broker string, err error) { u, err := url.Parse(dsn) if err != nil { return "", "", "", "", fmt.Errorf("failed to parse DSN %q: %w", dsn, err) } if u.Scheme != "kafka" { return "", "", "", "", fmt.Errorf("invalid DSN scheme %q, expected 'kafka'", u.Scheme) } broker = u.Host if broker == "" { return "", "", "", "", fmt.Errorf("broker address is required in DSN %q", dsn) } username = u.User.Username() password, _ = u.User.Password() authMechanism = u.Query().Get("authMechanism") return } // Check checks Kafka connection func (k *Kafka) Check(ctx context.Context) (err error) { authMechanism, password, username, broker, err := parseDSN(k.dsn) if err != nil { return fmt.Errorf("failed to parse DSN %q: %w", k.dsn, err) } var saslMechanism sasl.Mechanism switch strings.ToUpper(authMechanism) { case scram.SHA256.Name(): saslMechanism, err = scram.Mechanism(scram.SHA256, username, password) case scram.SHA512.Name(): saslMechanism, err = scram.Mechanism(scram.SHA512, username, password) case "": saslMechanism = nil default: err = fmt.Errorf("unknown auth mechanism %q", authMechanism) } if err != nil { return fmt.Errorf("failed to create SASL mechanism: %w", err) } dialer := &kafka.Dialer{ SASLMechanism: saslMechanism, ClientID: "wait4x-kafka-checker", Timeout: DefaultConnectionTimeout, } conn, err := dialer.DialContext(ctx, "tcp", broker) if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the Kafka server", err, "broker", broker, "authMechanism", authMechanism, "username", username, "password", trimPassword(password), ) } return fmt.Errorf("failed to connect to Kafka broker %s: %w", broker, err) } defer conn.Close() // Use it as alternative to ping the broker _, err = conn.Brokers() if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to get Kafka broker list", err, "broker", broker, ) } return fmt.Errorf("failed to get Kafka broker list %s: %w", broker, err) } return nil } func trimPassword(password string) string { if len(password) > 4 { return password[:2] + strings.Repeat("*", len(password)-4) + password[len(password)-2:] } return strings.Repeat("*", len(password)) } wait4x-wait4x-7fb9e45/checker/kafka/kafka_test.go000066400000000000000000000050671507553353000217410ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package kafka provides a Kafka checker. package kafka import ( "context" "testing" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/kafka" "wait4x.dev/v3/checker" ) // KafkaSuite is a test suite for Kafka checker type KafkaSuite struct { suite.Suite container *kafka.KafkaContainer } // SetupSuite starts a Kafka container func (s *KafkaSuite) SetupSuite() { var err error s.container, err = kafka.Run( context.Background(), "confluentinc/confluent-local:7.5.0", testcontainers.WithLogger(log.TestLogger(s.T())), ) s.Require().NoError(err) } // TearDownSuite stops the Kafka container func (s *KafkaSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the Kafka checker func (s *KafkaSuite) TestIdentity() { chk := New("kafka://127.0.0.1:9093") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("127.0.0.1:9093", identity) } // TestInvalidIdentity tests the invalid identity of the Kafka checker func (s *KafkaSuite) TestInvalidIdentity() { chk := New("xxx://127.0.0.1:3306") _, err := chk.Identity() s.Assert().ErrorContains(err, "failed to parse DSN") } // TestValidConnection tests the invalid connection of the Kafka server func (s *KafkaSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("kafka://127.0.0.1:8075") s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidConnection tests the valid connection of the Kafka server func (s *KafkaSuite) TestValidConnection() { ctx := context.Background() bs, err := s.container.Brokers(ctx) s.T().Log("Kafka brokers:", bs, err) chk := New("kafka://" + bs[0]) s.Assert().NoError(chk.Check(ctx)) } // TestKafka runs the Kafka test suite func TestKafka(t *testing.T) { suite.Run(t, new(KafkaSuite)) } wait4x-wait4x-7fb9e45/checker/mock_checker.go000066400000000000000000000020541507553353000211560ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package checker import ( "context" "github.com/stretchr/testify/mock" ) // MockChecker is the struct that mocks the Checker. type MockChecker struct { mock.Mock } // Identity mocks the checker's identity func (mc *MockChecker) Identity() (string, error) { args := mc.Called() return args.String(0), args.Error(1) } // Check mocks the checker's check func (mc *MockChecker) Check(ctx context.Context) error { args := mc.Called(ctx) return args.Error(0) } wait4x-wait4x-7fb9e45/checker/mongodb/000077500000000000000000000000001507553353000176365ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/mongodb/mongodb.go000066400000000000000000000043601507553353000216150ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mongodb provides the MongoDB checker for the Wait4X application. package mongodb import ( "context" "errors" "regexp" "strings" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/x/mongo/driver/topology" "wait4x.dev/v3/checker" ) var hidePasswordRegexp = regexp.MustCompile(`^(mongodb://[^/:]+):[^:@]+@`) // MongoDB is a MongoDB checker type MongoDB struct { dsn string } // New creates a new MongoDB checker func New(dsn string) checker.Checker { i := &MongoDB{ dsn: dsn, } return i } // Identity returns the identity of the MongoDB checker func (m *MongoDB) Identity() (string, error) { cops := options.Client().ApplyURI(m.dsn) if len(cops.Hosts) == 0 { return "", errors.New("can't retrieve the checker identity") } return strings.Join(cops.Hosts, ","), nil } // Check checks the MongoDB connection func (m *MongoDB) Check(ctx context.Context) (err error) { // Creates a new Client and then initializes it using the Connect method. c, err := mongo.Connect(ctx, options.Client().ApplyURI(m.dsn)) if err != nil { return err } defer func(c *mongo.Client, ctx context.Context) { if merr := c.Disconnect(ctx); merr != nil { err = merr } }(c, ctx) // Ping the primary err = c.Ping(ctx, readpref.Primary()) if err != nil { if checker.IsConnectionRefused(err) || errors.Is(err, topology.ErrServerSelectionTimeout) { return checker.NewExpectedError( "failed to establish a connection to the MongoDB server", err, "dsn", hidePasswordRegexp.ReplaceAllString(m.dsn, `$1:***@`), ) } return err } return nil } wait4x-wait4x-7fb9e45/checker/mongodb/mongodb_test.go000066400000000000000000000051671507553353000226620ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mongodb provides the MongoDB checker for the Wait4X application. package mongodb import ( "context" "testing" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/mongodb" "wait4x.dev/v3/checker" ) // MongoDBSuite is a test suite for MongoDB checker type MongoDBSuite struct { suite.Suite container *mongodb.MongoDBContainer } // SetupSuite starts a MongoDB container func (s *MongoDBSuite) SetupSuite() { var err error s.container, err = mongodb.Run( context.Background(), "mongo:6", testcontainers.WithLogger(log.TestLogger(s.T())), ) s.Require().NoError(err) } // TearDownSuite stops the MongoDB container func (s *MongoDBSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the MongoDB checker func (s *MongoDBSuite) TestIdentity() { chk := New("mongodb://127.0.0.1:27017") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("127.0.0.1:27017", identity) } // TestInvalidIdentity tests the invalid identity of the MongoDB checker func (s *MongoDBSuite) TestInvalidIdentity() { chk := New("xxx://127.0.0.1:3306") _, err := chk.Identity() s.Assert().ErrorContains(err, "can't retrieve the checker identity") } // TestValidConnection tests the invalid connection of the MongoDB server func (s *MongoDBSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("mongodb://127.0.0.1:8080") s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidConnection tests the valid connection of the MongoDB server func (s *MongoDBSuite) TestValidConnection() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint) s.Assert().Nil(chk.Check(ctx)) } // TestMongoDB runs the MongoDB test suite func TestMongoDB(t *testing.T) { suite.Run(t, new(MongoDBSuite)) } wait4x-wait4x-7fb9e45/checker/mysql/000077500000000000000000000000001507553353000173565ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/mysql/mysql.go000066400000000000000000000052111507553353000210510ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mysql provides the MySQL checker for the Wait4X application. package mysql import ( "context" "database/sql" "fmt" "regexp" "github.com/go-sql-driver/mysql" "wait4x.dev/v3/checker" ) var hidePasswordRegexp = regexp.MustCompile(`^([^:]+):[^:@]+@`) const ( expectTableQuery = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '%s')" ) // MySQL is a MySQL checker type MySQL struct { dsn string expectTable string } // Option is a function that configures the MySQL checker type Option func(m *MySQL) // New creates a new MySQL checker func New(dsn string, opts ...Option) checker.Checker { m := &MySQL{ dsn: dsn, } // apply the list of options to MySQL for _, opt := range opts { opt(m) } return m } // WithExpectTable configures the table existence check func WithExpectTable(table string) Option { return func(m *MySQL) { m.expectTable = table } } // Identity returns the identity of the MySQL checker func (m *MySQL) Identity() (string, error) { cfg, err := mysql.ParseDSN(m.dsn) if err != nil { return "", fmt.Errorf("can't retrieve the checker identity: %w", err) } return cfg.Addr, nil } // Check checks the MySQL connection func (m *MySQL) Check(ctx context.Context) (err error) { db, err := sql.Open("mysql", m.dsn) if err != nil { return err } defer func(db *sql.DB) { if dberr := db.Close(); dberr != nil { err = dberr } }(db) err = db.PingContext(ctx) if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the mysql server", err, "dsn", hidePasswordRegexp.ReplaceAllString(m.dsn, `$1:***@`), ) } return err } // check if the table exists if option has been set if m.expectTable != "" { query := fmt.Sprintf(expectTableQuery, m.expectTable) var exists bool err = db.QueryRowContext(ctx, query).Scan(&exists) if err != nil { return err } if !exists { return checker.NewExpectedError( "table does not exist", nil, "table", m.expectTable, ) } } return nil } wait4x-wait4x-7fb9e45/checker/mysql/mysql_test.go000066400000000000000000000066731507553353000221250ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mysql provides the MySQL checker for the Wait4X application. package mysql import ( "context" "testing" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/testcontainers/testcontainers-go/wait" "wait4x.dev/v3/checker" ) // MySQLSuite is a test suite for MySQL checker type MySQLSuite struct { suite.Suite container *mysql.MySQLContainer } // SetupSuite starts a MySQL container func (s *MySQLSuite) SetupSuite() { var err error s.container, err = mysql.Run( context.Background(), "mysql:8.0.36", testcontainers.WithLogger(log.TestLogger(s.T())), testcontainers.WithWaitStrategy(wait.ForListeningPort("33060")), ) s.Require().NoError(err) } // TearDownSuite stops the MySQL container func (s *MySQLSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the MySQL checker func (s *MySQLSuite) TestIdentity() { chk := New("user:password@tcp(localhost:3306)/dbname?tls=skip-verify") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("localhost:3306", identity) } // TestInvalidIdentity tests the invalid identity of the MySQL checker func (s *MySQLSuite) TestInvalidIdentity() { chk := New("xxx://127.0.0.1:3306") _, err := chk.Identity() s.Assert().ErrorContains(err, "default addr for network 'xxx:/' unknown") } // TestValidConnection tests the valid connection of the MySQL server func (s *MySQLSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("user:password@tcp(localhost:8080)/dbname?tls=skip-verify") s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidAddress tests the valid address of the MySQL server func (s *MySQLSuite) TestValidAddress() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint) s.Assert().Nil(chk.Check(ctx)) } func (s *MySQLSuite) TestTableNotExists() { var expectedError *checker.ExpectedError ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint, WithExpectTable("not_existing_table")) s.Assert().ErrorAs(chk.Check(ctx), &expectedError) } func (s *MySQLSuite) TestExpectTable() { ctx := context.Background() _, _, err := s.container.Exec(ctx, []string{"mysql", "-u", "test", "-ptest", "-D", "test", "-e", "CREATE TABLE my_table (id INT)"}) s.Require().NoError(err) endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint, WithExpectTable("my_table")) s.Assert().Nil(chk.Check(ctx)) } // TestMySQL runs the MySQL test suite func TestMySQL(t *testing.T) { suite.Run(t, new(MySQLSuite)) } wait4x-wait4x-7fb9e45/checker/postgresql/000077500000000000000000000000001507553353000204145ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/postgresql/postgresql.go000066400000000000000000000054121507553353000231500ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package postgresql provides the PostgreSQL checker for the Wait4X application. package postgresql import ( "context" "database/sql" "fmt" "net/url" "regexp" "wait4x.dev/v3/checker" // Needed for the PostgreSQL driver _ "github.com/lib/pq" ) var hidePasswordRegexp = regexp.MustCompile(`^(postgres://[^/:]+):[^:@]+@`) const ( expectTableQuery = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '%s')" ) // Option is a function that configures the PostgreSQL checker type Option func(p *PostgreSQL) // PostgreSQL is a PostgreSQL checker type PostgreSQL struct { dsn string expectTable string } // New creates a new PostgreSQL checker func New(dsn string, opts ...Option) checker.Checker { p := &PostgreSQL{ dsn: dsn, } // apply the list of options to PostgreSQL for _, opt := range opts { opt(p) } return p } // WithExpectTable configures the table existence check func WithExpectTable(table string) Option { return func(p *PostgreSQL) { p.expectTable = table } } // Identity returns the identity of the PostgreSQL checker func (p *PostgreSQL) Identity() (string, error) { u, err := url.Parse(p.dsn) if err != nil { return "", fmt.Errorf("can't retrieve the checker identity: %w", err) } return u.Host, nil } // Check checks the PostgreSQL connection func (p *PostgreSQL) Check(ctx context.Context) (err error) { db, err := sql.Open("postgres", p.dsn) if err != nil { return err } defer func(db *sql.DB) { if dberr := db.Close(); dberr != nil { err = dberr } }(db) err = db.PingContext(ctx) if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the postgresql server", err, "dsn", hidePasswordRegexp.ReplaceAllString(p.dsn, `$1:***@`), ) } return err } // check if the table exists if option has been set if p.expectTable != "" { query := fmt.Sprintf(expectTableQuery, p.expectTable) var exists bool err = db.QueryRowContext(ctx, query).Scan(&exists) if err != nil { return err } if !exists { return checker.NewExpectedError( "table does not exist", nil, "table", p.expectTable, ) } } return nil } wait4x-wait4x-7fb9e45/checker/postgresql/postgresql_test.go000066400000000000000000000072221507553353000242100ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package postgresql provides the PostgreSQL checker for the Wait4X application. package postgresql import ( "context" "testing" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" "wait4x.dev/v3/checker" ) // PostgreSQLSuite is a test suite for PostgreSQL checker type PostgreSQLSuite struct { suite.Suite container *postgres.PostgresContainer } // SetupSuite starts a PostgreSQL container func (s *PostgreSQLSuite) SetupSuite() { var err error s.container, err = postgres.Run( context.Background(), "postgres:16-alpine", testcontainers.WithLogger(log.TestLogger(s.T())), testcontainers.WithWaitStrategy(wait.ForListeningPort("5432")), ) s.Require().NoError(err) } // TearDownSuite stops the PostgreSQL container func (s *PostgreSQLSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the PostgreSQL checker func (s *PostgreSQLSuite) TestIdentity() { chk := New("postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("1.2.3.4:5432", identity) } // TestInvalidIdentity tests the invalid identity of the PostgreSQL checker func (s *PostgreSQLSuite) TestInvalidIdentity() { chk := New("127.0.0.1:5432") _, err := chk.Identity() s.Assert().ErrorContains(err, "first path segment in URL cannot contain colon") } // TestValidConnection tests the valid connection of the PostgreSQL server func (s *PostgreSQLSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidAddress tests the valid address of the PostgreSQL server func (s *PostgreSQLSuite) TestValidAddress() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint + "sslmode=disable") s.Assert().Nil(chk.Check(ctx)) } func (s *PostgreSQLSuite) TestTableNotExists() { var expectedError *checker.ExpectedError ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint+"sslmode=disable", WithExpectTable("not_existing_table")) s.Assert().ErrorAs(chk.Check(ctx), &expectedError) } func (s *PostgreSQLSuite) TestExpectTable() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) _, _, err = s.container.Exec(ctx, []string{"psql", `postgresql://postgres:postgres@localhost:5432/postgres`, "-c", "CREATE TABLE my_table (id INT)"}) s.Require().NoError(err) chk := New(endpoint+"sslmode=disable", WithExpectTable("my_table")) s.Assert().Nil(chk.Check(ctx)) } // TestPostgreSQL runs the PostgreSQL test suite func TestPostgreSQL(t *testing.T) { suite.Run(t, new(PostgreSQLSuite)) } wait4x-wait4x-7fb9e45/checker/rabbitmq/000077500000000000000000000000001507553353000200125ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/rabbitmq/rabbitmq.go000066400000000000000000000074441507553353000221530ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package rabbitmq provides the RabbitMQ checker for the Wait4X application. package rabbitmq import ( "context" "crypto/tls" "fmt" "net" "regexp" "time" amqp "github.com/rabbitmq/amqp091-go" "wait4x.dev/v3/checker" ) var hidePasswordRegexp = regexp.MustCompile(`(amqp://[^/:]+):[^:@]+@`) // Option configures a RabbitMQ. type Option func(r *RabbitMQ) const ( // DefaultHeartbeat is the default heartbeat duration DefaultHeartbeat = 10 * time.Second // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 3 * time.Second // DefaultLocale is the default locale DefaultLocale = "en_US" // DefaultInsecureSkipTLSVerify is the default value for whether to skip tls verify DefaultInsecureSkipTLSVerify = false ) // RabbitMQ is a RabbitMQ checker type RabbitMQ struct { dsn string timeout time.Duration insecureSkipTLSVerify bool } // New creates a new RabbitMQ checker func New(dsn string, opts ...Option) checker.Checker { t := &RabbitMQ{ dsn: dsn, timeout: DefaultConnectionTimeout, insecureSkipTLSVerify: DefaultInsecureSkipTLSVerify, } // Apply options to RabbitMQ checker for _, opt := range opts { opt(t) } return t } // WithTimeout configures a timeout for establishing new connections func WithTimeout(timeout time.Duration) Option { return func(r *RabbitMQ) { r.timeout = timeout } } // WithInsecureSkipTLSVerify configures whether to skip tls verify func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option { return func(r *RabbitMQ) { r.insecureSkipTLSVerify = insecureSkipTLSVerify } } // Identity returns the identity of the RabbitMQ checker func (r *RabbitMQ) Identity() (string, error) { u, err := amqp.ParseURI(r.dsn) if err != nil { return "", fmt.Errorf("can't retrieve the checker identity: %w", err) } return fmt.Sprintf("%s:%d", u.Host, u.Port), nil } // Check checks the RabbitMQ connection func (r *RabbitMQ) Check(ctx context.Context) (err error) { conn, err := amqp.DialConfig( r.dsn, amqp.Config{ Heartbeat: DefaultHeartbeat, Locale: DefaultLocale, TLSClientConfig: &tls.Config{ InsecureSkipVerify: r.insecureSkipTLSVerify, }, Dial: func(network, addr string) (net.Conn, error) { d := net.Dialer{Timeout: r.timeout} conn, err := d.DialContext(ctx, network, addr) if err != nil { return nil, err } // Heartbeating hasn't started yet, don't stall forever on a dead server. // A deadline is set for TLS and AMQP handshaking. After AMQP is established, // the deadline is cleared in openComplete. if err := conn.SetDeadline(time.Now().Add(r.timeout)); err != nil { return nil, err } return conn, nil }, }, ) if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the rabbitmq server", err, "dsn", hidePasswordRegexp.ReplaceAllString(r.dsn, `$1:***@`), ) } return err } defer func(conn *amqp.Connection) { if connerr := conn.Close(); connerr != nil { err = connerr } }(conn) // Open a channel to check the connection. _, err = conn.Channel() if err != nil { return err } return nil } wait4x-wait4x-7fb9e45/checker/rabbitmq/rabbitmq_test.go000066400000000000000000000054141507553353000232050ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package rabbitmq provides the RabbitMQ checker for the Wait4X application. package rabbitmq import ( "context" "testing" "time" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/rabbitmq" "wait4x.dev/v3/checker" ) // RabbitMQSuite is a test suite for RabbitMQ checker type RabbitMQSuite struct { suite.Suite container *rabbitmq.RabbitMQContainer } // SetupSuite starts a RabbitMQ container func (s *RabbitMQSuite) SetupSuite() { var err error s.container, err = rabbitmq.Run( context.Background(), "rabbitmq:3.12.11-management-alpine", testcontainers.WithLogger(log.TestLogger(s.T())), ) s.Require().NoError(err) } // TearDownSuite stops the RabbitMQ container func (s *RabbitMQSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the RabbitMQ checker func (s *RabbitMQSuite) TestIdentity() { chk := New("amqp://guest:guest@127.0.0.1:5672/vhost") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("127.0.0.1:5672", identity) } // TestInvalidIdentity tests the invalid identity of the RabbitMQ checker func (s *RabbitMQSuite) TestInvalidIdentity() { chk := New("127.0.0.1:5672") _, err := chk.Identity() s.Assert().ErrorContains(err, `can't retrieve the checker identity: parse "127.0.0.1:5672"`) } // TestValidConnection tests the valid connection of the RabbitMQ server func (s *RabbitMQSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("amqp://user:pass@127.0.0.1:5672/vhost") s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidAddress tests the valid address of the RabbitMQ server func (s *RabbitMQSuite) TestValidConnection() { ctx := context.Background() endpoint, err := s.container.AmqpURL(ctx) s.Require().NoError(err) chk := New(endpoint, WithTimeout(5*time.Second), WithInsecureSkipTLSVerify(true)) s.Assert().Nil(chk.Check(ctx)) } // TestRabbitMQ runs the RabbitMQ test suite func TestRabbitMQ(t *testing.T) { suite.Run(t, new(RabbitMQSuite)) } wait4x-wait4x-7fb9e45/checker/redis/000077500000000000000000000000001507553353000173175ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/redis/redis.go000066400000000000000000000064761507553353000207710ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package redis provides the Redis checker for the Wait4X application. package redis import ( "context" "errors" "fmt" "regexp" "strings" "time" "github.com/go-redis/redis/v8" "wait4x.dev/v3/checker" ) var hidePasswordRegexp = regexp.MustCompile(`([^/]+//[^/:]+):[^:@]+@`) // Option configures a Redis. type Option func(r *Redis) const ( // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 3 * time.Second ) // Redis is a Redis checker type Redis struct { address string expectKey string timeout time.Duration } // New creates a new Redis checker func New(address string, opts ...Option) checker.Checker { r := &Redis{ address: address, timeout: DefaultConnectionTimeout, } // apply the list of options to Redis for _, opt := range opts { opt(r) } return r } // WithTimeout configures a timeout for establishing new connections func WithTimeout(timeout time.Duration) Option { return func(r *Redis) { r.timeout = timeout } } // WithExpectKey configures a key expectation func WithExpectKey(key string) Option { return func(r *Redis) { r.expectKey = key } } // Identity returns the identity of the Redis checker func (r *Redis) Identity() (string, error) { opts, err := redis.ParseURL(r.address) if err != nil { return "", fmt.Errorf("can't retrieve the checker identity: %w", err) } return opts.Addr, nil } // Check checks the Redis connection func (r *Redis) Check(ctx context.Context) error { opts, err := redis.ParseURL(r.address) if err != nil { return err } opts.DialTimeout = r.timeout client := redis.NewClient(opts) // Check Redis connection _, err = client.Ping(ctx).Result() if err != nil { if checker.IsConnectionRefused(err) { return checker.NewExpectedError( "failed to establish a connection to the redis server", err, "dsn", hidePasswordRegexp.ReplaceAllString(r.address, `$1:***@`), ) } return err } // It can connect to Redis successfully if r.expectKey == "" { return nil } splittedKey := strings.Split(r.expectKey, "=") keyHasValue := len(splittedKey) == 2 val, err := client.Get(ctx, splittedKey[0]).Result() if err != nil { if errors.Is(err, redis.Nil) { // Redis key does not exist. return checker.NewExpectedError("the key doesn't exist", nil, "key", splittedKey[0]) } // Error occurred on get Redis key return err } // The Redis key exists and user doesn't want to match value if !keyHasValue { return nil } // When the user expect a key with value matched, _ := regexp.MatchString(splittedKey[1], val) if matched { return nil } return checker.NewExpectedError( "the key and desired value doesn't exist", nil, "key", splittedKey[0], "actual", val, "expect", splittedKey[1], ) } wait4x-wait4x-7fb9e45/checker/redis/redis_test.go000066400000000000000000000066051507553353000220220ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package redis provides the Redis checker for the Wait4X application. package redis import ( "context" "testing" "time" "github.com/go-redis/redis/v8" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" redismodule "github.com/testcontainers/testcontainers-go/modules/redis" "wait4x.dev/v3/checker" ) // RedisSuite is a test suite for Redis checker type RedisSuite struct { suite.Suite container *redismodule.RedisContainer } // SetupSuite starts a Redis container func (s *RedisSuite) SetupSuite() { var err error s.container, err = redismodule.Run( context.Background(), "redis:7", testcontainers.WithLogger(log.TestLogger(s.T())), ) s.Require().NoError(err) } // TearDownSuite stops the Redis container func (s *RedisSuite) TearDownSuite() { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } // TestIdentity tests the identity of the Redis checker func (s *RedisSuite) TestIdentity() { chk := New("redis://127.0.0.1:8787") identity, err := chk.Identity() s.Require().NoError(err) s.Assert().Equal("127.0.0.1:8787", identity) } // TestInvalidIdentity tests the invalid identity of the Redis checker func (s *RedisSuite) TestInvalidIdentity() { chk := New("xxx://127.0.0.1:8787") _, err := chk.Identity() s.Assert().ErrorContains(err, "invalid URL scheme: xxx") } // TestValidConnection tests the valid connection of the Redis server func (s *RedisSuite) TestInvalidConnection() { var expectedError *checker.ExpectedError chk := New("redis://127.0.0.1:8787", WithTimeout(5*time.Second)) s.Assert().ErrorAs(chk.Check(context.Background()), &expectedError) } // TestValidAddress tests the valid address of the Redis server func (s *RedisSuite) TestValidAddress() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) chk := New(endpoint) s.Assert().Nil(chk.Check(ctx)) } // TestKeyExistence tests the key existence of the Redis server func (s *RedisSuite) TestKeyExistence() { ctx := context.Background() endpoint, err := s.container.ConnectionString(ctx) s.Require().NoError(err) opts, err := redis.ParseURL(endpoint) s.Require().NoError(err) redisClient := redis.NewClient(opts) redisClient.Set(ctx, "Foo", "Bar", time.Hour) chk := New(endpoint, WithExpectKey("Foo")) s.Assert().Nil(chk.Check(ctx)) chk = New(endpoint, WithExpectKey("Foo=^B.*$")) s.Assert().Nil(chk.Check(ctx)) var expectedError *checker.ExpectedError chk = New(endpoint, WithExpectKey("Foo=^b[A-Z]$")) s.Assert().ErrorAs(chk.Check(ctx), &expectedError) chk = New(endpoint, WithExpectKey("Bob")) s.Assert().ErrorAs(chk.Check(ctx), &expectedError) } // TestRedis runs the Redis test suite func TestRedis(t *testing.T) { suite.Run(t, new(RedisSuite)) } wait4x-wait4x-7fb9e45/checker/tcp/000077500000000000000000000000001507553353000167775ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/tcp/tcp.go000066400000000000000000000040441507553353000201160ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tcp provides the TCP checker for the Wait4X application. package tcp import ( "context" "net" "os" "time" "wait4x.dev/v3/checker" ) // Option configures a TCP checker type Option func(t *TCP) const ( // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 3 * time.Second ) // TCP is a TCP checker type TCP struct { address string timeout time.Duration } // New creates a new TCP checker func New(address string, opts ...Option) checker.Checker { t := &TCP{ address: address, timeout: DefaultConnectionTimeout, } // Apply the list of options to TCP for _, opt := range opts { opt(t) } return t } // WithTimeout configures a timeout for maximum amount of time a dial will wait for a connection to complete func WithTimeout(timeout time.Duration) Option { return func(t *TCP) { t.timeout = timeout } } // Identity returns the identity of the TCP checker func (t *TCP) Identity() (string, error) { return t.address, nil } // Check checks the TCP connection func (t *TCP) Check(ctx context.Context) error { d := net.Dialer{Timeout: t.timeout} _, err := d.DialContext(ctx, "tcp", t.address) if err != nil { if os.IsTimeout(err) { return checker.NewExpectedError("timed out while making a tcp call", err, "timeout", t.timeout) } else if checker.IsConnectionRefused(err) { return checker.NewExpectedError("failed to establish a tcp connection", err) } return err } return nil } wait4x-wait4x-7fb9e45/checker/tcp/tcp_test.go000066400000000000000000000211511507553353000211530ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tcp provides the TCP checker for the Wait4X application. package tcp import ( "context" "errors" "fmt" "net" "strconv" "testing" "time" "github.com/stretchr/testify/suite" "wait4x.dev/v3/checker" ) // TCPSuite is a test suite for TCP checker type TCPSuite struct { suite.Suite // Shared resources for the test suite listener net.Listener ipv6Listener net.Listener port int unusedPort int serverDone chan struct{} } // SetupSuite sets up test suite resources func (s *TCPSuite) SetupSuite() { // Set up a TCP server for tests that need an active connection var err error s.listener, err = net.Listen("tcp", "127.0.0.1:0") s.Require().NoError(err) // Parse the port _, portStr, err := net.SplitHostPort(s.listener.Addr().String()) s.Require().NoError(err) s.port, err = strconv.Atoi(portStr) s.Require().NoError(err) // Find an unused port for connection refused tests s.unusedPort = s.port + 1 // Set up a channel to track server completion s.serverDone = make(chan struct{}) // Handle connections in a goroutine go func() { defer close(s.serverDone) for { conn, err := s.listener.Accept() if err != nil { return // listener closed } if conn != nil { s.Require().NoError(conn.Close()) } } }() // Try to set up IPv6 listener if supported conn, err := net.Dial("udp", "[::1]:1") if err == nil { s.Require().NoError(conn.Close()) s.ipv6Listener, err = net.Listen("tcp", "[::1]:0") if err != nil { s.T().Log("IPv6 listener setup failed:", err) } else { // Handle IPv6 connections go func() { for { conn, err := s.ipv6Listener.Accept() if err != nil { return // listener closed } if conn != nil { s.Require().NoError(conn.Close()) } } }() } } } // TearDownSuite tears down test suite resources func (s *TCPSuite) TearDownSuite() { // Close listeners if s.listener != nil { s.Require().NoError(s.listener.Close()) <-s.serverDone // Wait for server goroutine to complete } if s.ipv6Listener != nil { s.Require().NoError(s.ipv6Listener.Close()) } } // TestNew checks the constructor with default and custom options func (s *TCPSuite) TestNew() { // Test default values tc := New("127.0.0.1:8080").(*TCP) s.Equal("127.0.0.1:8080", tc.address) s.Equal(DefaultConnectionTimeout, tc.timeout) // Test with options customTimeout := 5 * time.Second tc = New("127.0.0.1:8080", WithTimeout(customTimeout)).(*TCP) s.Equal("127.0.0.1:8080", tc.address) s.Equal(customTimeout, tc.timeout) } // TestWithTimeout tests the timeout option func (s *TCPSuite) TestWithTimeout() { tc := &TCP{timeout: DefaultConnectionTimeout} opt := WithTimeout(10 * time.Second) opt(tc) s.Equal(10*time.Second, tc.timeout) } // TestIdentity tests the Identity method func (s *TCPSuite) TestIdentity() { address := "127.0.0.1:8080" tc := New(address) identity, err := tc.Identity() s.NoError(err) s.Equal(address, identity) } // TestCheckSuccessful tests successful TCP connection func (s *TCPSuite) TestCheckSuccessful() { tc := New(s.listener.Addr().String()) err := tc.Check(context.Background()) s.NoError(err) } // TestCheckConnectionRefused tests connection refused error func (s *TCPSuite) TestCheckConnectionRefused() { address := fmt.Sprintf("127.0.0.1:%d", s.unusedPort) tc := New(address, WithTimeout(500*time.Millisecond)) err := tc.Check(context.Background()) // The error should be an ExpectedError s.Error(err) var expectedErr *checker.ExpectedError s.True(errors.As(err, &expectedErr)) s.Contains(expectedErr.Error(), "failed to establish a tcp connection") } // TestCheckInvalidAddress tests invalid address format func (s *TCPSuite) TestCheckInvalidAddress() { tc := New("invalid-address") err := tc.Check(context.Background()) // This should be a generic error, not an ExpectedError s.Error(err) var expectedErr *checker.ExpectedError s.True(errors.As(err, &expectedErr)) } // TestCheckTimeout tests timeout behavior func (s *TCPSuite) TestCheckTimeout() { // Use a black-hole IP that will cause timeout tc := New("240.0.0.1:12345", WithTimeout(500*time.Millisecond)) start := time.Now() err := tc.Check(context.Background()) elapsed := time.Since(start) // Verify the timeout was respected s.Error(err) s.True(elapsed >= 500*time.Millisecond, "Timeout was not respected") // Check error type and details var expectedErr *checker.ExpectedError if s.True(errors.As(err, &expectedErr)) { s.Contains(expectedErr.Error(), "timed out while making a tcp call") details := expectedErr.Details() s.Equal("timeout", details[0]) s.Equal(500*time.Millisecond, details[1]) } } // TestCheckContextCancellation tests context cancellation func (s *TCPSuite) TestCheckContextCancellation() { ctx, cancel := context.WithCancel(context.Background()) // Use a black-hole IP to ensure the operation would take time tc := New("240.0.0.1:12345", WithTimeout(10*time.Second)) // Cancel the context after a short delay go func() { time.Sleep(100 * time.Millisecond) cancel() }() start := time.Now() err := tc.Check(ctx) elapsed := time.Since(start) s.Error(err) s.True(elapsed < 5*time.Second, "Context cancellation was not respected") s.ErrorIs(err, context.Canceled) } // TestCheckNameResolution tests name resolution errors func (s *TCPSuite) TestCheckNameResolution() { tc := New("non-existent-domain.example:12345", WithTimeout(500*time.Millisecond)) err := tc.Check(context.Background()) s.Error(err) var expectedErr *checker.ExpectedError s.True(errors.As(err, &expectedErr), "Name resolution error should be wrapped as an ExpectedError") } // TestCheckIPv6Address tests IPv6 support func (s *TCPSuite) TestCheckIPv6Address() { if s.ipv6Listener == nil { s.T().Skip("IPv6 not available on this system") } tc := New(s.ipv6Listener.Addr().String()) err := tc.Check(context.Background()) s.NoError(err, "Should be able to connect to IPv6 address") } // TestTableDriven defines table-driven tests for various scenarios func (s *TCPSuite) TestTableDriven() { // Create a context with a reasonable timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Define test cases tests := []struct { name string address string timeout time.Duration ctx context.Context shouldError bool errorType string // "expected", "other", or "" if no error }{ { name: "Valid Address", address: fmt.Sprintf("127.0.0.1:%d", s.port), timeout: 1 * time.Second, ctx: ctx, shouldError: false, }, { name: "Connection Refused", address: fmt.Sprintf("127.0.0.1:%d", s.unusedPort), timeout: 1 * time.Second, ctx: ctx, shouldError: true, errorType: "expected", // ExpectedError }, { name: "Very Short Timeout", address: "240.0.0.1:12345", // non-routable IP, will time out timeout: 1 * time.Millisecond, // ultra short timeout ctx: ctx, shouldError: true, errorType: "expected", // ExpectedError for timeout }, { name: "Invalid Address Format", address: "not-a-valid-address", timeout: 1 * time.Second, ctx: ctx, shouldError: true, errorType: "expected", }, } // Run all test cases for _, tt := range tests { s.Run(tt.name, func() { tc := New(tt.address, WithTimeout(tt.timeout)) err := tc.Check(tt.ctx) if tt.shouldError { s.Error(err) var expectedErr *checker.ExpectedError isExpectedErr := errors.As(err, &expectedErr) if tt.errorType == "expected" { s.True(isExpectedErr, "Expected an ExpectedError but got: %v", err) } else if tt.errorType == "other" { s.False(isExpectedErr, "Expected a non-ExpectedError") } } else { s.NoError(err) } }) } } // Helper method to get a cancelled context func (s *TCPSuite) getCancelledContext() context.Context { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately return ctx } // TestTCPSuite runs the test suite func TestTCPSuite(t *testing.T) { suite.Run(t, new(TCPSuite)) } wait4x-wait4x-7fb9e45/checker/temporal/000077500000000000000000000000001507553353000200345ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/checker/temporal/temporal.go000066400000000000000000000167141507553353000222170ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package temporal provides the Temporal checker for the Wait4X application. package temporal import ( "context" "crypto/tls" "errors" "net" "os" "regexp" "time" "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/health/grpc_health_v1" "wait4x.dev/v3/checker" ) // Option configures a Temporal checker type Option func(t *Temporal) // CheckMode specifies the check mode type CheckMode string const ( // DefaultConnectionTimeout is the default connection timeout duration DefaultConnectionTimeout = 3 * time.Second // DefaultInsecureTransport is the default insecure transport security DefaultInsecureTransport = false // DefaultInsecureSkipTLSVerify is the default insecure skip tls verify DefaultInsecureSkipTLSVerify = false // CheckModeServer is the "server" check mode CheckModeServer = "server" // CheckModeWorker is the "worker" check mode CheckModeWorker = "worker" ) var ( // ErrInvalidMode defines invalid mode error ErrInvalidMode = errors.New("invalid checkMode provided") // ErrNoNamespace defines no namespace error ErrNoNamespace = errors.New(`no namespace provided (use temporal.WithNamespace("__namespace__"))`) // ErrNoTaskQueue defines no task queue error ErrNoTaskQueue = errors.New(`no task queue provided (use temporal.WithTaskQueue("__task_queue__"))`) ) // Temporal is a Temporal checker type Temporal struct { checkMode CheckMode target string timeout time.Duration namespace string taskQueue string insecureTransport bool insecureSkipTLSVerify bool expectWorkerIdentityRegex string } // New creates a new Temporal checker func New(checkMode CheckMode, target string, opts ...Option) checker.Checker { t := &Temporal{ checkMode: checkMode, target: target, timeout: DefaultConnectionTimeout, insecureTransport: DefaultInsecureTransport, insecureSkipTLSVerify: DefaultInsecureSkipTLSVerify, } // apply the list of options to Temporal for _, opt := range opts { opt(t) } return t } // WithTimeout configures a timeout for maximum amount of time a dial will wait for a GRPC connection to complete func WithTimeout(timeout time.Duration) Option { return func(t *Temporal) { t.timeout = timeout } } // WithNamespace configures the Temporal namespace that is mandatory for the CheckModeWorker func WithNamespace(namespace string) Option { return func(t *Temporal) { t.namespace = namespace } } // WithTaskQueue configures the Temporal task queue that is mandatory for the CheckModeWorker func WithTaskQueue(taskQueue string) Option { return func(t *Temporal) { t.taskQueue = taskQueue } } // WithInsecureTransport disables transport security func WithInsecureTransport(insecureTransport bool) Option { return func(t *Temporal) { t.insecureTransport = insecureTransport } } // WithInsecureSkipTLSVerify configures insecure skip tls verify func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option { return func(t *Temporal) { t.insecureSkipTLSVerify = insecureSkipTLSVerify } } // WithExpectWorkerIdentityRegex configures worker (Poller) identity expectation that is mandatory for the CheckModeWorker func WithExpectWorkerIdentityRegex(expectWorkerIdentityRegex string) Option { return func(t *Temporal) { t.expectWorkerIdentityRegex = expectWorkerIdentityRegex } } // Identity returns the identity of the Temporal checker func (t *Temporal) Identity() (string, error) { return t.target, nil } // Check checks the Temporal connection func (t *Temporal) Check(ctx context.Context) (err error) { conn, err := t.getGRPCConn() if err != nil { return err } defer func(conn *grpc.ClientConn) { if connErr := conn.Close(); connErr != nil { err = connErr } }(conn) switch t.checkMode { case CheckModeWorker: if t.namespace == "" { return ErrNoNamespace } if t.taskQueue == "" { return ErrNoTaskQueue } return t.checkWorker(ctx, conn) case CheckModeServer: return t.checkServer(ctx, conn) default: return ErrInvalidMode } } // getGRPCConn gets a GRPC connection func (t *Temporal) getGRPCConn() (*grpc.ClientConn, error) { opts := []grpc.DialOption{ grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { d := net.Dialer{Timeout: t.timeout} return d.DialContext(ctx, "tcp", addr) }), } if t.insecureTransport { opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) } else { opts = append( opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: t.insecureSkipTLSVerify})), ) } conn, err := grpc.NewClient(t.target, opts...) if err != nil { if os.IsTimeout(err) { return nil, checker.NewExpectedError("timed out while making a grpc call", err) } else if checker.IsConnectionRefused(err) { return nil, checker.NewExpectedError("failed to establish a grpc connection", err) } return nil, err } return conn, nil } // checkServer checks the Temporal server func (t *Temporal) checkServer(ctx context.Context, conn grpc.ClientConnInterface) error { healthClient := grpc_health_v1.NewHealthClient(conn) req := &grpc_health_v1.HealthCheckRequest{ Service: "temporal.api.workflowservice.v1.WorkflowService", } resp, err := healthClient.Check(ctx, req) if err != nil { return checker.NewExpectedError("failed to health check", err) } if resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING { return checker.NewExpectedError( "health check returned unhealthy", nil, "status", resp.GetStatus(), ) } return nil } // checkWorker checks the Temporal worker func (t *Temporal) checkWorker(ctx context.Context, conn grpc.ClientConnInterface) error { client := workflowservice.NewWorkflowServiceClient(conn) req := &workflowservice.DescribeTaskQueueRequest{ Namespace: t.namespace, TaskQueue: &taskqueue.TaskQueue{ Name: t.taskQueue, }, TaskQueueType: enums.TASK_QUEUE_TYPE_WORKFLOW, } resp, err := client.DescribeTaskQueue(ctx, req) if err != nil { return checker.NewExpectedError( "failed to describe the task queue", err, ) } if len(resp.Pollers) == 0 { return checker.NewExpectedError("no worker (poller) registered", nil) } if t.expectWorkerIdentityRegex != "" { workerMatched := false for _, poller := range resp.Pollers { matched, err := regexp.MatchString(t.expectWorkerIdentityRegex, poller.Identity) if err != nil { return checker.NewExpectedError("failed to match string", err) } if matched { workerMatched = true } } if !workerMatched { return checker.NewExpectedError( "the worker (poller) hasn't registered yet", nil, "pattern", t.expectWorkerIdentityRegex, ) } } return nil } wait4x-wait4x-7fb9e45/checker/utils.go000066400000000000000000000020611507553353000176770ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package checker import ( "errors" "net" "syscall" ) // IsConnectionRefused attempts to determine if the given error was caused by a failure to establish a connection. func IsConnectionRefused(err error) bool { for err != nil { switch t := err.(type) { case *net.OpError: if t.Op == "dial" || t.Op == "read" { return true } case syscall.Errno: if t == syscall.ECONNREFUSED { return true } } err = errors.Unwrap(err) } return false } wait4x-wait4x-7fb9e45/cmd/000077500000000000000000000000001507553353000153505ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/cmd/wait4x/000077500000000000000000000000001507553353000165705ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/cmd/wait4x/main.go000066400000000000000000000014501507553353000200430ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the Wait4X application. package main import ( "wait4x.dev/v3/internal/cmd" ) // main is the main function for the Wait4X application func main() { cmd.Execute() } wait4x-wait4x-7fb9e45/doc.go000066400000000000000000000022371507553353000157050ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package wait4x provides functionality to wait for services and ports to become available // or reach specific states. It supports configurable timeout durations and polling intervals, // making it suitable for service dependency management, health checking, and integration testing. // // Features: // - Port availability checking // - Service state monitoring // - Configurable timeout and interval settings // - Support for multiple protocols and service types // // For more information and examples, visit: https://wait4x.dev package wait4x // import "wait4x.dev/v3" wait4x-wait4x-7fb9e45/docker-bake.hcl000066400000000000000000000012521507553353000174440ustar00rootroot00000000000000// Special target: https://github.com/docker/metadata-action#bake-definition target "docker-metadata-action" {} target "image" { inherits = ["docker-metadata-action"] platforms = [ "linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64", "linux/ppc64le", "linux/s390x" ] } target "artifact" { target = "artifact" output = ["./dist"] platforms = [ "linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64", "linux/mips", "linux/mipsle", "linux/mips64", "linux/mips64le", "linux/ppc64le", "linux/s390x", "windows/amd64", "windows/arm64", "darwin/amd64", "darwin/arm64" ] } wait4x-wait4x-7fb9e45/examples/000077500000000000000000000000001507553353000164235ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/000077500000000000000000000000001507553353000172045ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/README.md000066400000000000000000000061071507553353000204670ustar00rootroot00000000000000# Wait4X as an Importable Package These examples demonstrate how to use Wait4X as an importable package in your Go applications. Wait4X isn't just a CLI tool - it provides a powerful library that you can integrate directly into your Go code. ## Examples Overview 1. **Basic TCP Checker** (`tcp_basic/main.go`): Simple example of waiting for a TCP port to become available. 2. **Advanced HTTP Checker** (`http_advanced/main.go`): Demonstrates complex HTTP checking with custom headers, body validations, status code checks, and more. 3. **Parallel Service Checking** (`parallel_services/main.go`): Shows how to check multiple services in parallel, waiting for all of them to be ready before proceeding. 4. **Reverse Checking** (`reverse_checking/main.go`): Example of using the inverse check to wait for a port to become free. 5. **Custom Checker** (`custom_checker/main.go`): Shows how to create your own custom checker by implementing the Checker interface. ## Using Wait4X in Your Go Projects To use Wait4X in your Go project, add it as a dependency: ```bash go get wait4x.dev/v3 ``` Then import the packages you need: ```go import ( "wait4x.dev/v3/checker/tcp" // TCP checker "wait4x.dev/v3/checker/http" // HTTP checker "wait4x.dev/v3/checker/redis" // Redis checker // ...other checkers "wait4x.dev/v3/waiter" // Waiter functionality ) ``` ### Core Components 1. **Checkers**: Implements the `checker.Checker` interface: ```go type Checker interface { Identity() (string, error) Check(ctx context.Context) error } ``` 2. **Waiter**: Provides waiting functionality with options like timeout, interval, backoff, etc. 3. **Context Usage**: All checkers and waiters support context for cancellation and timeouts. ### Common Patterns 1. **Option Pattern**: All checkers use the functional options pattern for configuration. 2. **Error Handling**: Use the `ExpectedError` type for expected failures vs. unexpected errors. 3. **Parallel Execution**: Use `WaitParallelContext` to check multiple services simultaneously. 4. **Context Propagation**: Always pass context to allow for proper cancellation and timeouts. ## Extending Wait4X To create your own checker: 1. Define a type that implements the `checker.Checker` interface 2. Implement the `Identity()` and `Check()` methods 3. Use the `checker.NewExpectedError()` function for creating appropriate error types See `custom_checker.go` for a complete example of implementing a custom checker. ## Best Practices 1. Always use contexts with timeouts to prevent indefinite waiting 2. Consider using exponential backoff for services that might take a while to start 3. Use parallel checking when waiting for multiple independent services 4. Handle errors appropriately - distinguish between timeout errors and other errors 5. Add logging where appropriate to understand what's happening during waiting ## Additional Resources - Go Reference Documentation: https://pkg.go.dev/wait4x.dev/v3 - GitHub Repository: https://github.com/wait4x/wait4x - Report Issues: https://github.com/wait4x/wait4x/issueswait4x-wait4x-7fb9e45/examples/pkg/custom_checker/000077500000000000000000000000001507553353000222025ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/custom_checker/main.go000066400000000000000000000070421507553353000234600ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the Custom Checker example. package main import ( "context" "errors" "fmt" "log" "os" "time" "wait4x.dev/v3/checker" "wait4x.dev/v3/waiter" ) // FileChecker checks if a file exists and meets criteria type FileChecker struct { filePath string minSize int64 permissions os.FileMode } // NewFileChecker creates a new file checker func NewFileChecker(filePath string, minSize int64, permissions os.FileMode) *FileChecker { return &FileChecker{ filePath: filePath, minSize: minSize, permissions: permissions, } } // Identity returns the identity of the checker func (f *FileChecker) Identity() (string, error) { return fmt.Sprintf("file(%s)", f.filePath), nil } // Check verifies the file exists and meets the criteria func (f *FileChecker) Check(ctx context.Context) error { // Check if context is done select { case <-ctx.Done(): return ctx.Err() default: // Continue checking } // Check if file exists fileInfo, err := os.Stat(f.filePath) if err != nil { if os.IsNotExist(err) { return checker.NewExpectedError("file does not exist", err, "path", f.filePath) } return err } // Check file size if minimum size specified if f.minSize > 0 && fileInfo.Size() < f.minSize { return checker.NewExpectedError( "file is smaller than expected", nil, "path", f.filePath, "actual_size", fileInfo.Size(), "expected_min_size", f.minSize, ) } // Check permissions if specified if f.permissions != 0 { actualPerms := fileInfo.Mode().Perm() if actualPerms&f.permissions != f.permissions { return checker.NewExpectedError( "file has incorrect permissions", nil, "path", f.filePath, "actual_permissions", actualPerms, "expected_permissions", f.permissions, ) } } return nil } // main is the main function for the Custom Checker example func main() { // Create a context with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Create our custom file checker // This will check that a file: // 1. Exists // 2. Is at least 1024 bytes (1KB) in size // 3. Has read permission for everyone fileChecker := NewFileChecker( "/tmp/application.log", 1024, // 1KB minimum size os.FileMode(0444), // Read permission for all ) // Wait for the file to be ready fmt.Println("Waiting for log file to be created with correct size and permissions...") err := waiter.WaitContext( ctx, fileChecker, waiter.WithTimeout(time.Minute), waiter.WithInterval(2*time.Second), ) if err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Fatalf("Timed out waiting for file: %v", err) } log.Fatalf("Error waiting for file: %v", err) } fmt.Println("File is ready!") // Now we could proceed with reading or processing the file processLogFile(fileChecker.filePath) } func processLogFile(path string) { fmt.Printf("Processing log file at %s...\n", path) // Your file processing code here } wait4x-wait4x-7fb9e45/examples/pkg/http_advanced/000077500000000000000000000000001507553353000220105ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/http_advanced/main.go000066400000000000000000000043361507553353000232710ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the HTTP Advanced example. package main import ( "context" "fmt" "log" "net/http" "strings" "time" httpChecker "wait4x.dev/v3/checker/http" "wait4x.dev/v3/waiter" ) // main is the main function for the HTTP Advanced example func main() { // Create a context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create custom HTTP headers headers := http.Header{} headers.Add("Authorization", "Bearer my-token") headers.Add("Content-Type", "application/json") // Prepare a request body requestBody := strings.NewReader(`{"query": "status"}`) // Create an HTTP checker with multiple validations checker := httpChecker.New( "https://api.example.com/health", httpChecker.WithTimeout(5*time.Second), httpChecker.WithExpectStatusCode(200), httpChecker.WithExpectBodyJSON("status"), // Check that 'status' field exists in JSON httpChecker.WithExpectBodyRegex(`"healthy":\s*true`), // Regex to check response httpChecker.WithExpectHeader("Content-Type=application/json"), httpChecker.WithRequestHeaders(headers), httpChecker.WithRequestBody(requestBody), httpChecker.WithInsecureSkipTLSVerify(true), // Skip TLS verification ) // Wait for the API to be available and responding correctly fmt.Println("Waiting for API health endpoint...") err := waiter.WaitContext( ctx, checker, waiter.WithTimeout(2*time.Minute), waiter.WithInterval(5*time.Second), waiter.WithBackoffPolicy(waiter.BackoffPolicyExponential), ) if err != nil { log.Fatalf("API health check failed: %v", err) } fmt.Println("API is healthy and ready!") } wait4x-wait4x-7fb9e45/examples/pkg/parallel_services/000077500000000000000000000000001507553353000227035ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/parallel_services/main.go000066400000000000000000000052351507553353000241630ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the Parallel Services example. package main import ( "context" "fmt" "log" "time" "github.com/go-logr/stdr" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/http" "wait4x.dev/v3/checker/postgresql" "wait4x.dev/v3/checker/redis" "wait4x.dev/v3/waiter" ) // main is the main function for the Parallel Services example func main() { // Set up a logger stdr.SetVerbosity(4) // Set log level logger := stdr.New(log.New(log.Writer(), "[Wait4X] ", log.LstdFlags|log.Lshortfile)) // Create a context with timeout for the entire operation ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() // Create checkers for different services checkers := []checker.Checker{ // Redis checker redis.New( "redis://localhost:6379", redis.WithTimeout(5*time.Second), redis.WithExpectKey("app:status=ready"), // Check if key exists with specific value ), // PostgreSQL checker postgresql.New( "postgres://postgres:password@localhost:5432/app_db?sslmode=disable", ), // HTTP API checker http.New( "http://localhost:8080/health", http.WithTimeout(3*time.Second), http.WithExpectStatusCode(200), http.WithExpectBodyJSON("status.healthy"), ), } // Set up common options for all waiters waitOptions := []waiter.Option{ waiter.WithTimeout(time.Minute), waiter.WithInterval(2 * time.Second), waiter.WithBackoffPolicy(waiter.BackoffPolicyExponential), waiter.WithBackoffCoefficient(1.5), waiter.WithBackoffExponentialMaxInterval(15 * time.Second), waiter.WithLogger(logger), } // Wait for all services in parallel fmt.Println("Waiting for all required services to be available...") err := waiter.WaitParallelContext(ctx, checkers, waitOptions...) if err != nil { log.Fatalf("Failed waiting for services: %v", err) } fmt.Println("All services are ready!") // Continue with application startup startApplication() } // startApplication is a helper function to start the application func startApplication() { fmt.Println("Starting application...") // Your application code here } wait4x-wait4x-7fb9e45/examples/pkg/reverse_checking/000077500000000000000000000000001507553353000225125ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/reverse_checking/main.go000066400000000000000000000037431507553353000237740ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the Reverse Checking example. package main import ( "context" "fmt" "log" "time" "wait4x.dev/v3/checker/tcp" "wait4x.dev/v3/waiter" ) // main is the main function for the Reverse Checking example func main() { // Create a context with a timeout ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() // Port to check port := "localhost:8080" // Create a TCP checker for the port tcpChecker := tcp.New(port, tcp.WithTimeout(2*time.Second)) fmt.Printf("Waiting for port %s to become free...\n", port) // Use invert check to wait until the TCP connection fails (port is free) err := waiter.WaitContext( ctx, tcpChecker, waiter.WithTimeout(45*time.Second), waiter.WithInterval(3*time.Second), // The InvertCheck option is key here - it inverts the success condition // So we wait until the checker fails (port is closed) waiter.WithInvertCheck(true), ) if err != nil { log.Fatalf("Failed waiting for port to become free: %v", err) } fmt.Printf("Port %s is now free!\n", port) // Example: Now that the port is free, start our own service on it startServiceOnPort(port) } // startServiceOnPort is a helper function to start a service on the given port func startServiceOnPort(port string) { fmt.Printf("Starting new service on port %s...\n", port) // Your code to start a service on the now-free port } wait4x-wait4x-7fb9e45/examples/pkg/tcp_basic/000077500000000000000000000000001507553353000211335ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/examples/pkg/tcp_basic/main.go000066400000000000000000000036011507553353000224060ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package main is the main package for the TCP Basic example. package main import ( "context" "fmt" "log" "time" "wait4x.dev/v3/checker/tcp" "wait4x.dev/v3/waiter" ) // main is the main function for the TCP Basic example func main() { // Create a context with a 30-second timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Create a TCP checker for localhost:6379 with a 5-second connection timeout tcpChecker := tcp.New("localhost:6379", tcp.WithTimeout(5*time.Second)) // Specify waiter options options := []waiter.Option{ waiter.WithTimeout(time.Minute), // Total wait timeout waiter.WithInterval(2 * time.Second), // Time between retry attempts waiter.WithBackoffPolicy("exponential"), // Use exponential backoff waiter.WithBackoffCoefficient(2.0), // Double the wait time each retry waiter.WithBackoffExponentialMaxInterval(10 * time.Second), // Max 10s between retries } // Wait for the TCP port to be available fmt.Println("Waiting for Redis to be available on port 6379...") err := waiter.WaitContext(ctx, tcpChecker, options...) if err != nil { log.Fatalf("Failed waiting for Redis: %v", err) } fmt.Println("Redis is available!") } wait4x-wait4x-7fb9e45/flake.lock000066400000000000000000000027331507553353000165460ustar00rootroot00000000000000{ "nodes": { "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1731533236, "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1750215678, "narHash": "sha256-Rc/ytpamXRf6z8UA2SGa4aaWxUXRbX2MAWIu2C8M+ok=", "owner": "nixos", "repo": "nixpkgs", "rev": "5395fb3ab3f97b9b7abca147249fa2e8ed27b192", "type": "github" }, "original": { "owner": "nixos", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } wait4x-wait4x-7fb9e45/flake.nix000066400000000000000000000021651507553353000164130ustar00rootroot00000000000000{ description = "Wait4X allows you to wait for a port or a service to enter the requested state."; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils, }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; packageName = "wait4x"; version = "${self.shortRev or self.dirtyShortRev or "dirty"}"; in { formatter = pkgs.alejandra; devShells.default = pkgs.mkShell { name = packageName; buildInputs = with pkgs; [ go gotools golint gopls revive golangci-lint delve git gh gnumake ]; }; packages.default = pkgs.buildGoModule { pname = packageName; inherit version; src = self; vendorHash = "sha256-ODcHrmmHHeZbi1HVDkYPCyHs7mcs2UGdBzicP1+eOSI="; doCheck = false; nativeBuildInputs = with pkgs; [git]; GOCACHE = "$(mktemp -d)"; }; }); } wait4x-wait4x-7fb9e45/go.mod000066400000000000000000000127041507553353000157170ustar00rootroot00000000000000module wait4x.dev/v3 go 1.23.0 toolchain go1.23.5 require ( github.com/antchfx/htmlquery v1.3.4 github.com/go-logr/logr v1.4.3 github.com/go-logr/stdr v1.2.2 github.com/go-logr/zerologr v1.2.3 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.9.3 github.com/influxdata/influxdb-client-go/v2 v2.14.0 github.com/lib/pq v1.10.9 github.com/miekg/dns v1.1.68 github.com/rabbitmq/amqp091-go v1.10.0 github.com/rs/zerolog v1.34.0 github.com/segmentio/kafka-go v0.4.48 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.0 github.com/testcontainers/testcontainers-go v0.38.0 github.com/testcontainers/testcontainers-go/modules/kafka v0.38.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.38.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.38.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.38.0 github.com/testcontainers/testcontainers-go/modules/redis v0.38.0 github.com/tidwall/gjson v1.18.0 github.com/tonglil/buflogr v1.1.1 go.mongodb.org/mongo-driver v1.17.4 go.temporal.io/api v1.51.0 google.golang.org/grpc v1.71.0 mvdan.cc/sh/v3 v3.12.0 ) require ( dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/antchfx/xpath v1.3.3 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdelapenya/tlscert v0.2.0 // 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.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.18 // 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-20240221224432-82ca36839d55 // indirect github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp 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.38.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sync v0.14.0 // indirect golang.org/x/tools v0.33.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/fatih/color v1.18.0 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v1.0.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect ) wait4x-wait4x-7fb9e45/go.sum000066400000000000000000001140671507553353000157510ustar00rootroot00000000000000dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/IBM/sarama v1.42.1 h1:wugyWa15TDEHh2kvq2gAy1IHLjEjuYOYgXz/ruC/OSQ= github.com/IBM/sarama v1.42.1/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ= github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM= github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs= github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 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/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4= github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d h1:fjMbDVUGsMQiVZnSQsmouYJvMdwsGiDipOZoN66v844= github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 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.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 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/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 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-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs= github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= github.com/shirou/gopsutil/v4 v4.25.5/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/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= github.com/testcontainers/testcontainers-go/modules/kafka v0.38.0 h1:ZZpiVK2V2sArn0fv2s/jaQdGwOgNf8JvVxnLQL1JEPY= github.com/testcontainers/testcontainers-go/modules/kafka v0.38.0/go.mod h1:XB6IGYbw+KqegO10jqLe5NoxIe1aW9FKdj2f+G8fUcQ= github.com/testcontainers/testcontainers-go/modules/mongodb v0.38.0 h1:A+YGYRoNLjDcYYnupsZBj3O3OfgEnS/o/MbQjiTqQwo= github.com/testcontainers/testcontainers-go/modules/mongodb v0.38.0/go.mod h1:4PMThrMlJpuUqLG+sCca3pWJKuReeQGioszuESf+uO0= github.com/testcontainers/testcontainers-go/modules/mysql v0.38.0 h1:msUPAl0LVBalG3m2KhmbFHeRrxCw36xmQFCEhzqsvqo= github.com/testcontainers/testcontainers-go/modules/mysql v0.38.0/go.mod h1:PFyaiqBahyh1BMz23ij99z4LJGsDpkpuZKz6rchlUWc= github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 h1:KFdx9A0yF94K70T6ibSuvgkQQeX1xKlZVF3hEagXEtY= github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0/go.mod h1:T/QRECND6N6tAKMxF1Za+G2tpwnGEHcODzHRsgIpw9M= github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.38.0 h1:FqwAf/NluzqpwlKNOx17iXA2VQWusAnXwwviZs7t1lE= github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.38.0/go.mod h1:28+n/mRaV0F4J09rSkEzOEzqbGv1KTcoxJi/DdswP8g= github.com/testcontainers/testcontainers-go/modules/redis v0.38.0 h1:289pn0BFmGqDrd6BrImZAprFef9aaPZacx07YOQaPV4= github.com/testcontainers/testcontainers-go/modules/redis v0.38.0/go.mod h1:EcKPWRzOglnQfYe+ekA8RPEIWSNJTGwaC5oE5bQV+D0= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tonglil/buflogr v1.1.1 h1:CKAjOHBSMmgbRFxpn/RhQHPj5oANc7ekhlsoUDvcZIg= github.com/tonglil/buflogr v1.1.1/go.mod h1:WLLtPRLqcFYWQLbA+ytXy5WrFTYnfA+beg1MpvJCxm4= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.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= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= 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.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 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.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= 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.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 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.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.temporal.io/api v1.51.0 h1:9+e14GrIa7nWoWoudqj/PSwm33yYjV+u8TAR9If7s/g= go.temporal.io/api v1.51.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 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= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 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= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= wait4x-wait4x-7fb9e45/internal/000077500000000000000000000000001507553353000164215ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/cmd/000077500000000000000000000000001507553353000171645ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/cmd/dns/000077500000000000000000000000001507553353000177505ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/cmd/dns/a.go000066400000000000000000000060611507553353000205220ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/internal/contextutil" dns "wait4x.dev/v3/checker/dns/a" "wait4x.dev/v3/waiter" ) // NewACommand creates a new DNS A command func NewACommand() *cobra.Command { command := &cobra.Command{ Use: "A ADDRESS [-- command [args...]]", Aliases: []string{"a"}, Short: "Check DNS A records for a given domain", Long: "Check DNS A records to verify domain name resolution to IPv4 addresses. Supports checking against expected IPs and custom nameservers.", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the A command") } return nil }, Example: ` # Check A records existence for a domain wait4x dns A wait4x.dev # Check A records with specific expected IPv4 addresses wait4x dns A wait4x.dev --expect-ip 172.67.154.180 wait4x dns A wait4x.dev --expect-ip 172.67.154.180 --expect-ip 104.21.60.85 # Check A records using a custom nameserver wait4x dns A wait4x.dev --expect-ip 172.67.154.180 -n gordon.ns.cloudflare.com # Check A records with timeout and interval settings wait4x dns A wait4x.dev --timeout 30s --interval 5s # Invert the check (wait until records don't match) wait4x dns A wait4x.dev --expect-ip 172.67.154.180 --invert-check`, RunE: runA, } command.Flags().StringArray("expect-ip", []string{}, "Expected IPv4 addresses to match against A records") return command } // runA is the command handler for the "dns A" command func runA(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("failed to parse --nameserver flag: %w", err) } expectIPs, err := cmd.Flags().GetStringArray("expect-ip") if err != nil { return fmt.Errorf("failed to parse --expect-ip flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedIPV4s(expectIPs), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/a_test.go000066400000000000000000000037711507553353000215660ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewACommand tests the NewACommand function func TestNewACommand(t *testing.T) { cmd := NewACommand() assert.NotNil(t, cmd) assert.Equal(t, "A ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"a"}, cmd.Aliases) } // TestACommand_NoArgs tests the ACommand with no arguments func TestACommand_NoArgs(t *testing.T) { cmd := NewACommand() err := cmd.Args(cmd, []string{}) assert.Error(t, err) assert.Equal(t, "ADDRESS is required argument for the A command", err.Error()) } // TestACommand_WithArgs tests the ACommand with arguments func TestACommand_WithArgs(t *testing.T) { cmd := NewACommand() err := cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) } // TestRunA tests the ACommand with different flags and arguments func TestRunA(t *testing.T) { tests := []struct { name string args []string flags map[string]string }{ { name: "basic check", args: []string{"example.com"}, }, { name: "with expected IP", args: []string{"example.com"}, flags: map[string]string{ "expect-ip": "93.184.216.34", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := NewACommand() for flag, value := range tt.flags { err := cmd.Flags().Set(flag, value) assert.NoError(t, err) } err := cmd.Args(cmd, tt.args) assert.NoError(t, err) }) } } wait4x-wait4x-7fb9e45/internal/cmd/dns/aaaa.go000066400000000000000000000061431507553353000211660ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" dns "wait4x.dev/v3/checker/dns/aaaa" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewAAAACommand creates a new DNS AAAA command func NewAAAACommand() *cobra.Command { command := &cobra.Command{ Use: "AAAA ADDRESS [-- command [args...]]", Aliases: []string{"aaaa"}, Short: "Check DNS AAAA (IPv6) records for a given domain", Long: "Check for the existence and validity of DNS AAAA (IPv6) records for a specified domain. Supports verification against expected IPv6 addresses and custom nameserver configuration.", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the AAAA command") } return nil }, Example: ` # Check AAAA records existence wait4x dns AAAA wait4x.dev # Check AAAA records with expected IPv6 addresses wait4x dns AAAA wait4x.dev --expect-ip '2606:4700:3033::ac43:9ab4' # Check AAAA records with multiple expected IPv6 addresses wait4x dns AAAA wait4x.dev --expect-ip '2606:4700:3033::ac43:9ab4' --expect-ip '2606:4700:3034::ac43:9ab4' # Check AAAA records using a specific nameserver wait4x dns AAAA wait4x.dev --expect-ip '2606:4700:3033::ac43:9ab4' --nameserver gordon.ns.cloudflare.com # Check AAAA records with custom interval and timeout wait4x dns AAAA wait4x.dev --interval 5s --timeout 60s`, RunE: runAAAA, } command.Flags().StringArray("expect-ip", nil, "Expect ipv6s.") return command } // runAAAA is the command handler for the "dns AAAA" command func runAAAA(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("failed to parse --nameserver flag: %w", err) } expectIPs, err := cmd.Flags().GetStringArray("expect-ip") if err != nil { return fmt.Errorf("failed to parse --expect-ip flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedIPV6s(expectIPs), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/aaaa_test.go000066400000000000000000000040651507553353000222260ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewAAAACommand tests the NewAAAACommand function func TestNewAAAACommand(t *testing.T) { cmd := NewAAAACommand() assert.Equal(t, "AAAA ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"aaaa"}, cmd.Aliases) assert.Equal(t, "Check DNS AAAA (IPv6) records for a given domain", cmd.Short) err := cmd.Args(cmd, []string{}) assert.Error(t, err) assert.Equal(t, "ADDRESS is required argument for the AAAA command", err.Error()) err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) flags := cmd.Flags() expectIP, err := flags.GetStringArray("expect-ip") assert.NoError(t, err) assert.Empty(t, expectIP) } // TestRunAAAA tests the AAAACommand with different flags and arguments func TestRunAAAA(t *testing.T) { cmd := NewAAAACommand() err := cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) cmd.Flags().Set("expect-ip", "2606:4700:3033::ac43:9ab4") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) cmd.Flags().Set("nameserver", "8.8.8.8") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) cmd.Flags().Set("interval", "1s") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) cmd.Flags().Set("timeout", "5s") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) cmd.Flags().Set("invert-check", "true") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) } wait4x-wait4x-7fb9e45/internal/cmd/dns/cname.go000066400000000000000000000055071507553353000213710ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" dns "wait4x.dev/v3/checker/dns/cname" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewCNAMECommand creates a new DNS CNAME command func NewCNAMECommand() *cobra.Command { command := &cobra.Command{ Use: "CNAME ADDRESS [-- command [args...]]", Aliases: []string{"cname"}, Short: "Check DNS CNAME records for a given domain", Long: "Check DNS CNAME records and optionally verify expected domain names", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the CNAME command") } return nil }, Example: ` # Check CNAME record existence wait4x dns CNAME example.com # Check CNAME records with expected domains wait4x dns CNAME example.com --expected-domain target.example.com # Check CNAME record using a specific nameserver wait4x dns CNAME example.com --expected-domain target.example.com -n 8.8.8.8 # Check CNAME record with custom timeout and interval wait4x dns CNAME example.com --timeout 30s --interval 5s `, RunE: runCNAME, } command.Flags().StringArray("expect-domain", nil, "Expect domains.") return command } // runCNAME is the command handler for the "dns CNAME" command func runCNAME(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("failed to parse --nameserver flag: %w", err) } expectDomains, err := cmd.Flags().GetStringArray("expect-domain") if err != nil { return fmt.Errorf("failed to parse --expect-domain flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedDomains(expectDomains), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/cname_test.go000066400000000000000000000034141507553353000224230ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewCNAMECommand tests the NewCNAMECommand function func TestNewCNAMECommand(t *testing.T) { cmd := NewCNAMECommand() assert.Equal(t, "CNAME ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"cname"}, cmd.Aliases) assert.Equal(t, "Check DNS CNAME records for a given domain", cmd.Short) err := cmd.Args(cmd, []string{}) assert.Error(t, err) assert.Equal(t, "ADDRESS is required argument for the CNAME command", err.Error()) err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) flags := cmd.Flags() expectDomain, err := flags.GetStringArray("expect-domain") assert.NoError(t, err) assert.Empty(t, expectDomain) } // TestRunCNAME tests the CNAMECommand with different flags and arguments func TestRunCNAME(t *testing.T) { cmd := NewCNAMECommand() cmd.Flags().Duration("interval", 0, "") cmd.Flags().Duration("timeout", 0, "") cmd.Flags().Bool("invert-check", false, "") cmd.Flags().String("nameserver", "", "") err := cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) } wait4x-wait4x-7fb9e45/internal/cmd/dns/dns.go000066400000000000000000000024671507553353000210740ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "github.com/spf13/cobra" ) // NewDNSCommand creates a new DNS command func NewDNSCommand() *cobra.Command { command := &cobra.Command{ Use: "dns", Long: "Check DNS records for various types like A, AAAA, CNAME, MX, TXT, and NS", Short: "Check DNS records", } command.PersistentFlags().StringP("nameserver", "n", "", "Nameserver to use for the DNS query (e.g. 8.8.8.8:53)") command.AddCommand(NewACommand()) command.AddCommand(NewAAAACommand()) command.AddCommand(NewCNAMECommand()) command.AddCommand(NewMXCommand()) command.AddCommand(NewTXTCommand()) command.AddCommand(NewNSCommand()) return command } wait4x-wait4x-7fb9e45/internal/cmd/dns/mx.go000066400000000000000000000062501507553353000207260ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" dns "wait4x.dev/v3/checker/dns/mx" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewMXCommand creates a new DNS MX command func NewMXCommand() *cobra.Command { command := &cobra.Command{ Use: "MX ADDRESS [-- command [args...]]", Aliases: []string{"mx"}, Short: "Check DNS MX (mail exchanger) records for a given domain", Long: "Check for the existence and validity of DNS MX (mail exchanger) records for a specified domain. MX records specify the mail servers responsible for receiving email on behalf of the domain.", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the MX command") } return nil }, Example: ` # Check MX records existence wait4x dns MX wait4x.dev # Check MX records with expected domains wait4x dns MX wait4x.dev --expect-domain 'route1.mx.cloudflare.net' # Check MX records with multiple expected domains wait4x dns MX wait4x.dev --expect-domain 'route1.mx.cloudflare.net' --expect-domain 'route2.mx.cloudflare.net' # Check MX records by defined nameserver wait4x dns MX wait4x.dev --expect-domain 'route1.mx.cloudflare.net' --nameserver 'gordon.ns.cloudflare.com' # Check MX records with custom timeout and interval wait4x dns MX wait4x.dev --timeout 30s --interval 5s`, RunE: runMX, } command.Flags().StringArray("expect-domain", nil, "Expected domain names in MX records. Can be specified multiple times for multiple domains.") return command } // runMX is the command handler for the "dns MX" command func runMX(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("unable to parse --nameserver flag: %w", err) } expectDomains, err := cmd.Flags().GetStringArray("expect-domain") if err != nil { return fmt.Errorf("unable to parse --expect-domain flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedDomains(expectDomains), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/mx_test.go000066400000000000000000000025621507553353000217670ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewMXCommand tests the NewMXCommand function func TestNewMXCommand(t *testing.T) { cmd := NewMXCommand() assert.Equal(t, "MX ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"mx"}, cmd.Aliases) assert.Equal(t, "Check DNS MX (mail exchanger) records for a given domain", cmd.Short) err := cmd.Args(cmd, []string{}) assert.EqualError(t, err, "ADDRESS is required argument for the MX command") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) flags := cmd.Flags() expectDomain, err := flags.GetStringArray("expect-domain") assert.NoError(t, err) assert.Empty(t, expectDomain) } wait4x-wait4x-7fb9e45/internal/cmd/dns/ns.go000066400000000000000000000061351507553353000207240ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" dns "wait4x.dev/v3/checker/dns/ns" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewNSCommand creates a new DNS NS command func NewNSCommand() *cobra.Command { command := &cobra.Command{ Use: "NS ADDRESS [-- command [args...]]", Aliases: []string{"ns"}, Short: "Check DNS NS records for a given domain", Long: "Check DNS NS records for a given domain name and verify nameserver records", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the NS command") } return nil }, Example: ` # Check NS records existence wait4x dns NS wait4x.dev # Check NS records with expected nameservers wait4x dns NS wait4x.dev --expect-nameserver 'emma.ns.cloudflare.com' # Check NS records with multiple expected nameservers wait4x dns NS wait4x.dev --expect-nameserver 'emma.ns.cloudflare.com' --expect-nameserver 'gordon.ns.cloudflare.com' # Check NS records using a specific nameserver wait4x dns NS wait4x.dev --nameserver '8.8.8.8:53' # Check NS records with timeout and interval wait4x dns NS wait4x.dev --timeout 60s --interval 5s # Invert the check (wait until NS records don't match) wait4x dns NS wait4x.dev --expect-nameserver 'emma.ns.cloudflare.com' --invert-check`, RunE: runNS, } command.Flags().StringArray("expect-nameserver", nil, "Expect nameservers.") return command } // runNS is the command handler for the "dns NS" command func runNS(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("failed to parse --nameserver flag: %w", err) } expectNameservers, err := cmd.Flags().GetStringArray("expect-nameserver") if err != nil { return fmt.Errorf("failed to parse --expect-nameserver flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedNameservers(expectNameservers), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/ns_test.go000066400000000000000000000025551507553353000217650ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewNSCommand tests the NewNSCommand function func TestNewNSCommand(t *testing.T) { cmd := NewNSCommand() assert.Equal(t, "NS ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"ns"}, cmd.Aliases) assert.Equal(t, "Check DNS NS records for a given domain", cmd.Short) err := cmd.Args(cmd, []string{}) assert.EqualError(t, err, "ADDRESS is required argument for the NS command") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) flags := cmd.Flags() expectNameserver, err := flags.GetStringArray("expect-nameserver") assert.NoError(t, err) assert.Empty(t, expectNameserver) } wait4x-wait4x-7fb9e45/internal/cmd/dns/txt.go000066400000000000000000000057401507553353000211240ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" dns "wait4x.dev/v3/checker/dns/txt" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewTXTCommand creates a new DNS TXT command func NewTXTCommand() *cobra.Command { command := &cobra.Command{ Use: "TXT ADDRESS [-- command [args...]]", Aliases: []string{"txt"}, Short: "Check DNS TXT records for a given domain", Long: "Check DNS TXT records for a given domain name and verify TXT records", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the TXT command") } return nil }, Example: ` # Check TXT records existence wait4x dns TXT wait4x.dev # Check TXT records with expected values wait4x dns TXT wait4x.dev --expect-value 'include:_spf.mx.cloudflare.net' # Check TXT records by defined nameserver wait4x dns TXT wait4x.dev --expect-value 'include:_spf.mx.cloudflare.net' --nameserver gordon.ns.cloudflare.com # Check TXT records with multiple expected values wait4x dns TXT wait4x.dev --expect-value 'v=spf1' --expect-value 'include:_spf.mx.cloudflare.net' # Check TXT records with timeout and interval wait4x dns TXT wait4x.dev --timeout 60s --interval 5s`, RunE: runTXT, } command.Flags().StringArray("expect-value", nil, "Expected TXT record values") return command } // runTXT is the command handler for the "dns TXT" command func runTXT(cmd *cobra.Command, args []string) error { nameserver, err := cmd.Flags().GetString("nameserver") if err != nil { return fmt.Errorf("failed to parse --nameserver flag: %w", err) } expectValues, err := cmd.Flags().GetStringArray("expect-value") if err != nil { return fmt.Errorf("failed to parse --expect-value flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } dc := dns.New( args[0], dns.WithExpectedValues(expectValues), dns.WithNameServer(nameserver), ) return waiter.WaitContext(cmd.Context(), dc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/dns/txt_test.go000066400000000000000000000025461507553353000221640ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package dns provides the DNS command-line interface for the Wait4X application. package dns import ( "testing" "github.com/stretchr/testify/assert" ) // TestNewTXTCommand tests the NewTXTCommand function func TestNewTXTCommand(t *testing.T) { cmd := NewTXTCommand() assert.Equal(t, "TXT ADDRESS [-- command [args...]]", cmd.Use) assert.Equal(t, []string{"txt"}, cmd.Aliases) assert.Equal(t, "Check DNS TXT records for a given domain", cmd.Short) err := cmd.Args(cmd, []string{}) assert.EqualError(t, err, "ADDRESS is required argument for the TXT command") err = cmd.Args(cmd, []string{"example.com"}) assert.NoError(t, err) flags := cmd.Flags() expectValue, err := flags.GetStringArray("expect-value") assert.NoError(t, err) assert.Empty(t, expectValue) } wait4x-wait4x-7fb9e45/internal/cmd/exec.go000066400000000000000000000057421507553353000204470ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "mvdan.cc/sh/v3/shell" "wait4x.dev/v3/checker/exec" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewExecCommand creates a new exec sub-command func NewExecCommand() *cobra.Command { execCommand := &cobra.Command{ Use: "exec COMMAND [ARGS...] [flags]", Short: "Check command execution", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("COMMAND is required argument for the exec command") } return nil }, Example: ` # Wait for a command to exit with code 0 wait4x exec "ls /tmp" # Wait for a command to exit with a specific code wait4x exec "ls /nonexistent" --exit-code 2 # Enable exponential backoff retry wait4x exec "bash ./some-script.sh" --exit-code 0 --backoff-policy exponential --backoff-exponential-max-interval 120s --timeout 120s`, RunE: runExec, } execCommand.Flags().Int("exit-code", 0, "Expected exit code from the command") return execCommand } // runExec runs the exec command func runExec(cmd *cobra.Command, args []string) error { exitCode, _ := cmd.Flags().GetInt("exit-code") logger, err := logr.FromContext(cmd.Context()) if err != nil { return err } if len(args) == 0 { return fmt.Errorf("no command specified") } // Split the command into parts using shell parser commandParts, err := shell.Fields(args[0], nil) if err != nil { return err } command := commandParts[0] var commandArgs []string if len(commandParts) > 1 { commandArgs = append(commandArgs, commandParts[1:]...) } if len(args) > 1 { commandArgs = append(commandArgs, args[1:]...) } checker := exec.New(command, exec.WithArgs(commandArgs), exec.WithExpectExitCode(exitCode), ) return waiter.WaitContext( cmd.Context(), checker, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval( contextutil.GetBackoffExponentialMaxInterval(cmd.Context()), ), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/http.go000066400000000000000000000176741507553353000205110ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "bufio" "errors" "fmt" "io" nethttp "net/http" "net/textproto" "net/url" "strings" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/http" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewHTTPCommand creates a new http sub-command func NewHTTPCommand() *cobra.Command { httpCommand := &cobra.Command{ Use: "http ADDRESS... [flags] [-- command [args...]]", Short: "Check HTTP connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the http command") } _, err := url.Parse(args[0]) if err != nil { return err } return nil }, Example: ` # If you want checking just http connection wait4x http https://ifconfig.co # If you want checking http connection and expect specify http status code wait4x http https://ifconfig.co --expect-status-code 200 # If you want to check a http response header # NOTE: the value in the expected header is regex. # Sample response header: Authorization Token 1234ABCD # You can match it by these ways: # Full key value: wait4x http https://ifconfig.co --expect-header "Authorization=Token 1234ABCD" # Value starts with: wait4x http https://ifconfig.co --expect-header "Authorization=Token" # Regex value: wait4x http https://ifconfig.co --expect-header "Authorization=Token\s.+" # Body JSON: wait4x http https://ifconfig.co/json --expect-body-json "user_agent.product" To know more about JSON syntax https://github.com/tidwall/gjson/blob/master/SYNTAX.md # Body XPath: wait4x http https://www.kernel.org/ --expect-body-xpath "//*[@id="tux-gear"]" # Request headers: wait4x http https://ifconfig.co --request-header "Content-Type: application/json" --request-header "Authorization: Token 123" # Post request (application/x-www-form-urlencoded): wait4x http https://httpbin.org/post --request-header "Content-Type: application/x-www-form-urlencoded" --request-body 'key=value&name=test' # Post request (application/json): wait4x http https://httpbin.org/post --request-header "Content-Type: application/json" --request-body '{"key": "value", "name": "test"}' # Disable auto redirect wait4x http https://www.wait4x.dev --expect-status-code 301 --no-redirect # Enable exponential backoff retry wait4x http https://ifconfig.co --expect-status-code 200 --backoff-policy exponential --backoff-exponential-max-interval 120s --timeout 120s # Self-signed certificates wait4x http https://www.wait4x.dev --cert-file /path/to/certfile --key-file /path/to/keyfile # CA file wait4x http https://www.wait4x.dev --ca-file /path/to/cafile`, RunE: runHTTP, } httpCommand.Flags().Int("expect-status-code", 0, "Expect response code e.g. 200, 204, ... .") httpCommand.Flags().String("expect-body-regex", "", "Expect response body pattern.") httpCommand.Flags().String("expect-body-json", "", "Expect response body JSON pattern.") httpCommand.Flags().String("expect-body-xpath", "", "Expect response body XPath pattern.") httpCommand.Flags().String("expect-header", "", "Expect response header pattern.") httpCommand.Flags().StringArray("request-header", nil, "User request headers.") httpCommand.Flags().String("request-body", "", "User request body.") httpCommand.Flags(). Duration("connection-timeout", http.DefaultConnectionTimeout, "Http connection timeout, The timeout includes connection time, any redirects, and reading the response body.") httpCommand.Flags(). Bool("insecure-skip-tls-verify", http.DefaultInsecureSkipTLSVerify, "Skips tls certificate checks for the HTTPS request.") httpCommand.Flags(). Bool("no-redirect", http.DefaultNoRedirect, "Do not follow HTTP 3xx redirects.") httpCommand.Flags(). String("ca-file", "", "Use this CA bundle to authenticate certificates of servers with HTTPS enabled.") httpCommand.Flags(). String("cert-file", "", "Utilize this SSL certificate file to identify the HTTPS client.") httpCommand.Flags(). String("key-file", "", "Utilize this SSL key file to identify the HTTPS client.") httpCommand.Flags().Bool("h2c", false, "Enable HTTP/2 cleartext (h2c) for http:// URLs.") return httpCommand } func runHTTP(cmd *cobra.Command, args []string) error { expectStatusCode, _ := cmd.Flags().GetInt("expect-status-code") expectBodyRegex, _ := cmd.Flags().GetString("expect-body-regex") expectBodyJSON, _ := cmd.Flags().GetString("expect-body-json") expectBodyXPath, _ := cmd.Flags().GetString("expect-body-xpath") expectHeader, _ := cmd.Flags().GetString("expect-header") requestRawHeaders, _ := cmd.Flags().GetStringArray("request-header") requestBody, _ := cmd.Flags().GetString("request-body") connectionTimeout, _ := cmd.Flags().GetDuration("connection-timeout") insecureSkipTLSVerify, _ := cmd.Flags().GetBool("insecure-skip-tls-verify") noRedirect, _ := cmd.Flags().GetBool("no-redirect") caFile, _ := cmd.Flags().GetString("ca-file") certFile, _ := cmd.Flags().GetString("cert-file") keyFile, _ := cmd.Flags().GetString("key-file") h2c, _ := cmd.Flags().GetBool("h2c") logger, err := logr.FromContext(cmd.Context()) if err != nil { return err } // Validate cert and key files. if (certFile != "" && keyFile == "") || (keyFile != "" && certFile == "") { return fmt.Errorf( "both certFile and keyFile should be assigned values, not just one of them", ) } // Convert raw headers (e.g. 'a: b') into a http Header. var requestHeaders nethttp.Header if len(requestRawHeaders) > 0 { rawHTTPHeaders := strings.Join(requestRawHeaders, "\r\n") tpReader := textproto.NewReader( bufio.NewReader(strings.NewReader(rawHTTPHeaders + "\r\n\n")), ) MIMEHeaders, err := tpReader.ReadMIMEHeader() if err != nil { return fmt.Errorf("can't parse the request header: %w", err) } requestHeaders = nethttp.Header(MIMEHeaders) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } // Request body. var requestBodyReader io.Reader if len(requestBody) != 0 { requestBodyReader = strings.NewReader(requestBody) } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = http.New(arg, http.WithExpectStatusCode(expectStatusCode), http.WithExpectBodyRegex(expectBodyRegex), http.WithExpectBodyJSON(expectBodyJSON), http.WithExpectBodyXPath(expectBodyXPath), http.WithExpectHeader(expectHeader), http.WithRequestHeaders(requestHeaders), http.WithRequestBody(requestBodyReader), http.WithTimeout(connectionTimeout), http.WithInsecureSkipTLSVerify(insecureSkipTLSVerify), http.WithNoRedirect(noRedirect), http.WithCAFile(caFile), http.WithCertFile(certFile), http.WithKeyFile(keyFile), http.WithH2C(h2c), ) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval( contextutil.GetBackoffExponentialMaxInterval(cmd.Context()), ), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/http_test.go000066400000000000000000000077501507553353000215420ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "testing" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "wait4x.dev/v3/internal/test" "github.com/stretchr/testify/assert" ) func TestHTTPCommandInvalidArgument(t *testing.T) { rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand(rootCmd, "http") assert.Equal(t, "ADDRESS is required argument for the http command", err.Error()) } func TestHTTPCommandInvalidAddress(t *testing.T) { rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand(rootCmd, "http", "http://local host") assert.Contains(t, err.Error(), "invalid character \" \" in host name") } func TestHTTPConnectionSuccess(t *testing.T) { rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand(rootCmd, "http", "https://wait4x.dev") assert.Nil(t, err) } func TestHTTPConnectionSuccessThenExecuteCommand(t *testing.T) { rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand(rootCmd, "http", "https://wait4x.dev", "--", "date") assert.Nil(t, err) } func TestHTTPConnectionFail(t *testing.T) { rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand(rootCmd, "http", "http://not-exists-doomain.tld", "-t", "2s") assert.Equal(t, context.DeadlineExceeded, err) } func TestHTTPRequestHeaderSuccess(t *testing.T) { hts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) resp := new(bytes.Buffer) for key, value := range r.Header { _, err := fmt.Fprintf(resp, "%s=%s,", key, value) assert.Nil(t, err) } _, err := w.Write(resp.Bytes()) assert.Nil(t, err) })) defer hts.Close() rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand( rootCmd, "http", hts.URL, "--request-header", "X-Foo: value1", "--request-header", "X-Foo: value2", "--request-header", "X-Bar: long \n value", "--expect-body-regex", "(.*X-Foo=\\[value1 value2\\].*X-Bar=\\[long value\\].*)|(.*X-Bar=\\[long value\\].*X-Foo=\\[value1 value2\\].*)", ) assert.Nil(t, err) } func TestHTTPRequestHeaderFail(t *testing.T) { hts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) defer hts.Close() rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand( rootCmd, "http", hts.URL, "--request-header", "X-Bar: long value\n\r", ) assert.Contains(t, err.Error(), "can't parse the request header") } func TestHTTPH2CFlagSuccess(t *testing.T) { t.Setenv("HTTP_PROXY", "") t.Setenv("NO_PROXY", "*") h2cOnly := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor != 2 { http.Error(w, "h2c required", http.StatusHTTPVersionNotSupported) return } w.WriteHeader(http.StatusOK) }) srv := httptest.NewUnstartedServer(h2c.NewHandler(h2cOnly, &http2.Server{})) srv.Start() defer srv.Close() rootCmd := NewRootCommand() rootCmd.AddCommand(NewHTTPCommand()) _, err := test.ExecuteCommand( rootCmd, "http", srv.URL, "--h2c", "--no-redirect", "--expect-status-code", "200", "-t", "2s", ) assert.Nil(t, err) } wait4x-wait4x-7fb9e45/internal/cmd/influxdb.go000066400000000000000000000046471507553353000213410ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_influxdb // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/influxdb" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewInfluxDBCommand creates a new influxdb sub-command func NewInfluxDBCommand() *cobra.Command { influxdbCommand := &cobra.Command{ Use: "influxdb SERVER_URL... [flags] [-- command [args...]]", Short: "Check InfluxDB connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("SERVER_URL is required argument for the influxdb command") } return nil }, Example: ` # Checking InfluxDB connection wait4x influxdb http://localhost:8086 `, RunE: runInfluxDB, } return influxdbCommand } func runInfluxDB(cmd *cobra.Command, args []string) error { logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = influxdb.New(arg) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/influxdb_disabled.go000066400000000000000000000021231507553353000231530ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_influxdb // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "github.com/spf13/cobra" ) // NewInfluxDBCommand creates a new influxdb sub-command func NewInfluxDBCommand() *cobra.Command { return &cobra.Command{ Use: "influxdb", Short: "Check InfluxDB connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("Influxdb feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/kafka.go000066400000000000000000000045301507553353000205720ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_kafka package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/kafka" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewKafkaCommand creates the kafka sub-command func NewKafkaCommand() *cobra.Command { kafkaCommand := &cobra.Command{ Use: "kafka DSN... [flags] [-- command [args...]]", Short: "Check Kafka connection", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("DSN is required argument for the kafka command") } return nil }, Example: ` # Checking Kafka connection with credentials wait4x kafka 'kafka://user:pass@127.0.0.1:9090/?authMechanism=scram-sha-256' `, RunE: runKafka, } return kafkaCommand } func runKafka(cmd *cobra.Command, args []string) error { logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = kafka.New(arg) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/kafka_disabled.go000066400000000000000000000017621507553353000224250ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_kafka package cmd import ( "errors" "github.com/spf13/cobra" ) // NewKafkaCommand creates the kafka sub-command func NewKafkaCommand() *cobra.Command { return &cobra.Command{ Use: "kafka", Short: "Check Kafka connection - this feature is disabled", RunE: func(cmd *cobra.Command, args []string) error { return errors.New("Kafka feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/mongodb.go000066400000000000000000000050361507553353000211440ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_mongodb // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/mongodb" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewMongoDBCommand creates the mongodb sub-command func NewMongoDBCommand() *cobra.Command { mongodbCommand := &cobra.Command{ Use: "mongodb DSN... [flags] [-- command [args...]]", Short: "Check MongoDB connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("DSN is required argument for the mongodb command") } return nil }, Example: ` # Checking MongoDB connection wait4x mongodb 'mongodb://127.0.0.1:27017' # Checking MongoDB connection with credentials and options wait4x mongodb 'mongodb://user:pass@127.0.0.1:27017/?maxPoolSize=20&w=majority' `, RunE: runMongoDB, } return mongodbCommand } func runMongoDB(cmd *cobra.Command, args []string) error { logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = mongodb.New(arg) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/mongodb_disabled.go000066400000000000000000000021141507553353000227650ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_mongodb // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "github.com/spf13/cobra" ) // NewMongoDBCommand creates a new mongodb sub-command func NewMongoDBCommand() *cobra.Command { return &cobra.Command{ Use: "mongodb", Short: "Check MongoDB connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("MongoDB feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/mysql.go000066400000000000000000000054241507553353000206650ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_mysql // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/mysql" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewMysqlCommand creates a new mysql sub-command func NewMysqlCommand() *cobra.Command { mysqlCommand := &cobra.Command{ Use: "mysql DSN... [flags] [-- command [args...]]", Short: "Check MySQL connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("DSN is required argument for the mysql command") } return nil }, Example: ` # Checking MySQL TCP connection wait4x mysql user:password@tcp(localhost:5555)/dbname?tls=skip-verify # Checking MySQL UNIX Socket existence wait4x mysql username:password@unix(/tmp/mysql.sock)/myDatabase `, RunE: runMysql, } mysqlCommand.Flags().String("expect-table", "", "Expect a table to exist in the database") return mysqlCommand } func runMysql(cmd *cobra.Command, args []string) error { logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } expectTable, err := cmd.Flags().GetString("expect-table") if err != nil { return fmt.Errorf("failed to parse --expect-table flag: %w", err) } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = mysql.New(arg, mysql.WithExpectTable(expectTable)) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/mysql_disabled.go000066400000000000000000000020761507553353000225140ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_mysql // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "github.com/spf13/cobra" ) // NewMySQLCommand creates a new mysql sub-command func NewMysqlCommand() *cobra.Command { return &cobra.Command{ Use: "mysql", Short: "Check MySQL connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("MySQL feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/mysql_test.go000066400000000000000000000271211507553353000217220ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "context" "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/modules/mysql" "wait4x.dev/v3/internal/test" ) // MySQLCommandSuite is a test suite for MySQL command functionality type MySQLCommandSuite struct { suite.Suite // Shared resources for the test suite rootCmd *cobra.Command mysqlCmd *cobra.Command container *mysql.MySQLContainer connectionString string } // SetupSuite sets up test suite resources func (s *MySQLCommandSuite) SetupSuite() { // Set up a MySQL container for tests that need an active connection var err error s.container, err = mysql.Run( context.Background(), "mysql:8.0.36", testcontainers.WithLogger(log.TestLogger(s.T())), ) s.Require().NoError(err) // Get the connection string for testing s.connectionString, err = s.container.ConnectionString(context.Background()) s.Require().NoError(err) } // TearDownSuite tears down test suite resources func (s *MySQLCommandSuite) TearDownSuite() { // Close container if s.container != nil { err := s.container.Terminate(context.Background()) s.Require().NoError(err) } } // SetupTest sets up each test func (s *MySQLCommandSuite) SetupTest() { s.rootCmd = NewRootCommand() s.mysqlCmd = NewMysqlCommand() s.rootCmd.AddCommand(s.mysqlCmd) } // TestNewMysqlCommand tests the MySQL command creation func (s *MySQLCommandSuite) TestNewMysqlCommand() { cmd := NewMysqlCommand() s.Equal("mysql DSN... [flags] [-- command [args...]]", cmd.Use) s.Equal("Check MySQL connection", cmd.Short) s.NotNil(cmd.Example) s.Contains(cmd.Example, "wait4x mysql user:password@tcp(localhost:5555)/dbname?tls=skip-verify") s.Contains(cmd.Example, "wait4x mysql username:password@unix(/tmp/mysql.sock)/myDatabase") } // TestMysqlCommandValidation tests argument validation func (s *MySQLCommandSuite) TestMysqlCommandValidation() { // Test missing arguments _, err := test.ExecuteCommand(s.rootCmd, "mysql") s.Require().Error(err) s.Equal("DSN is required argument for the mysql command", err.Error()) // Test empty arguments err = s.mysqlCmd.Args(s.mysqlCmd, []string{}) s.Require().Error(err) s.Equal("DSN is required argument for the mysql command", err.Error()) // Test valid arguments err = s.mysqlCmd.Args(s.mysqlCmd, []string{"user:password@tcp(localhost:3306)/dbname"}) s.NoError(err) err = s.mysqlCmd.Args(s.mysqlCmd, []string{ "user:password@tcp(localhost:3306)/dbname", "user2:password2@tcp(localhost:3307)/dbname2", }) s.NoError(err) } // TestMysqlConnectionScenarios tests various connection scenarios func (s *MySQLCommandSuite) TestMysqlConnectionScenarios() { // Test successful connection _, err := test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString) s.NoError(err) // Test connection failure (timeout) _, err = test.ExecuteCommand(s.rootCmd, "mysql", "user:password@tcp(localhost:8080)/dbname", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) // Test connection timeout with black-hole IP _, err = test.ExecuteCommand(s.rootCmd, "mysql", "user:password@tcp(240.0.0.1:3306)/dbname", "-t", "1s") s.Error(err) s.Equal(context.DeadlineExceeded, err) // Test multiple DSNs (mixed valid/invalid) _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "user:password@tcp(localhost:8080)/dbname", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestMysqlCommandFlags tests all command flags func (s *MySQLCommandSuite) TestMysqlCommandFlags() { // Test interval flag _, err := test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "-i", "500ms") s.NoError(err) // Test timeout flag _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "-t", "0") s.NoError(err) // Test quiet mode _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "-q") s.NoError(err) // Test no color flag _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--no-color") s.NoError(err) // Test dash separator for command execution _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--", "echo", "success") s.NoError(err) } // TestMysqlCommandInvertCheck tests invert check functionality func (s *MySQLCommandSuite) TestMysqlCommandInvertCheck() { // With invert check, a successful connection should fail _, err := test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "-v", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) // With invert check, a failed connection should succeed _, err = test.ExecuteCommand(s.rootCmd, "mysql", "user:password@tcp(localhost:8080)/dbname", "-v", "-t", "2s") s.NoError(err) } // TestMysqlCommandBackoffPolicy tests backoff policy functionality func (s *MySQLCommandSuite) TestMysqlCommandBackoffPolicy() { // Test exponential backoff _, err := test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--backoff-policy", "exponential") s.NoError(err) // Test invalid backoff policy _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--backoff-policy", "invalid") s.Error(err) s.Contains(err.Error(), "--backoff-policy must be one of") // Test backoff coefficient _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--backoff-policy", "exponential", "--backoff-exponential-coefficient", "1.5") s.NoError(err) // Test backoff max interval _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--backoff-policy", "exponential", "--backoff-exponential-max-interval", "3s") s.NoError(err) // Test invalid backoff max interval _, err = test.ExecuteCommand(s.rootCmd, "mysql", s.connectionString, "--backoff-policy", "exponential", "--backoff-exponential-max-interval", "100ms", "-i", "200ms") s.Require().Error(err) s.Contains(err.Error(), "--backoff-exponential-max-interval must be greater than --interval") } // TestMysqlCommandDSNFormats tests various DSN formats func (s *MySQLCommandSuite) TestMysqlCommandDSNFormats() { // Test invalid DSN format _, err := test.ExecuteCommand(s.rootCmd, "mysql", "invalid-dsn-format", "-t", "2s") s.Require().Error(err) s.Contains(err.Error(), "can't retrieve the checker identity") // Test Unix socket DSN (will timeout) _, err = test.ExecuteCommand(s.rootCmd, "mysql", "user:password@unix(/tmp/mysql.sock)/dbname", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) // Test TLS DSN (will timeout) _, err = test.ExecuteCommand(s.rootCmd, "mysql", "user:password@tcp(240.0.0.1:3306)/dbname?tls=skip-verify", "-t", "1s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestMysqlCommandTableDriven tests the MySQL command with various scenarios using table-driven tests func (s *MySQLCommandSuite) TestMysqlCommandTableDriven() { tests := []struct { name string args []string shouldError bool errorType string errorMessage string }{ { name: "valid connection", args: []string{"mysql", s.connectionString}, shouldError: false, }, { name: "invalid DSN format", args: []string{"mysql", "invalid-dsn"}, shouldError: true, errorType: "validation", errorMessage: "can't retrieve the checker identity", }, { name: "connection timeout", args: []string{"mysql", "user:password@tcp(240.0.0.1:3306)/dbname", "-t", "1s"}, shouldError: true, errorType: "timeout", errorMessage: "", }, { name: "missing DSN argument", args: []string{"mysql"}, shouldError: true, errorType: "validation", errorMessage: "DSN is required argument for the mysql command", }, { name: "multiple valid DSNs", args: []string{"mysql", s.connectionString, s.connectionString}, shouldError: false, }, { name: "mixed valid and invalid DSNs", args: []string{"mysql", s.connectionString, "user:password@tcp(localhost:8080)/dbname", "-t", "2s"}, shouldError: true, errorType: "timeout", errorMessage: "", }, { name: "with custom interval", args: []string{"mysql", s.connectionString, "-i", "500ms"}, shouldError: false, }, { name: "with exponential backoff", args: []string{"mysql", s.connectionString, "--backoff-policy", "exponential"}, shouldError: false, }, { name: "with invalid backoff policy", args: []string{"mysql", s.connectionString, "--backoff-policy", "invalid"}, shouldError: true, errorType: "validation", errorMessage: "--backoff-policy must be one of", }, { name: "with invert check on valid connection", args: []string{"mysql", s.connectionString, "-v", "-t", "2s"}, shouldError: true, errorType: "timeout", errorMessage: "", }, { name: "with invert check on invalid connection", args: []string{"mysql", "user:password@tcp(localhost:8080)/dbname", "-v", "-t", "2s"}, shouldError: false, }, { name: "with quiet mode", args: []string{"mysql", s.connectionString, "-q"}, shouldError: false, }, { name: "with no color", args: []string{"mysql", s.connectionString, "--no-color"}, shouldError: false, }, { name: "with zero timeout", args: []string{"mysql", s.connectionString, "-t", "0"}, shouldError: false, }, { name: "with command execution", args: []string{"mysql", s.connectionString, "--", "echo", "success"}, shouldError: false, }, } for _, tt := range tests { s.Run(tt.name, func() { // Create a fresh root command for each test to avoid flag pollution rootCmd := NewRootCommand() mysqlCmd := NewMysqlCommand() rootCmd.AddCommand(mysqlCmd) _, err := test.ExecuteCommand(rootCmd, tt.args...) if tt.shouldError { s.Require().Error(err) if tt.errorType == "timeout" { s.Require().ErrorIs(err, context.DeadlineExceeded) } else if tt.errorType == "validation" { s.Require().ErrorContains(err, tt.errorMessage) } else if tt.errorType == "connection" { s.Require().ErrorContains(err, tt.errorMessage) } } else { s.Require().NoError(err) } }) } } // TestMysqlCommandHelp tests the MySQL command help func (s *MySQLCommandSuite) TestMysqlCommandHelp() { output, err := test.ExecuteCommand(s.rootCmd, "mysql", "--help") s.Require().NoError(err) s.Contains(output, "Check MySQL connection") s.Contains(output, "DSN") } // TestMysqlCommandExample tests the MySQL command example func (s *MySQLCommandSuite) TestMysqlCommandExample() { // The example should be present in the command s.Contains(s.mysqlCmd.Example, "wait4x mysql user:password@tcp(localhost:5555)/dbname?tls=skip-verify") s.Contains(s.mysqlCmd.Example, "wait4x mysql username:password@unix(/tmp/mysql.sock)/myDatabase") } // TestMySQLCommandSuite runs the MySQL command test suite func TestMySQLCommandSuite(t *testing.T) { suite.Run(t, new(MySQLCommandSuite)) } wait4x-wait4x-7fb9e45/internal/cmd/postgresql.go000066400000000000000000000054601507553353000217230ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_postgresql // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/postgresql" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewPostgresqlCommand creates a new postgresql sub-command func NewPostgresqlCommand() *cobra.Command { postgresqlCommand := &cobra.Command{ Use: "postgresql DSN... [flags] [-- command [args...]]", Aliases: []string{"postgres", "postgre"}, Short: "Check PostgreSQL connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("DSN is required argument for the postgresql command") } return nil }, Example: ` # Checking PostgreSQL TCP connection wait4x postgresql postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full `, RunE: runPostgresql, } postgresqlCommand.Flags().String("expect-table", "", "Expect a table to exist in the database") return postgresqlCommand } func runPostgresql(cmd *cobra.Command, args []string) error { logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } expectTable, err := cmd.Flags().GetString("expect-table") if err != nil { return fmt.Errorf("failed to parse --expect-table flag: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = postgresql.New(arg, postgresql.WithExpectTable(expectTable)) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/postgresql_disabled.go000066400000000000000000000021411507553353000235430ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_postgresql // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "github.com/spf13/cobra" ) // NewPostgresqlCommand creates a new postgresql sub-command func NewPostgresqlCommand() *cobra.Command { return &cobra.Command{ Use: "postgresql", Short: "Check PostgreSQL connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("PostgreSQL feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/rabbitmq.go000066400000000000000000000064071507553353000213230ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_rabbitmq package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/rabbitmq" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewRabbitMQCommand creates a new rabbitmq sub-command func NewRabbitMQCommand() *cobra.Command { rabbitmqCommand := &cobra.Command{ Use: "rabbitmq DSN... [flags] [-- command [args...]]", Short: "Check RabbitMQ connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("DSN is required argument for the rabbitmq sub-command") } return nil }, Example: ` # Checking RabbitMQ connection wait4x rabbitmq 'amqp://127.0.0.1:5672' # Checking RabbitMQ connection with credentials and vhost wait4x rabbitmq 'amqp://guest:guest@127.0.0.1:5672/vhost' `, RunE: runRabbitMQ, } rabbitmqCommand.Flags().Duration("connection-timeout", rabbitmq.DefaultConnectionTimeout, "Timeout is the maximum amount of time a dial will wait for a connection to complete.") rabbitmqCommand.Flags().Bool("insecure-skip-tls-verify", rabbitmq.DefaultInsecureSkipTLSVerify, "InsecureSkipTLSVerify controls whether a client verifies the server's certificate chain and hostname.") return rabbitmqCommand } func runRabbitMQ(cmd *cobra.Command, args []string) error { conTimeout, err := cmd.Flags().GetDuration("connection-timeout") if err != nil { return fmt.Errorf("unable to parse --connection-timeout flag: %w", err) } insecureSkipTLSVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify") if err != nil { return fmt.Errorf("unable to parse --insecure-skip-tls-verify flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("unable to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = rabbitmq.New( arg, rabbitmq.WithTimeout(conTimeout), rabbitmq.WithInsecureSkipTLSVerify(insecureSkipTLSVerify), ) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/rabbitmq_disabled.go000066400000000000000000000020071507553353000231420ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_rabbitmq package cmd import ( "errors" "github.com/spf13/cobra" ) // NewRabbitMQCommand creates the rabbitmq sub-command func NewRabbitMQCommand() *cobra.Command { return &cobra.Command{ Use: "rabbitmq", Short: "Check RabbitMQ connection - this feature is disabled", RunE: func(cmd *cobra.Command, args []string) error { return errors.New("RabbitMQ feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/redis.go000066400000000000000000000066151507553353000206310ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_redis // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/redis" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewRedisCommand creates a new redis sub-command func NewRedisCommand() *cobra.Command { redisCommand := &cobra.Command{ Use: "redis ADDRESS... [flags] [-- command [args...]]", Short: "Check Redis connection or key existence", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the redis command") } return nil }, Example: ` # Checking Redis connection wait4x redis redis://127.0.0.1:6379 # Specify username, password and db wait4x redis redis://user:password@localhost:6379/1 # Checking Redis connection over unix socket wait4x redis unix://user:password@/path/to/redis.sock?db=1 # Checking a key existence wait4x redis redis://127.0.0.1:6379 --expect-key FOO # Checking a key existence and matching the value wait4x redis redis://127.0.0.1:6379 --expect-key "FOO=^b[A-Z]r$" `, RunE: runRedis, } redisCommand.Flags().Duration("connection-timeout", redis.DefaultConnectionTimeout, "Dial timeout for establishing new connections.") redisCommand.Flags().String("expect-key", "", "Checking key existence.") return redisCommand } // runRedis runs the redis command func runRedis(cmd *cobra.Command, args []string) error { conTimeout, err := cmd.Flags().GetDuration("connection-timeout") if err != nil { return fmt.Errorf("failed to parse --connection-timeout flag: %w", err) } expectKey, err := cmd.Flags().GetString("expect-key") if err != nil { return fmt.Errorf("failed to parse --expect-key flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = redis.New( arg, redis.WithExpectKey(expectKey), redis.WithTimeout(conTimeout), ) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/redis_disabled.go000066400000000000000000000017571507553353000224620ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_redis package cmd import ( "errors" "github.com/spf13/cobra" ) // NewRedisCommand creates a new redis sub-command func NewRedisCommand() *cobra.Command { return &cobra.Command{ Use: "redis", Short: "Check Redis connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("Redis feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/root.go000066400000000000000000000163561507553353000205110ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "context" "errors" "fmt" "os" "os/exec" "os/signal" "time" "github.com/fatih/color" "github.com/go-logr/logr" "github.com/go-logr/zerologr" "github.com/rs/zerolog" "github.com/spf13/cobra" "wait4x.dev/v3/internal/cmd/dns" "wait4x.dev/v3/internal/cmd/temporal" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) const ( // ExitError is the exit code used when the command encounters an error. ExitError = 1 // ExitTimedOut is the exit code used when the command times out. ExitTimedOut = 124 ) // NewRootCommand creates a new root command func NewRootCommand() *cobra.Command { rootCmd := &cobra.Command{ Use: "wait4x", Short: "Wait4X allows waiting for a port or a service to enter into specify state", Long: `Wait4X allows waiting for a port to enter into specify state or waiting for a service e.g. redis, mysql, postgres, ... to enter inter ready state`, CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, }, PersistentPreRunE: func(cmd *cobra.Command, _ []string) (err error) { quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return fmt.Errorf("unable to parse --quiet flag: %w", err) } noColor, err := cmd.Flags().GetBool("no-color") if err != nil { return fmt.Errorf("unable to parse --no-color flag: %w", err) } timeout, err := cmd.Flags().GetDuration("timeout") if err != nil { return fmt.Errorf("unable to parse --timeout flag: %w", err) } interval, err := cmd.Flags().GetDuration("interval") if err != nil { return fmt.Errorf("unable to parse --interval flag: %w", err) } invertCheck, err := cmd.Flags().GetBool("invert-check") if err != nil { return fmt.Errorf("unable to parse --invert-check flag: %w", err) } backoffPolicy, err := cmd.Flags().GetString("backoff-policy") if err != nil { return fmt.Errorf("unable to parse --backoff-policy flag: %w", err) } backoffCoefficient, err := cmd.Flags().GetFloat64("backoff-exponential-coefficient") if err != nil { return fmt.Errorf("unable to parse --backoff-exponential-coefficient flag: %w", err) } backoffExpMaxInterval, err := cmd.Flags().GetDuration("backoff-exponential-max-interval") if err != nil { return fmt.Errorf("unable to parse --backoff-exponential-max-interval flag: %w", err) } cmd.SetContext(contextutil.WithTimeout(cmd.Context(), timeout)) cmd.SetContext(contextutil.WithInterval(cmd.Context(), interval)) cmd.SetContext(contextutil.WithInvertCheck(cmd.Context(), invertCheck)) cmd.SetContext(contextutil.WithBackoffPolicy(cmd.Context(), backoffPolicy)) cmd.SetContext(contextutil.WithBackoffCoefficient(cmd.Context(), backoffCoefficient)) cmd.SetContext(contextutil.WithBackoffExponentialMaxInterval(cmd.Context(), backoffExpMaxInterval)) // Validate backoff policy value backoffPolicyValues := []string{waiter.BackoffPolicyExponential, waiter.BackoffPolicyLinear} if !contains(backoffPolicyValues, backoffPolicy) { return fmt.Errorf("--backoff-policy must be one of %v", backoffPolicyValues) } if backoffPolicy == waiter.BackoffPolicyExponential && backoffExpMaxInterval < interval { return fmt.Errorf("--backoff-exponential-max-interval must be greater than --interval") } // Prevent showing error when the quiet mode enabled. cmd.SilenceErrors = quiet lvl := zerolog.InfoLevel if quiet { lvl = zerolog.Disabled } // Prevent showing usage when subcommand return error. cmd.SilenceUsage = true zl := zerolog.New( zerolog.ConsoleWriter{ Out: os.Stderr, NoColor: color.NoColor || noColor, TimeFormat: time.RFC3339, }, ).Level(lvl). With(). Timestamp(). Logger() logger := zerologr.New(&zl) // VerbosityFieldName (v) is not emitted. zerologr.VerbosityFieldName = "" cmd.SetContext(logr.NewContext(cmd.Context(), logger)) return nil }, PersistentPostRunE: func(cmd *cobra.Command, args []string) error { if cmd.ArgsLenAtDash() != -1 && (len(args)-cmd.ArgsLenAtDash()) > 0 { command := args[cmd.ArgsLenAtDash():][0] arguments := args[cmd.ArgsLenAtDash():][1:] for i, arg := range arguments { arguments[i] = os.ExpandEnv(arg) } c := exec.CommandContext(cmd.Context(), command, arguments...) c.Stdout = os.Stdout c.Stderr = os.Stderr return c.Run() } return nil }, } rootCmd.PersistentFlags().DurationP("interval", "i", 1*time.Second, "Interval time between each loop.") rootCmd.PersistentFlags().String("backoff-policy", "linear", `Select the backoff policy ("`+waiter.BackoffPolicyLinear+`"|"`+waiter.BackoffPolicyExponential+`".`) rootCmd.PersistentFlags().Duration("backoff-exponential-max-interval", 5*time.Second, "Maximum interval time between each loop when backoff-policy is exponential.") rootCmd.PersistentFlags().Float64("backoff-exponential-coefficient", 2.0, "Coefficient used to calculate the exponential backoff when backoff-policy is exponential.") rootCmd.PersistentFlags().DurationP("timeout", "t", 10*time.Second, "Timeout is the maximum amount of time that Wait4X will wait for a checking operation, 0 is unlimited.") rootCmd.PersistentFlags().BoolP("invert-check", "v", false, "Invert the sense of checking.") rootCmd.PersistentFlags().StringP("log-level", "l", zerolog.InfoLevel.String(), "Set the logging level (\"trace\"|\"debug\"|\"info\")") rootCmd.PersistentFlags().MarkDeprecated("log-level", "You don't need to the flag anymore. By default, Wait4X returns error logs. This flag will be removed in v4.0.0") rootCmd.PersistentFlags().Bool("no-color", false, "If specified, output won't contain any color.") rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Quiet or silent mode. Do not show logs or error messages.") return rootCmd } // Execute run Wait4X application func Execute() { rootCmd := NewRootCommand() rootCmd.AddCommand(NewTCPCommand()) rootCmd.AddCommand(dns.NewDNSCommand()) rootCmd.AddCommand(NewHTTPCommand()) rootCmd.AddCommand(NewPostgresqlCommand()) rootCmd.AddCommand(NewMysqlCommand()) rootCmd.AddCommand(NewRedisCommand()) rootCmd.AddCommand(NewInfluxDBCommand()) rootCmd.AddCommand(NewKafkaCommand()) rootCmd.AddCommand(NewMongoDBCommand()) rootCmd.AddCommand(NewRabbitMQCommand()) rootCmd.AddCommand(temporal.NewTemporalCommand()) rootCmd.AddCommand(NewVersionCommand()) rootCmd.AddCommand(NewExecCommand()) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() if err := rootCmd.ExecuteContext(ctx); err != nil { if errors.Is(err, context.DeadlineExceeded) { os.Exit(ExitTimedOut) } os.Exit(ExitError) } } wait4x-wait4x-7fb9e45/internal/cmd/tcp.go000066400000000000000000000051371507553353000203070ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmd import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker" "wait4x.dev/v3/checker/tcp" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewTCPCommand creates a new tcp sub-command func NewTCPCommand() *cobra.Command { tcpCommand := &cobra.Command{ Use: "tcp ADDRESS... [flags] [-- command [args...]]", Short: "Check TCP connection", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("ADDRESS is required argument for the tcp command") } return nil }, Example: ` # If you want checking just tcp connection wait4x tcp 127.0.0.1:9090 `, RunE: runTCP, } tcpCommand.Flags().Duration("connection-timeout", tcp.DefaultConnectionTimeout, "Timeout is the maximum amount of time a dial will wait for a connection to complete.") return tcpCommand } func runTCP(cmd *cobra.Command, args []string) error { conTimeout, err := cmd.Flags().GetDuration("connection-timeout") if err != nil { return fmt.Errorf("failed to parse --connection-timeout flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } checkers := make([]checker.Checker, len(args)) for i, arg := range args { checkers[i] = tcp.New(arg, tcp.WithTimeout(conTimeout)) } return waiter.WaitParallelContext( cmd.Context(), checkers, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithBackoffPolicy(contextutil.GetBackoffPolicy(cmd.Context())), waiter.WithBackoffCoefficient(contextutil.GetBackoffCoefficient(cmd.Context())), waiter.WithBackoffExponentialMaxInterval(contextutil.GetBackoffExponentialMaxInterval(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/tcp_test.go000066400000000000000000000365231507553353000213510ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "context" "net" "strconv" "testing" "time" "github.com/spf13/cobra" "github.com/stretchr/testify/suite" "wait4x.dev/v3/internal/test" ) // TCPCommandSuite is a test suite for TCP command functionality type TCPCommandSuite struct { suite.Suite // Shared resources for the test suite rootCmd *cobra.Command tcpCmd *cobra.Command listener net.Listener port int unusedPort int serverDone chan struct{} } // SetupSuite sets up test suite resources func (s *TCPCommandSuite) SetupSuite() { // Set up a TCP server for tests that need an active connection var err error s.listener, err = net.Listen("tcp", "127.0.0.1:0") s.Require().NoError(err) // Parse the port _, portStr, err := net.SplitHostPort(s.listener.Addr().String()) s.Require().NoError(err) s.port, err = strconv.Atoi(portStr) s.Require().NoError(err) // Find an unused port for connection refused tests s.unusedPort = s.port + 1 // Set up a channel to track server completion s.serverDone = make(chan struct{}) // Handle connections in a goroutine go func() { defer close(s.serverDone) for { conn, err := s.listener.Accept() if err != nil { return // listener closed } if conn != nil { s.Require().NoError(conn.Close()) } } }() } // TearDownSuite tears down test suite resources func (s *TCPCommandSuite) TearDownSuite() { // Close listener if s.listener != nil { s.Require().NoError(s.listener.Close()) <-s.serverDone // Wait for server goroutine to complete } } // SetupTest sets up each test func (s *TCPCommandSuite) SetupTest() { s.rootCmd = NewRootCommand() s.tcpCmd = NewTCPCommand() s.rootCmd.AddCommand(s.tcpCmd) } // TestNewTCPCommand tests the TCP command creation func (s *TCPCommandSuite) TestNewTCPCommand() { cmd := NewTCPCommand() s.Equal("tcp ADDRESS... [flags] [-- command [args...]]", cmd.Use) s.Equal("Check TCP connection", cmd.Short) s.NotNil(cmd.Example) s.Contains(cmd.Example, "wait4x tcp 127.0.0.1:9090") // Test that the command has the expected flags flags := cmd.Flags() connectionTimeout, err := flags.GetDuration("connection-timeout") s.NoError(err) s.Equal(3*time.Second, connectionTimeout) // Default from tcp package } // TestTCPCommandInvalidArgument tests the TCP command with invalid arguments func (s *TCPCommandSuite) TestTCPCommandInvalidArgument() { _, err := test.ExecuteCommand(s.rootCmd, "tcp") s.Error(err) s.Equal("ADDRESS is required argument for the tcp command", err.Error()) } // TestTCPCommandEmptyArgs tests the TCP command with empty arguments func (s *TCPCommandSuite) TestTCPCommandEmptyArgs() { err := s.tcpCmd.Args(s.tcpCmd, []string{}) s.Error(err) s.Equal("ADDRESS is required argument for the tcp command", err.Error()) } // TestTCPCommandValidArgs tests the TCP command with valid arguments func (s *TCPCommandSuite) TestTCPCommandValidArgs() { err := s.tcpCmd.Args(s.tcpCmd, []string{"127.0.0.1:8080"}) s.NoError(err) err = s.tcpCmd.Args(s.tcpCmd, []string{"127.0.0.1:8080", "192.168.1.1:9090"}) s.NoError(err) } // TestTCPConnectionSuccess tests the TCP connection success func (s *TCPCommandSuite) TestTCPConnectionSuccess() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53") s.NoError(err) } // TestTCPConnectionSuccessLocal tests the TCP connection success to local server func (s *TCPCommandSuite) TestTCPConnectionSuccessLocal() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", s.listener.Addr().String()) s.NoError(err) } // TestTCPConnectionFail tests the TCP connection failure func (s *TCPCommandSuite) TestTCPConnectionFail() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPConnectionFailUnusedPort tests the TCP connection failure on unused port func (s *TCPCommandSuite) TestTCPConnectionFailUnusedPort() { address := net.JoinHostPort("127.0.0.1", strconv.Itoa(s.unusedPort)) _, err := test.ExecuteCommand(s.rootCmd, "tcp", address, "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPConnectionTimeout tests the TCP connection timeout behavior func (s *TCPCommandSuite) TestTCPConnectionTimeout() { // Use a black-hole IP that will cause timeout _, err := test.ExecuteCommand(s.rootCmd, "tcp", "240.0.0.1:12345", "-t", "1s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPConnectionWithCustomTimeout tests the TCP connection with custom timeout func (s *TCPCommandSuite) TestTCPConnectionWithCustomTimeout() { // Test with a very short connection timeout _, err := test.ExecuteCommand(s.rootCmd, "tcp", "240.0.0.1:12345", "--connection-timeout", "100ms", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPConnectionWithInvalidTimeout tests the TCP connection with invalid timeout func (s *TCPCommandSuite) TestTCPConnectionWithInvalidTimeout() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "--connection-timeout", "invalid") s.Error(err) s.Contains(err.Error(), "invalid argument \"invalid\" for \"--connection-timeout\" flag") } // TestTCPMultipleAddresses tests the TCP command with multiple addresses func (s *TCPCommandSuite) TestTCPMultipleAddresses() { // Test with multiple valid addresses _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "8.8.8.8:53") s.NoError(err) } // TestTCPMultipleAddressesMixed tests the TCP command with mixed valid/invalid addresses func (s *TCPCommandSuite) TestTCPMultipleAddressesMixed() { // One valid, one invalid - should fail _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "127.0.0.1:8080", "-t", "2s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPCommandWithDash tests the TCP command with dash separator for command execution func (s *TCPCommandSuite) TestTCPCommandWithDash() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--", "echo", "success") s.NoError(err) } // TestTCPCommandWithInvertCheck tests the TCP command with invert check flag func (s *TCPCommandSuite) TestTCPCommandWithInvertCheck() { // With invert check, we expect the command to fail when connection succeeds // Use a shorter timeout to make the test fail faster _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-v", "-t", "1s") s.Error(err) s.Equal(context.DeadlineExceeded, err) } // TestTCPCommandWithInvertCheckFail tests the TCP command with invert check when connection fails func (s *TCPCommandSuite) TestTCPCommandWithInvertCheckFail() { // With invert check, we expect the command to succeed when connection fails _, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "-v", "-t", "2s") s.NoError(err) } // TestTCPCommandWithInterval tests the TCP command with custom interval func (s *TCPCommandSuite) TestTCPCommandWithInterval() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-i", "500ms") s.NoError(err) } // TestTCPCommandWithBackoffPolicy tests the TCP command with different backoff policies func (s *TCPCommandSuite) TestTCPCommandWithBackoffPolicy() { // Test with exponential backoff _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "exponential") s.NoError(err) // Test with linear backoff _, err = test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "linear") s.NoError(err) } // TestTCPCommandWithInvalidBackoffPolicy tests the TCP command with invalid backoff policy func (s *TCPCommandSuite) TestTCPCommandWithInvalidBackoffPolicy() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "invalid") s.Error(err) s.Contains(err.Error(), "--backoff-policy must be one of") } // TestTCPCommandWithBackoffCoefficient tests the TCP command with custom backoff coefficient func (s *TCPCommandSuite) TestTCPCommandWithBackoffCoefficient() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-exponential-coefficient", "1.5") s.NoError(err) } // TestTCPCommandWithBackoffMaxInterval tests the TCP command with custom backoff max interval func (s *TCPCommandSuite) TestTCPCommandWithBackoffMaxInterval() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-exponential-max-interval", "3s") s.NoError(err) } // TestTCPCommandWithInvalidBackoffMaxInterval tests the TCP command with invalid backoff max interval func (s *TCPCommandSuite) TestTCPCommandWithInvalidBackoffMaxInterval() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "exponential", "--backoff-exponential-max-interval", "100ms", "-i", "200ms") s.Error(err) s.Contains(err.Error(), "--backoff-exponential-max-interval must be greater than --interval") } // TestTCPCommandWithQuietMode tests the TCP command with quiet mode func (s *TCPCommandSuite) TestTCPCommandWithQuietMode() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-q") s.NoError(err) } // TestTCPCommandWithNoColor tests the TCP command with no color flag func (s *TCPCommandSuite) TestTCPCommandWithNoColor() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--no-color") s.NoError(err) } // TestTCPCommandWithZeroTimeout tests the TCP command with zero timeout (unlimited) func (s *TCPCommandSuite) TestTCPCommandWithZeroTimeout() { // This should work but take longer, so we'll use a short timeout for the test _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-t", "0s") s.NoError(err) } // TestTCPCommandWithInvalidAddressFormat tests the TCP command with invalid address format func (s *TCPCommandSuite) TestTCPCommandWithInvalidAddressFormat() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "invalid-address", "-t", "2s") s.Error(err) // The error should be either a connection error or timeout if err == context.DeadlineExceeded { // Timeout is acceptable for invalid addresses s.Equal(context.DeadlineExceeded, err) } else { // Or it should be a connection error s.Contains(err.Error(), "failed to establish a tcp connection") } } // TestTCPCommandWithIPv6Address tests the TCP command with IPv6 address func (s *TCPCommandSuite) TestTCPCommandWithIPv6Address() { _, err := test.ExecuteCommand(s.rootCmd, "tcp", "[::1]:53", "-t", "2s") // This might fail if IPv6 is not available, but should not crash if err != nil { if err == context.DeadlineExceeded { // Timeout is acceptable for IPv6 if not available s.Equal(context.DeadlineExceeded, err) } else { s.Contains(err.Error(), "failed to establish a tcp connection") } } } // TestTCPCommandTableDriven defines table-driven tests for various scenarios func (s *TCPCommandSuite) TestTCPCommandTableDriven() { tests := []struct { name string args []string shouldError bool errorType string // "timeout", "validation", "connection", "connection_or_timeout", or "" if no error }{ { name: "Valid Address", args: []string{"tcp", "1.1.1.1:53"}, shouldError: false, }, { name: "No Arguments", args: []string{"tcp"}, shouldError: true, errorType: "validation", }, { name: "Connection Refused", args: []string{"tcp", "240.0.0.1:12345", "-t", "2s"}, shouldError: true, errorType: "timeout", }, { name: "Invalid Address Format", args: []string{"tcp", "not-a-valid-address", "-t", "2s"}, shouldError: true, errorType: "connection_or_timeout", }, { name: "Multiple Valid Addresses", args: []string{"tcp", "1.1.1.1:53", "8.8.8.8:53"}, shouldError: false, }, { name: "With Custom Interval", args: []string{"tcp", "1.1.1.1:53", "-i", "500ms"}, shouldError: false, }, { name: "With Invert Check Success", args: []string{"tcp", "240.0.0.1:12345", "-v", "-t", "2s"}, shouldError: false, // Should succeed because connection fails and we're inverting }, { name: "With Invert Check Failure", args: []string{"tcp", "1.1.1.1:53", "-v", "-t", "1s"}, shouldError: true, // Should fail because connection succeeds and we're inverting errorType: "timeout", }, } for _, tt := range tests { s.Run(tt.name, func() { _, err := test.ExecuteCommand(s.rootCmd, tt.args...) if tt.shouldError { s.Error(err) if tt.errorType == "timeout" { s.Equal(context.DeadlineExceeded, err) } else if tt.errorType == "validation" { s.Contains(err.Error(), "ADDRESS is required argument for the tcp command") } else if tt.errorType == "connection" { s.Contains(err.Error(), "failed to establish a tcp connection") } else if tt.errorType == "connection_or_timeout" { if err == context.DeadlineExceeded { s.Equal(context.DeadlineExceeded, err) } else { s.Contains(err.Error(), "failed to establish a tcp connection") } } } else { s.NoError(err) } }) } } // TestTCPCommandFlags tests the TCP command flags func (s *TCPCommandSuite) TestTCPCommandFlags() { flags := s.tcpCmd.Flags() // Test connection-timeout flag connectionTimeout, err := flags.GetDuration("connection-timeout") s.NoError(err) s.Equal(3*time.Second, connectionTimeout) // Test that the flag is required s.True(flags.Lookup("connection-timeout") != nil) } // TestTCPCommandHelp tests the TCP command help func (s *TCPCommandSuite) TestTCPCommandHelp() { output, err := test.ExecuteCommand(s.rootCmd, "tcp", "--help") s.NoError(err) s.Contains(output, "Check TCP connection") s.Contains(output, "connection-timeout") } // TestTCPCommandExample tests the TCP command example func (s *TCPCommandSuite) TestTCPCommandExample() { // The example should be present in the command s.Contains(s.tcpCmd.Example, "wait4x tcp 127.0.0.1:9090") } // TestTCPCommandRunE tests the runTCP function directly func (s *TCPCommandSuite) TestTCPCommandRunE() { // Test argument validation - this should work without logger context err := s.tcpCmd.Args(s.tcpCmd, []string{}) s.Error(err) s.Equal("ADDRESS is required argument for the tcp command", err.Error()) // Test with valid arguments - this should also work for argument validation err = s.tcpCmd.Args(s.tcpCmd, []string{"1.1.1.1:53"}) s.NoError(err) // For testing the actual runTCP function, we need to use the full command execution // since runTCP requires a logger in the context _, err = test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53") s.NoError(err) } // TestTCPCommandWithContext tests the TCP command with context func (s *TCPCommandSuite) TestTCPCommandWithContext() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s.tcpCmd.SetContext(ctx) // Test that the command works with a valid context _, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53") s.NoError(err) } // TestTCPCommandSuite runs the test suite func TestTCPCommandSuite(t *testing.T) { suite.Run(t, new(TCPCommandSuite)) } wait4x-wait4x-7fb9e45/internal/cmd/temporal/000077500000000000000000000000001507553353000210075ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/cmd/temporal/server.go000066400000000000000000000055611507553353000226530ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_temporal // Package temporal provides the Temporal command-line interface for the Wait4X application. package temporal import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker/temporal" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewServerCommand creates a new server sub-command func NewServerCommand() *cobra.Command { serverCommand := &cobra.Command{ Use: "server TARGET [flags] [-- command [args...]]", Short: "Check Temporal server health check", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("TARGET is required argument for the server command") } return nil }, Example: ` # Checking just Temporal server health check wait4x temporal server 127.0.0.1:7233 # Checking insecure Temporal server (no TLS) wait4x temporal server 127.0.0.1:7233 --insecure-transport `, RunE: runServer, } return serverCommand } // runServer runs the server command func runServer(cmd *cobra.Command, args []string) error { conTimeout, err := cmd.Flags().GetDuration("connection-timeout") if err != nil { return fmt.Errorf("failed to parse connection-timeout flag: %w", err) } insecureTransport, err := cmd.Flags().GetBool("insecure-transport") if err != nil { return fmt.Errorf("failed to parse insecure-transport flag: %w", err) } insecureSkipTLSVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify") if err != nil { return fmt.Errorf("failed to parse insecure-skip-tls-verify flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } tc := temporal.New( temporal.CheckModeServer, args[0], temporal.WithTimeout(conTimeout), temporal.WithInsecureTransport(insecureTransport), temporal.WithInsecureSkipTLSVerify(insecureSkipTLSVerify), ) return waiter.WaitContext( cmd.Context(), tc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/temporal/temporal.go000066400000000000000000000031351507553353000231630ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_temporal // Package temporal provides the Temporal command-line interface for the Wait4X application. package temporal import ( "github.com/spf13/cobra" "wait4x.dev/v3/checker/temporal" ) // NewTemporalCommand creates a new temporal sub-command func NewTemporalCommand() *cobra.Command { temporalCommand := &cobra.Command{ Use: "temporal", Short: "Check Temporal server & worker", } temporalCommand.PersistentFlags().Duration("connection-timeout", temporal.DefaultConnectionTimeout, "Timeout is the maximum amount of time a dial will wait for a GRPC connection to complete.") temporalCommand.PersistentFlags().Bool("insecure-transport", temporal.DefaultInsecureTransport, "Skips GRPC transport security.") temporalCommand.PersistentFlags().Bool("insecure-skip-tls-verify", temporal.DefaultInsecureSkipTLSVerify, "Skips tls certificate checks for the GRPC request.") temporalCommand.AddCommand(NewServerCommand()) temporalCommand.AddCommand(NewWorkerCommand()) return temporalCommand } wait4x-wait4x-7fb9e45/internal/cmd/temporal/temporal_disabled.go000066400000000000000000000021461507553353000250130ustar00rootroot00000000000000// Copyright 2018-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build disable_temporal // Package temporal provides the Temporal command-line interface for the Wait4X application. package temporal import ( "errors" "github.com/spf13/cobra" ) // NewTemporalCommand creates a new temporal sub-command func NewTemporalCommand() *cobra.Command { return &cobra.Command{ Use: "temporal", Short: "Check Temporal connection - this feature is disabled", RunE: func(_ *cobra.Command, _ []string) error { return errors.New("Temporal feature disabled in this build.") }, } } wait4x-wait4x-7fb9e45/internal/cmd/temporal/worker.go000066400000000000000000000100351507553353000226460ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !disable_temporal // Package temporal provides the Temporal command-line interface for the Wait4X application. package temporal import ( "errors" "fmt" "github.com/go-logr/logr" "github.com/spf13/cobra" "wait4x.dev/v3/checker/temporal" "wait4x.dev/v3/internal/contextutil" "wait4x.dev/v3/waiter" ) // NewWorkerCommand creates a new worker sub-command func NewWorkerCommand() *cobra.Command { workerCommand := &cobra.Command{ Use: "worker TARGET [flags] [-- command [args...]]", Short: "Check Temporal worker registration", Args: func(_ *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("TARGET is required argument for the worker command") } return nil }, Example: ` # Checking a task queue that has registered workers (pollers) or not wait4x temporal worker 127.0.0.1:7233 --namespace __YOUR_NAMESPACE__ --task-queue __YOUR_TASK_QUEUE__ # Checking the specific a Temporal worker (pollers) wait4x temporal worker 127.0.0.1:7233 --namespace __YOUR_NAMESPACE__ --task-queue __YOUR_TASK_QUEUE__ --expect-worker-identity-regex ".*@__HOSTNAME__@.*" `, RunE: runWorker, } workerCommand.Flags().String("namespace", "", "Temporal namespace.") workerCommand.Flags().String("task-queue", "", "Temporal task queue.") workerCommand.Flags().String("expect-worker-identity-regex", "", "Expect Temporal worker (poller) identity regex.") cobra.MarkFlagRequired(workerCommand.Flags(), "namespace") cobra.MarkFlagRequired(workerCommand.Flags(), "task-queue") return workerCommand } // runWorker runs the worker command func runWorker(cmd *cobra.Command, args []string) error { conTimeout, err := cmd.Flags().GetDuration("connection-timeout") if err != nil { return fmt.Errorf("failed to parse --connection-timeout flag: %w", err) } insecureTransport, err := cmd.Flags().GetBool("insecure-transport") if err != nil { return fmt.Errorf("failed to parse --insecure-transport flag: %w", err) } insecureSkipTLSVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify") if err != nil { return fmt.Errorf("failed to parse --insecure-skip-tls-verify flag: %w", err) } namespace, err := cmd.Flags().GetString("namespace") if err != nil { return fmt.Errorf("failed to parse --namespace flag: %w", err) } taskQueue, err := cmd.Flags().GetString("task-queue") if err != nil { return fmt.Errorf("failed to parse --task-queue flag: %w", err) } expectWorkerIdentityRegex, err := cmd.Flags().GetString("expect-worker-identity-regex") if err != nil { return fmt.Errorf("failed to parse --expect-worker-identity-regex flag: %w", err) } logger, err := logr.FromContext(cmd.Context()) if err != nil { return fmt.Errorf("failed to get logger from context: %w", err) } // ArgsLenAtDash returns -1 when -- was not specified if i := cmd.ArgsLenAtDash(); i != -1 { args = args[:i] } tc := temporal.New( temporal.CheckModeWorker, args[0], temporal.WithTimeout(conTimeout), temporal.WithInsecureTransport(insecureTransport), temporal.WithInsecureSkipTLSVerify(insecureSkipTLSVerify), temporal.WithNamespace(namespace), temporal.WithTaskQueue(taskQueue), temporal.WithExpectWorkerIdentityRegex(expectWorkerIdentityRegex), ) return waiter.WaitContext( cmd.Context(), tc, waiter.WithTimeout(contextutil.GetTimeout(cmd.Context())), waiter.WithInterval(contextutil.GetInterval(cmd.Context())), waiter.WithInvertCheck(contextutil.GetInvertCheck(cmd.Context())), waiter.WithLogger(logger), ) } wait4x-wait4x-7fb9e45/internal/cmd/utils.go000066400000000000000000000014601507553353000206540ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd func contains(s []string, str string) bool { for _, v := range s { if v == str { return true } } return false } wait4x-wait4x-7fb9e45/internal/cmd/version.go000066400000000000000000000046001507553353000212000ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the command-line interface for the Wait4X application. package cmd import ( "bytes" "fmt" "runtime" "text/template" "github.com/spf13/cobra" ) var versionTemplate = `Version: {{.AppVersion}} Go version: {{.GoVersion}} Git commit: {{.GitCommit}} Built: {{.BuildTime}} OS/Arch: {{.GoOs}}/{{.GoArch}}` var ( // AppVersion represents Wait4X version AppVersion = "v3.6.0" // GitCommit represents Wait4X commit sha1 hash from git, output of $(git rev-parse HEAD) GitCommit = "97f6539e11575977097054f0769d021ac605fc91" // BuildTime represents Wait4X build time in ISO8601 format, output of $(date -u '+%FT%TZ') BuildTime = "1970-01-01T00:00:00Z" ) // Version represents some information which useful in version sub-command type Version struct { AppVersion string GoVersion string GoOs string GoArch string GitCommit string BuildTime string } // NewVersionCommand creates a new version sub-command func NewVersionCommand() *cobra.Command { versionCommand := &cobra.Command{ Use: "version", Short: "Show Wait4X version information", Long: "Display detailed version information about the Wait4X application", RunE: runVersion, } return versionCommand } // runVersion runs the version command func runVersion(_ *cobra.Command, _ []string) error { versionValues := Version{ AppVersion: AppVersion, GoVersion: runtime.Version(), GoOs: runtime.GOOS, GoArch: runtime.GOARCH, GitCommit: GitCommit, BuildTime: BuildTime, } var tmplBytes bytes.Buffer t := template.Must(template.New("version").Parse(versionTemplate)) err := t.Execute(&tmplBytes, versionValues) if err != nil { return fmt.Errorf("unable to parse version template: %w", err) } fmt.Println(tmplBytes.String()) return nil } wait4x-wait4x-7fb9e45/internal/contextutil/000077500000000000000000000000001507553353000210035ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/contextutil/contextutil.go000066400000000000000000000075071507553353000237250ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package contextutil provides utilities for working with the Go context package. package contextutil import ( "context" "time" ) // These are context keys used to store and retrieve various values in the context. type ( timeoutCtxKey struct{} intervalCtxKey struct{} invertCheckCtxKey struct{} backoffPolicyCtxKey struct{} backoffCoefficientCtxKey struct{} backoffExponentialMaxIntervalCtxKey struct{} ) // WithTimeout returns a new context with the given timeout value. func WithTimeout(ctx context.Context, timeout time.Duration) context.Context { return context.WithValue(ctx, timeoutCtxKey{}, timeout) } // GetTimeout retrieves timeout from context func GetTimeout(ctx context.Context) time.Duration { if v := ctx.Value(timeoutCtxKey{}); v != nil { return v.(time.Duration) } return 0 } // WithInterval returns a new context with the given interval value. func WithInterval(ctx context.Context, interval time.Duration) context.Context { return context.WithValue(ctx, intervalCtxKey{}, interval) } // GetInterval retrieves interval from context func GetInterval(ctx context.Context) time.Duration { if v := ctx.Value(intervalCtxKey{}); v != nil { return v.(time.Duration) } return 0 } // WithInvertCheck returns a new context with the given invert-check value. func WithInvertCheck(ctx context.Context, invertCheck bool) context.Context { return context.WithValue(ctx, invertCheckCtxKey{}, invertCheck) } // GetInvertCheck retrieves invert-check from context func GetInvertCheck(ctx context.Context) bool { if v := ctx.Value(invertCheckCtxKey{}); v != nil { return v.(bool) } return false } // WithBackoffPolicy returns a new context with the given backoff policy value. func WithBackoffPolicy(ctx context.Context, backoffPolicy string) context.Context { return context.WithValue(ctx, backoffPolicyCtxKey{}, backoffPolicy) } // GetBackoffPolicy retrieves the backoff policy from the given context. func GetBackoffPolicy(ctx context.Context) string { if v := ctx.Value(backoffPolicyCtxKey{}); v != nil { return v.(string) } return "" } // WithBackoffCoefficient returns a new context with the given backoff coefficient value. func WithBackoffCoefficient(ctx context.Context, backoffCoefficient float64) context.Context { return context.WithValue(ctx, backoffCoefficientCtxKey{}, backoffCoefficient) } // GetBackoffCoefficient retrieves the backoff coefficient from the given context. func GetBackoffCoefficient(ctx context.Context) float64 { if v := ctx.Value(backoffCoefficientCtxKey{}); v != nil { return v.(float64) } return 0 } // WithBackoffExponentialMaxInterval returns a new context with the given backoff exponential max interval value. func WithBackoffExponentialMaxInterval(ctx context.Context, backoffExponentialMaxInterval time.Duration) context.Context { return context.WithValue(ctx, backoffExponentialMaxIntervalCtxKey{}, backoffExponentialMaxInterval) } // GetBackoffExponentialMaxInterval retrieves the backoff exponential max interval from the given context. func GetBackoffExponentialMaxInterval(ctx context.Context) time.Duration { if v := ctx.Value(backoffExponentialMaxIntervalCtxKey{}); v != nil { return v.(time.Duration) } return 0 } wait4x-wait4x-7fb9e45/internal/contextutil/contextutil_test.go000066400000000000000000000236071507553353000247630ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package contextutil provides utilities for working with the Go context package. package contextutil import ( "context" "testing" "time" "github.com/stretchr/testify/suite" ) // ContextUtilSuite is a test suite for contextutil package type ContextUtilSuite struct { suite.Suite } // TestTimeoutFunctions tests both WithTimeout and GetTimeout functions func (s *ContextUtilSuite) TestTimeoutFunctions() { ctx := context.Background() timeout := 5 * time.Second // Test setting and getting timeout ctxWithTimeout := WithTimeout(ctx, timeout) s.Equal(timeout, GetTimeout(ctxWithTimeout)) // Test that original context is not modified s.Equal(time.Duration(0), GetTimeout(ctx)) // Test nested contexts (overwrite) ctxWithTimeout2 := WithTimeout(ctxWithTimeout, 10*time.Second) s.Equal(10*time.Second, GetTimeout(ctxWithTimeout2)) // Test zero timeout ctxWithZeroTimeout := WithTimeout(ctx, 0) s.Equal(time.Duration(0), GetTimeout(ctxWithZeroTimeout)) } // TestIntervalFunctions tests both WithInterval and GetInterval functions func (s *ContextUtilSuite) TestIntervalFunctions() { ctx := context.Background() interval := 2 * time.Second // Test setting and getting interval ctxWithInterval := WithInterval(ctx, interval) s.Equal(interval, GetInterval(ctxWithInterval)) // Test that original context is not modified s.Equal(time.Duration(0), GetInterval(ctx)) // Test nested contexts (overwrite) ctxWithInterval2 := WithInterval(ctxWithInterval, 7*time.Second) s.Equal(7*time.Second, GetInterval(ctxWithInterval2)) // Test zero interval ctxWithZeroInterval := WithInterval(ctx, 0) s.Equal(time.Duration(0), GetInterval(ctxWithZeroInterval)) } // TestInvertCheckFunctions tests both WithInvertCheck and GetInvertCheck functions func (s *ContextUtilSuite) TestInvertCheckFunctions() { ctx := context.Background() // Test setting and getting invert check to true ctxWithInvertTrue := WithInvertCheck(ctx, true) s.True(GetInvertCheck(ctxWithInvertTrue)) // Test setting and getting invert check to false ctxWithInvertFalse := WithInvertCheck(ctx, false) s.False(GetInvertCheck(ctxWithInvertFalse)) // Test that original context is not modified s.False(GetInvertCheck(ctx)) // Test nested contexts (overwrite) ctxWithInvertTrue2 := WithInvertCheck(ctxWithInvertTrue, false) s.False(GetInvertCheck(ctxWithInvertTrue2)) } // TestBackoffPolicyFunctions tests both WithBackoffPolicy and GetBackoffPolicy functions func (s *ContextUtilSuite) TestBackoffPolicyFunctions() { ctx := context.Background() policy := "exponential" // Test setting and getting backoff policy ctxWithPolicy := WithBackoffPolicy(ctx, policy) s.Equal(policy, GetBackoffPolicy(ctxWithPolicy)) // Test that original context is not modified s.Equal("", GetBackoffPolicy(ctx)) // Test nested contexts (overwrite) ctxWithPolicy2 := WithBackoffPolicy(ctxWithPolicy, "linear") s.Equal("linear", GetBackoffPolicy(ctxWithPolicy2)) // Test empty policy ctxWithEmptyPolicy := WithBackoffPolicy(ctx, "") s.Equal("", GetBackoffPolicy(ctxWithEmptyPolicy)) } // TestBackoffCoefficientFunctions tests both WithBackoffCoefficient and GetBackoffCoefficient functions func (s *ContextUtilSuite) TestBackoffCoefficientFunctions() { ctx := context.Background() coefficient := 2.5 // Test setting and getting backoff coefficient ctxWithCoefficient := WithBackoffCoefficient(ctx, coefficient) s.Equal(coefficient, GetBackoffCoefficient(ctxWithCoefficient)) // Test that original context is not modified s.Equal(0.0, GetBackoffCoefficient(ctx)) // Test nested contexts (overwrite) ctxWithCoefficient2 := WithBackoffCoefficient(ctxWithCoefficient, 1.8) s.Equal(1.8, GetBackoffCoefficient(ctxWithCoefficient2)) // Test zero coefficient ctxWithZeroCoefficient := WithBackoffCoefficient(ctx, 0.0) s.Equal(0.0, GetBackoffCoefficient(ctxWithZeroCoefficient)) // Test negative coefficient ctxWithNegativeCoefficient := WithBackoffCoefficient(ctx, -1.5) s.Equal(-1.5, GetBackoffCoefficient(ctxWithNegativeCoefficient)) } // TestBackoffExponentialMaxIntervalFunctions tests both WithBackoffExponentialMaxInterval and GetBackoffExponentialMaxInterval functions func (s *ContextUtilSuite) TestBackoffExponentialMaxIntervalFunctions() { ctx := context.Background() maxInterval := 30 * time.Second // Test setting and getting backoff exponential max interval ctxWithMaxInterval := WithBackoffExponentialMaxInterval(ctx, maxInterval) s.Equal(maxInterval, GetBackoffExponentialMaxInterval(ctxWithMaxInterval)) // Test that original context is not modified s.Equal(time.Duration(0), GetBackoffExponentialMaxInterval(ctx)) // Test nested contexts (overwrite) ctxWithMaxInterval2 := WithBackoffExponentialMaxInterval(ctxWithMaxInterval, 60*time.Second) s.Equal(60*time.Second, GetBackoffExponentialMaxInterval(ctxWithMaxInterval2)) // Test zero interval ctxWithZeroInterval := WithBackoffExponentialMaxInterval(ctx, 0) s.Equal(time.Duration(0), GetBackoffExponentialMaxInterval(ctxWithZeroInterval)) } // TestMultipleValues tests setting multiple values on the same context func (s *ContextUtilSuite) TestMultipleValues() { ctx := context.Background() // Set multiple values ctx = WithTimeout(ctx, 10*time.Second) ctx = WithInterval(ctx, 2*time.Second) ctx = WithInvertCheck(ctx, true) ctx = WithBackoffPolicy(ctx, "exponential") ctx = WithBackoffCoefficient(ctx, 2.0) ctx = WithBackoffExponentialMaxInterval(ctx, 60*time.Second) // Verify all values are set correctly s.Equal(10*time.Second, GetTimeout(ctx)) s.Equal(2*time.Second, GetInterval(ctx)) s.True(GetInvertCheck(ctx)) s.Equal("exponential", GetBackoffPolicy(ctx)) s.Equal(2.0, GetBackoffCoefficient(ctx)) s.Equal(60*time.Second, GetBackoffExponentialMaxInterval(ctx)) } // TestContextCompatibility tests that our values work with standard context operations func (s *ContextUtilSuite) TestContextCompatibility() { ctx := context.Background() // Set values ctx = WithTimeout(ctx, 5*time.Second) ctx = WithInterval(ctx, 1*time.Second) ctx = WithInvertCheck(ctx, true) // Test with cancellation cancelCtx, cancel := context.WithCancel(ctx) cancel() s.Equal(5*time.Second, GetTimeout(cancelCtx)) s.Equal(1*time.Second, GetInterval(cancelCtx)) s.True(GetInvertCheck(cancelCtx)) // Test with deadline deadlineCtx, cancelDeadline := context.WithDeadline(ctx, time.Now().Add(1*time.Hour)) defer cancelDeadline() s.Equal(5*time.Second, GetTimeout(deadlineCtx)) s.Equal(1*time.Second, GetInterval(deadlineCtx)) s.True(GetInvertCheck(deadlineCtx)) // Test with timeout timeoutCtx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second) defer cancelTimeout() s.Equal(5*time.Second, GetTimeout(timeoutCtx)) s.Equal(1*time.Second, GetInterval(timeoutCtx)) s.True(GetInvertCheck(timeoutCtx)) } // TestDefaultValues tests default values for all getters func (s *ContextUtilSuite) TestDefaultValues() { ctx := context.TODO() s.Equal(time.Duration(0), GetTimeout(ctx)) s.Equal(time.Duration(0), GetInterval(ctx)) s.False(GetInvertCheck(ctx)) s.Equal("", GetBackoffPolicy(ctx)) s.Equal(0.0, GetBackoffCoefficient(ctx)) s.Equal(time.Duration(0), GetBackoffExponentialMaxInterval(ctx)) } // TestTableDriven tests multiple scenarios in a table-driven approach func (s *ContextUtilSuite) TestTableDriven() { tests := []struct { name string setup func(context.Context) context.Context expected map[string]interface{} }{ { name: "all values set", setup: func(ctx context.Context) context.Context { ctx = WithTimeout(ctx, 15*time.Second) ctx = WithInterval(ctx, 3*time.Second) ctx = WithInvertCheck(ctx, true) ctx = WithBackoffPolicy(ctx, "constant") ctx = WithBackoffCoefficient(ctx, 1.5) ctx = WithBackoffExponentialMaxInterval(ctx, 90*time.Second) return ctx }, expected: map[string]interface{}{ "timeout": 15 * time.Second, "interval": 3 * time.Second, "invertCheck": true, "backoffPolicy": "constant", "backoffCoefficient": 1.5, "backoffExponentialMaxInterval": 90 * time.Second, }, }, { name: "zero values", setup: func(ctx context.Context) context.Context { ctx = WithTimeout(ctx, 0) ctx = WithInterval(ctx, 0) ctx = WithInvertCheck(ctx, false) ctx = WithBackoffPolicy(ctx, "") ctx = WithBackoffCoefficient(ctx, 0.0) ctx = WithBackoffExponentialMaxInterval(ctx, 0) return ctx }, expected: map[string]interface{}{ "timeout": time.Duration(0), "interval": time.Duration(0), "invertCheck": false, "backoffPolicy": "", "backoffCoefficient": 0.0, "backoffExponentialMaxInterval": time.Duration(0), }, }, } for _, tt := range tests { s.Run(tt.name, func() { ctx := context.Background() ctx = tt.setup(ctx) s.Equal(tt.expected["timeout"], GetTimeout(ctx)) s.Equal(tt.expected["interval"], GetInterval(ctx)) s.Equal(tt.expected["invertCheck"], GetInvertCheck(ctx)) s.Equal(tt.expected["backoffPolicy"], GetBackoffPolicy(ctx)) s.Equal(tt.expected["backoffCoefficient"], GetBackoffCoefficient(ctx)) s.Equal(tt.expected["backoffExponentialMaxInterval"], GetBackoffExponentialMaxInterval(ctx)) }) } } // TestContextutilSuite runs the test suite func TestContextutilSuite(t *testing.T) { suite.Run(t, new(ContextUtilSuite)) } wait4x-wait4x-7fb9e45/internal/test/000077500000000000000000000000001507553353000174005ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/internal/test/cmd.go000066400000000000000000000023501507553353000204720ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package test provides utilities for testing the Wait4X application. package test import ( "bytes" "github.com/spf13/cobra" ) // ExecuteCommand executes command in testing environment func ExecuteCommand(root *cobra.Command, args ...string) (output string, err error) { _, output, err = executeCommandC(root, args...) return output, err } // executeCommandC executes command in testing environment func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { buf := new(bytes.Buffer) root.SetOut(buf) root.SetErr(buf) root.SetArgs(args) c, err = root.ExecuteC() return c, buf.String(), err } wait4x-wait4x-7fb9e45/logo.png000066400000000000000000001336631507553353000162670ustar00rootroot00000000000000PNG  IHDRæ$gAMA a cHRMz&u0`:pQ<PLTE---;;;&%%QPPӄ힞zykge]WTXRPPLJ<:82-*51-HB@_[Yϣe_\rkivrpVVWۨyu2/0-0-}uq񹹸/10jdaLGD󨢡omoFFEӐ"! |rmzmj|ű̶ԾԹʴÿye]¹ԸZFDźmWT|zϺ,.1|x_\`cbYABrluplzwur|wxpe_Ёxmfbc訏7tRNS (IigU7#F3(rPEdһqbKGDאZ? pHYs+tIME  /0IDATxc'FX̕Um@J3mNI<ヨP E;T %V@)Ruuz$@Kr,g^L'L'k:dC\wwWU@(8HBwK_Ґc]v޽gϞ{oGvڹn_ސ7x]o߆8x.8<<ٽݾ!mq!\? #|?!eP״],"\#@-V{Px: b$8{ZGǟ8Cax (F =!>uCyD}G}>W #G~/'{~CC Gc=(I:sbAb2EBDdӢZJC,'&0L^Iwu}B;vYX77OpVe3Y cJҢI$C'@;2|6ê^{:<ܵg%OR,Ҋis0LLMGxB|#CG? / r|:+ 6m#M@|nAI3g. (_*ܽs8fx!EYk>חr,@ȇUBޤP('sA9''31By MKI,wv:< ѣo4f2bPsX|ҜEDES,3IH;24)ߍ}#.q3E\R+ϞȁIx8:m:~>ϡN{ZmF-KW#w`Q@Y0к<ǻ"W|{BҎǎw`72B2|S\ܹ+WLx\:}(cx0R|GiǑ}`ؙ^9gfFf+d9l/R ޯxIiP%)Q3Ӈvz:eʕA` W,7IAj0Dڅw zjXb9{.t+W]!\|5ah^P+At0 ]|h#`-&R J˧Ջ ؋Wr`Ҿy穣= ǃxfBpByjy7W6WKda6+ I|e9vB&s̕oR ;KP??=|vo3/ 蹂'N_[VP*UD@D?I=f@&ݸ[yV\Ȟgc(HZsA^~?+9AɼJu*땉j|V^}?~ݿ%)/u;<Qw?>?zcn  ]ݹٔ+UI@N \2R.??|;NQ~?u6z/}죏>?[o WN//Mss`&л$|_ {)}C%]cǢB/~F>-s $(@r)yq鍹}O>ǿ/>OW{(< Axݏ_짿)B}rA% _p~#JJJ<{:'oG~{?~1"oRcC_p{h>P:使ß!~}V,aTY(CnuE-;?ٴo>|{u |p]Ⱦ'F"`h7_'Evs*h1@)чUtMB^2 @ٶ˗󹹷{?~>Һ/> e7}8e/.v}v{($`eFTRfp+ oO>q>y3s5:A/d=G|߻~2;WY t pcxG .-n]J%#}C̖E>鹎0?yڳc-#ӟZW߂(_v PȋSfӹXaDWl9Ͼ|+9.Ҿk7}CcXnR[~b/sd>0"'RL,;<\A~<Ash'v%xs׿G^}! ^ᲧNz}:[M(s X}3o t]AWʫ s@9%WwZkW#Ŵk?X|}w_Zӳ@G \ sµ ×7|_qY([88|p-{%rs/>wDh?|eň6?pcSJ`ِ?觿yN\\ 9.0.8`x w˗H '^]Pۿg?}_,k<10q x!PPOHjA) l">8HL`.uA.+׿6Μ2|A!S3vh!:L 3mS#g~~*+6rT7J]@m 6GClQhS :`n +QrJ⣟Hߔu-WQC uҮQ8SS\?~;m 3Bhr0X[qhZN)7Vޡ:ڋZlr[8w?z[yXDH p}H℀\/>d\Y[L}-Tݏ{UQkl m?o?V"Wx72r|[E;G)'_z \`0BC\{iԔW|>\ =VѮG, ;S ma^ZSeW08pH1/ q|#>ߞU7B ]l.|QO¡rqZJc?/,ۺC>tVƁ)<{l p{X(),Oǀ8tO Bv#}ji*e@b|â-#}/u56J$?S .hՉM5^}Nx[W1 $ l<^xQ% {ZJUbe s="%^hw VyV[C S<*pߵTEt| ՎvNhgn RIGM$:*h E ;?Ϻo]SDc=^l@TRrB ({ r9 r"a>h /n*~ M`{Gs削)t]+7-/ +xj;S>߰*hh/&9^n 0ßp)Z`x-X Dv{όq 3S52 "žp-Q6.ˢ>Ѳ^/wtNmkzo 7X}n`:Gv -!2A߈Yrᓹ˝⿐_ ~h^ 0B7o nK, &'Tu'!ѯL5t}D<4VЎ}͆c[|5!D'/wH1 {%>8uC?pK%z:ԺlZ)rȵfϧeEP=,/u߁FFbW <<6~ o*=)pt'*_@HE;@#[Bd.k!7ۜ-yM_[09Raρ>+k ' (e-vf.#a$h+h~}#ǞaK4MZGW?>+3FNS @uUcih׍Ű؞5_s T:W,r#pCl"` <ߚ5@ﴘr1"r'w[8`/u@$HQ/[w-z_.3<;<kW`} jǰ'%Vv1 H cWrON<7['̭A˿mNEH9i75PG][λ; DP!@P]^VKEjӐh6%6" @Qb^OZd+x?bCC#`cPO͸ey*˧rtn=Y/Ғdh-i++O$%?@2.#s/fBnt+h+*ZԒuYQJUp eEERvpѮG)?O}c1>pƱḀd64NzrUQ4- \Z~Ŗ3*7I= ~>YWr~_h!sQ U$TQ[F`uzVh R Y\}5"llrJ&"`XI<,<؟ sւߠi8榉 8MNS{~=}?RoOY/>/nڮ*rEȎS}%ُW)zqij/}w9pϿ|>[ζ_̕ۥmBggHt T+X(L$=DיLM>~_[𥋙ל,v}ׯK*\>l|FgGww@*';+o:nYZr~}rgĴyͮr/lQ?mzl&?]a-$O\xS)/r;on¯u;".,WBk]_|_<&rY}UMʵO_ t &Pƍ7}ZZI6/܇{ 9=AދYqCtK:*hN@B &&'NNR?芶?u0GJ6}?rj&QÚMYB&3G?߬c_c2uK84zjwD;zJ^\wҥ,UA,劗n{|+}q\ ,e3l^q y\Zll^.rk]_<'mv(_0j廿y?jepX/\(sy TѴd`xN&Q.ǴRQ~_oYd3cGyy.{TYw{ǟO>w4x\H -tYֺAPOIM5$ rI a?g~B Gy!)dDS[` o~o.d+o׭V0Gʔ=2 jWDY w\2?/tf:F9fG#b&eeSK (XMVH}uT\X|?7??(rEXv4 )Mp\ e1YTʯ_+2IN9{s!Hь'9 %^|I= 蠟}~G??(W" l(FdžɠMT02I&*$2K.6_ÃN(LrJ20lsc390t yI- UO[{>>ß;DKE8៎QԱ4&cclȨZ2K]C>[Iœ(wk dZn#"P=?}ݿ~M0Q˔$aHQޱQa c,Ǥ 5Zgk q^kEʲcj9ח DqQW$gqN u6C{)(OC' $Yw@)"Zy@G j dbۨm6ZAq1\K=2q’1/rs¼bs Z?P 5[Ȼc\GNj ^&`5 zq5,wrdŖ yU U)2YA75CQE)l1fH33cÊ ӑY6;G[m=n\; C$UeMR@5? l$+(υUIz@~)uO/4| @f&ee8hSv*/ulp cO0/UA7~˷dw.OMl>*|G6H$@g-8mA6Dn`-/W-N,u*4ώxP*r"!lB*+$c\ ccá"R J91OnDw q5"mQ(&Nd߱a:`C󡨧3A\:8o0A9T\xlHXMUGM2Rkf<@R&-p8W3Ir 5h\D])$8_tL 4 矓8Nn$YS0y A"dƁ.0-eLҒJ}$طUmc4t hR䢴Wf`3w-eZB^ ҪKcC~# )! T7pxSRdbX庼g>5Bv@/T;Ǽd> bcb.?S/}."r1V]EHyc5_3O<0:eo~@53ν&TŖ R[oA__pv V8,Rѫ%JIcz`U ~l6:^h TУٞAK>n<\]QW@DxX?sr"&0*j:J`IfK]dE |^ c? n1H6sL/t8^w0`W4'J[HD]vLk'$۱pPƽkl?нP6tGw@p13?FQ 9ǿ *ǫni j"|:DjG4`t | 2'$T@E+Stb&@[ Yء!6@_T(P䄸+LEV4E[~ƍp˰.=IQ*Z 0]+hf^@03u!dNhVs 8N*ݭ> P,z%FK #JC,#i[r~~wq:40Oq9M%8xEMS"#k8PLG 7B;ڵ0HV;υ \8SAVQ|,=?%219 uPMHE_pYaV 9N֘??b$QS 8s>kݾ!a< rT:GȊQSyA0k@' i=E8N D>FxVFUGvfZUs-~C|xhy{)#xn6U5DsV FYN01MTm퓤1QV Vz& y LA 9.2O$Ȁ $K  ]71#g_/`| lh`9}6-83->(xx=l :vI70SàllrduhoR ; D@LR۳@6C,j/E@&8!QS۵wRTzL٩gOz1a Aq5I nRK'TrhFLlo>nwsդ&l!K`WD1̒C:q1RR][?2 Mv7"B4%P 2/H6 {]QOgs7y~!EvhB-)%MG-S (t`ib~ܒְhݝz$@\1 !K|'}gxQ^ڿl_:s5wG#gΦR7*d} v3n.e1w-Ui$5򷘬H$`X7N>|1$m_`N-$%CrmNV, ˟Q&s ESkb.4K*iM|; $$I5-)'Ob(co~A䅠"\X^ֵ=О |Jl]i 5䄧M'OOdHh 3i;nxDkjRZuXhM E}ƍTHϸ'=QsD+Ԫ1`I_i='#pI ASϞ<:LY\~F !736kНL2{~lCAѴ23f'Ut$og{ N# 6`SDb jf [g(Gզ Z_XFfDTF'_`Q,3E@.K! dRo8)1*F }]V+zE! @TVy{&h}cS9߻qi8'U5dkmczF[CeDckL: !E`8QuѮϰܵ9s'DO bX}\Rwă*9\l:8Z aFdEUk'd(wt;p=cppi?]xYE@_#|lI%P>p '][@I \Y򺼀"TtC<f^.8Q2d4@vŊ>O n⭌]@E%WE4.r 0 ,̬OpT("*>j!8)rA0 d 8_ t>?85A1@MUAIէd/$!4ɑiᒕ_YA'T\ *^ M^2 zyA5A XX bz cipB/PqNb ֎UP첏adQ_G<tX06f`620((jKX,0u@EQ"܂8 P{%x>RC6["5Kļ7H8oHm4`y`/^?fZR v~N8qy2ɱ<"¬U,(Lߙ&se *Zh"Y2Y;++U58AJK&/f3OP#`0B:Ν;'rB*g`_ȱ mYCe%jB#3K WE ZOģ}Ä3)8b(mR !0ޞ@KR+.l' Æ]8gh`R~HP##82n{vx̆p rUZ yQ$YkhI0&ø8=]$g+׳J %ugUy۳n{vbc8bmZs!(M*ΑN,M'Ɲa' eݚ Tl8ɞE=xK;S"] *U Ork@nޜb1F;R?N7G5Oxgphu0RbqIqn4rIP+Et{O=4T]Q6<73W-a^`U: `!P5đ+Fm܊`=&A[M8\"Yc 13ff2w vP tef7WT$K8MJ֐9heTOA4325x!~VS2奼C;C<3Ns/B 1}sFePɲSЊi1?[B)pA`Q4 uSsV#",^ I¨Q9˶#g#>p&8\!f5*+8פܬ=6@bQͨ5Ҿ.!,) 4 a0ljbUfeV>7dzDEϝb 'x3@1`Y$)PL1 (]=n*pUuuF8x^[MBiZYU`]NMj"2eTU TC<;zE@^絈@ン `-u&K,+qW s U*?LXɩkT15U_CR*1:!{f"r~Ptp'p/O 8̷=S 1]ȋwW, R⇏WZ Y6¢ꊲc4Q,݁yZ"FX0%WͪDeQBz(-R<<2K A&ub!0_qq;&B#j.b~ɛ?X*(bୢT*bCoHZ  g. 0J^5.YLRIF2#|ŅSz\]ÝA8(yL8O6H)Ȱ 0q0^Plrx4Ƣxh=رB| $!RQӏ[na>`w-.JBw>Iۑi\H$~- &`ɕ[hŠcUe PaQi&r4RU [KFw .\7 jP#)'f]ʚ@%tl&`}|W`` NkM0ta Z-ddhzk`f K  AwŅ vZ$  ~EݏR3Yn\%3N8r`*߈Ov2Ee@l 3U7ogb]@~(NC\_c+Ddτ]5 0?sOSph/P&EmjJ a1Ɠ $2D{0UsK)SaL-`;[ ]" H1<456R48iI2K;$Şs@0˕) E/Qⱔb4imNy2a0 b^\Hfd3c)'FWu3ߌ&^2LÑW j U][{+ ppB:5dbAq:`;@ʲ@I6Kc.oFgs ,>pݕ̿QKb}C.TA~ =FCW@ H TԴ0a888O|x`dDg2HIr$b%2*"\m`&@t*Cb БnUCmuu?6(٘J_U"Wj[ӃX$UJ;|pCQaEGRcd&,8Rq j,AriRHxa\̒ S)?x  Ju>d xvZe; ;# .9\7|I=\!cTp9t '@|a;v<\6:\ `Uy9!+-Uj͌G >YEB j54\2؜dsl%]?P3{ k z6LEC|b?`ࠉZUחoӶ@J37/RC"?5pmўgSr1L=ںbBGrdäN$,/}VVr_c2gװqH#Bϱ$8EeqðR*>U bo,?oRrZ"Z-3 o @6ڊ1 a(0V\nVhĦЦY&b_g1 ؋ujv6Fm#@! _x0;y)9qΡf[{:Φ-&p Ziɡ` 3d㘤q}Mݛ{|o0K2G" "Rͤ. <\=+a~X 9A;a8x֩NiF BXYcciImmdo#IHx:iqbJɬ6 4jM U3"j}n=|?}}m*բ9pMJ]k!,~wE\T`O[gz$S|h2!PA8פrl k+զ-#q6eL5rr't `塶5mTUS*O  Pav+7g]@D`]jɀ|N ք{<pfꟛ G#[E[a9RA8 ivu3&~MIJd][0x_x}#sW]?8]e`-- :SF*p9!.†ӉD7xԋdFдq{ 14Z8;^i74wUwLkxX8 B(7y ]TvgݻZѥ_WƊc _ʠScuZ-5ew]hGff~&FUt~N*& c[5yV|~lԘ:]t=\@_d9P# <u21[A*rG1w3w0ࡎs|r~w@4Tt.Z ܛ9en1PN A'|ji!Ŕdő_5"v?\Q'0 YeeA 3xpG7 Ιr}UlsH-/׻$ [n`PU*ǠtK4ۈc-h !˄;P1ꟲd; W+@Vj VI7  )'Ȏ 3Wmm  ,[J'W ކ4𸱸, H"B&E}Av?|( R U@5̯4gZkV JTgUGӔ"m_Ýmo|{fD﫭"O穷'1 L\1aԀB{AE?Ic )`6H@<( @@]+[5]cZJ*3f\~neqe.(bY ID:|UeiwCOrhE>4km}#3bR/HN|J.U}֦gxX *2V %lPu3 qY$(KĵLU\)@h|@~ N_l >0tER:%Cy*9B.=QA6~W!쒂/bK=B BZ}P "G_ P?V}gnR+` KɴYOtfq~\R8@/ty!klJ&*DfrilUһٔ@H9l6;ƨq95h+DckDD2` 6̎j Jث`{*0 0'XK#`t  ܱg:|ڮ@|xbSЬli9U|. kg|/1|vYk: D[1VON(y) 0OgսЛkhwZk S0p[P[ |yi^w2k5e@ oڲG[_EU%\1W(1b:Ӆy{Kv 5>;orG ~fy/H$N=f'(IlS]ֿ5s:z27} ۇ?0 b! 骢- S^p!`挝>avy-;x f)8 +o7o,-8^Xk4M+9H$2\[\5CPREUwR3~9c7>۵nF͗JJ?p X<iiָEr+ۊ|;O>20dڭVjAsh[5s#PK͖`|+~DP3:i "3]6@qbDŽl4oaKIx++͚32 "ݪ}r~\ ;Qm8Jr=v}_`%8U ) ܱ{2$+FŰJUwZtˊQ9vT3 pf9/5+;6B04̬0 i6‘Abfan54gYhܪ*0e ON)2k=b%ƚ $}e 40/SA5~ƛX[]`$EW܇8[j5,3dtv"D ZAW+ &Ej-X=ʁkvM?7?q%Zi%@Wq,3HưȊ3t=r- WIu9%+ a(\!woN' :$8 8 >".`=#&qڃ!<({.dG!>% DW4̩'GDppNd\q`5gSf|Hሯ8ԪB t;12grl/?&DakD's!e 9<6PO<1 wFgV3hAC: FG1 bewMքUVw>C$:f.[cW@DDSn7k yw|05BfZkW8oDZ L"^QOSgVY BT+Zq @ZC^. {N\MO[i r] qӸR7+6 Gv#ZcRji:kypTo޽ցHgCS]ZUNtPV\ @5*Df0B2m0O`@.[%0J.Wӳy`t6p?~lhKwOpiPsZ }ltg  wU`i*hI: e,VחKlu]@~J˥M 'A݌@shp+#<0 f5Tv9|MV8<U|@F{' 7MIpk} 6~l shW@+@@MJm* رp1sܹ\bN Һ xp G—l{Y@P9~Q@u@c^dzGQ3ҕr-qW ˕48>TU]el$n1U"7/. /qD!-'X5y)Y_ D6X>zW;`Ei|p"9sr&:l帠asn?c\kN;қs9sFekNe RkyeIdxti`+ ! *̠}KQr$8طgpF|vmnv/d\;pz%0%\qU4rkACbq־4XcÿwioQcI/ `$ڒa 4%9bq|6AX!2R{#Wx Qۜw!`Aq\[iF]+ @NQ<νok$ֶɰ6,@,ej4+\-]UzPG[ja~fXFE=tnp3a͞1jpC;U (*b!@#PZ7=HMΧzP0BfZPXL^.h gf2SأGg3`ף@R;I4<2oxTa͕k ''i^`HۯJ@ƕ|$bjQ樂b% H<&p^?v "CN`D ɥYcH1(pgrN&0_]irekZ8W&'jntvWq$Ή4R7V!r`(b%&e- \7F8>ZDCs*#3dQN >ԌfgX\ +`{X^Mf;%yxEq"؄QZHik',k5G&SwI 2E0cG,7N9#$YQ{rZK9M}1QJ_ ޷|fX9n/ ⚪ "kf,~\+Rԏj1:nKeqooAvA;cfF BQSٙ)Eg _)Q! egiY ^rgce-đs4NMwM"]9 3qiI61ȅ--{Tn,y-ZS)MJ7BH,^x|*vQY ga<*U"MDH` )b.]rDqsay).n8&v2 DJ2{:d) NG@Tӌ&*Oڶ+ UYIMRۼOyBKʅáɞ0 PYT88ߐHҵ|^w^$`ukA+*~/i ]/o?<5I6񥁲{qJkX2jdWY# XK&z6⦗(ڍĉ/07K؄ݍK%#lZJ.#Oxqtt`Aa5!+7@+~W%NEyeY9:m8 D {;()ca 1+n ?=Q^#+׫\H[!ņd7t|!*?F$E*s8܇bG: 5ɔ%[˙E9L84c=NN֞U9)^EN*B8aB 1!#¸Q ۽:i@f^1d0,pV.ĒTiI|NFcJM'_׋q-c2T2g00c;L̊\_g7 6,0\nrYꥸ@'wrc!`. .,yNhE 9\,Q4uomp+64B|3>PѹOuըu γ 0_<vg:`GQpsp$<1 >1ʹ›yZ |0:>>$0Ηh)gB 0 `U:M\ MӟJ#|ZrR8Z1lLI?> xQYF84u9%dgIJWNjA83LR/'{t>R tMW 3ɣF)qX!Ů5s((`G50D;tJ(V|\:<)T)vT@+MƮn 81n CDQI r6Qq3,}m mUgB[Rp9I˅ ӕF~-!Vu|$$ MXm G Vuf X\^^m@"\Yk9Yr(DG8~_4l0KNR-uVҕA(nҎYCRG_ ip7㹐Qx 5+ozmL7{u+D[P>, ^@ECv^hqCR`W${T(ܭ@/&kWKd MU]CLtyn7^~ZC^Ik c l O㫙FiY%,A =N>Oy>L4[ٛu>R?X)lə#pNFI)9J/pvb~0и,ɕbQଇӯ'OxG[K~͐4,EtFqMF"3Gu_&O&Ѭa{(fĚ R~o=_jLs%RwnsY$Xr\N8[Jʬ f͍8 UuVIF^ HPR;w^_˷^OA|$VX9kĴ)4d}}Qjߝ1̊z h$DϰOQj`I{ʱ2鶑5()%x `\eWu+ S995=a/|S_R3!ģ,ǜ!\byS\+%%hn"K!AEY""$TS('v d%W!ǻEv=~򤏚}k-㼔#+fm$ AAX䘫f4KH)eDc6BރT4itRyiav`z'XF+ҁvxzp߽e\ʈ֔H=H]$&Ix*c0Er[ۆG}㱻:КV/QW#f]`?'E>@6pK&Sb9/B,8j`KWydUNj4M[vh;Rp+$7ͦ* ʔuԻ։ &-M]1i0naK#"1=F[ B`ڹ:Bl'Ok2IyrydS縒sY9H%f?!ǚc$v$BHٜMD7wihD/ ruَw- BoLnX0CW)RE='G}֍fjZԚQU`J`Dӣ(ؤ3kJt`bDQI!JmD#dD4%(>I"JpJ"c;cz׃ZUHc:j0Y?YzAfKаq|#8IE=>z9+46rΨtq XSX J<6KINZAJ R}Xp'bz7 O!ĥbFi1NoZlJYk`-!4 0М` 50GAFI} pD:\P- t ۬OMn%Y *0d^cIP?h)K䐢O&D0GC 3nGIEV*i6XJK/~=/kV7 4߱" `PP#Ll B6zG=KpǮ=6GXaQ]2 8BD͖@Hm.}4i.ٗRCal}Ơ \Nd6P/6%ljbIqU5.}DHaAJ4:n8^fcߠa>Qݩ5-p&;!{ܹw?Q|6(ow$I ĵC+z Q̧d^e-,5"؅01_6Dqx(rg%xF4aH ^U`},,uV73`~I%r]Ø6w@B&4"B;|ه=>%. sDH:Mr3^D0#<q",>U0tZh/@!!㊂Eb1?>? -8=@.G^yxǨ[P"8J48JGGA;<|sSY^`Q,o[#ydp:RauX%Wֿ;.*͟t_ L6^Du٥Ilϻ{f ~hgåajMȭf$LfZg8.i3y!þz:(\mu@I3dK+ً,ȁ\:@l/6+NLA) {msJ?po (`Yi*'xPȐj$+.`vqOj:Fs+?7f~n7BDvxМGL@'?Y=k.EY1qكim-PWm̷#4wL@[*֞+Lg.4.+z01Odr+Wá߱`C02s-!5^Y#,~Ξ>{zvb|W 6f;jZ @A. rM^6/DgFf,w. /&3=y[*߆7Y98h Xoid(7e}{*C nF"6F'&X 4n|Cd4QOLIx*t6~-wu|]F-f=ZCPR`Y09! fz00*1Ez<pt@J@i!s"d:-r7rs K㨟q?-M .pJCPeC)hP\D HGM[G\HIrshܨ ӇlV.(zn( h6Q3h4 X]:)tk$GFx# 㸷YB"*1[k,eQ|PDXjA\ºV6;HL`" ^z |ft \A[Ax$Xs_k !2;=3#g#vX O_dFxV_lRhTxngPImp{ +Tse.G5 eoy~(Xp, #=d B @[8Y`OJlC@b +^ Q p\В2_h\+]hCЇp Ee>|&;$WЌ eFlm@ټn4Yz$LlU${_2Yk\b?05:d M:n Kj IR &6k$ZݙAzAds0(Y9NBq\^ 樗^-JPS5͂RLi#F܏" 6%*" ¬;kw~?߉ Ϧ kcL71ho^}Dc lE%/~j#1"q+7nTZܯ,@kJ tțD|N2o![ #dFq r?1>,+6MjΌΉ8tG P4_k?ޞ0N7be")t]USWV + Z\_%j FR^%cWAn(@DW^y̦#XŌ kS,H`{[d1Bw_;\{7%,j}Mo H[aqwt}s3Z22 )ǑA1543%&6M :NjY)Dɻc^dgy$囷I Z:hCj㮒"Y.:c?2XQEtioxX)H@/eEBTd6Maԡ[;oI{ jpd(X.s.1LܕKy6Mk@ˋNq՘&z3 Įm?cWMC@tkF1ndEw]A,Pմf~^D5Cb \:  ٳ~$uFG}BW>:K\~M iqnJ; aGL]#β^(rt< `CX!dZh5wc>5Z= @̚(,hvJD0 qϝرׇ+X'Wr_VٲFްb$#"*Yf lF6w+DDq2()m$, YDž⍎Bu $uQl60D1Vgj^h\2~ѴU' w7߰N;/,Jbm7y?;YdnLsފ PݵRt䈊q0MZu#D[v 3Gbc<3$q|pʗhU@pIlTZ@כ,P/g2_OН0@e3spH,@iM}k` ,(fCG _NuX7Y޵(Y%̘)]-bjmZ@kR Y Qs8&@8;T(w en9i>;{w 8x.Y[uX^X4ArRGT3E IvZ拸|d.*V"}P ի` xx Gkx%\,T:`Y6`'Q?a15hԍw7po Èd ~uvaųl2O˝s !|X` K33IDAT^uSn?X72bXE-v8L .٧ p)1o62 l_iMMX+Zf*.2h]w'-(hƵr)蠹B#&;`@\O_{=wN"`,ǾNȁ0}9r$+1VfWK:H,+A *(b]=Va` NKGbJFF3.w|U'oz*IGgNOݱARw+]vFnCț3s ܿ; WP8Êa 9ɢviYǿ&ۿS,* C*,}MOJ(e˖ \_EN2)g ^ p&sJ?R |7nכGKde=3'˔ĶjNkW[@N:;y)p /pb8pInXBcrSp2J`wG\LuA\[iC1ޔAf*2!ݡ nvMxp`ӒA4mt(^D2fߐ YLKi@쳂OZ9:ھi]Y* 0UА6Dy ,Go^)).mSFN2k&4ap# j텃9_$3"3V'긌V(wC1ڦ6mLms d}qeY] r0"NOPy4pcESc.eLurCH~w0k@5Í$:`UdWvdAQAgXb>iR͐R/b ar]GF0GaA6ZAI.eOO}ckENR<ݱ]R>(%Yq@ >*$B@&'>b=N\Ȧ_TЮ%h<>= p<j5-1!:&ָ-(+H8 A@ϯL(@6՚/室){p#$\|}}=_R%*n͔"񆊉g7b oPv{1]QF/'j7 "k@]="|E=fR:) }F_ɴ;-H׸E>,ߦB4pCF`0]\󭕕D\!2}SCڠ+ĊsyWn]{I`vLLTȼuwp*V\mEXz^Z7:jU!M6~[; TMh4W~ϒ¾ѲjM;Dd(;̟?l1ؿf?Lo?6d x+ nӬY09H@A j !MŌ^[wo0tpًϏ#`@*A_ݽ{(yk<(1/YtilV43lYLø]]^mK74g~ߓμޖd.o;f m  \y#6_q^ov9M+#oؑ=RP\kDە YqC-N\dDU?zBaBxQgxb;w̓9:vUP ݝuEVN!J͸y G0;c}r}P,fPڸLݏ&KVo+sD>8kqGͲXܺ:Jjۓ__ YXg،qǘhhC5@_L`oz]}|k,_Cݿ=(ZAͦwɓ>m3P5ra 4 tZ |T9!.%o5X.7`klxsXNZXˤ_\u6qe`6AlS0xףd"L60AEi@D-noLXϡ> RJu0APrʄwn vcD& 9.`T]8u?\K I[ΛdzDߏ5hKBLqW7zTDhFS>Ԇo`q4+X/9lp0gig 3rXťm@>_LdV3/L?p;|#\&h[K(rȹD\czL,r0L< nɰ! V-L\|R+d 08r@`WRꊲ*04^ ^-y͆H&P%H@mcg=v<]b9~,X?锵{V7H {w|+Y>j@UvG zv& Af{}$ $< m z0>d,D ul!.P\A =E`ҵ@ mplz< +$4x=eY(zNn@pmi\oot ,/3W|k#`;2dz^aWb&nMWMnʵ;^4>K$pCAVa|zi65.WgՅ6tZ5A]F cDxRlbK@ I<۬ |ʜ[vJfߝj0%Cnv@4w `YKEcU2 R=ҷ7#w1/fpi#0 $IYЍQHz37Nj(Uٹ:DAg8XnoBJR7+ԞpWȝ:`pB|P.l6 D x|^ T!n'd3^0 0trBz}|[MĨBl vcUx`GI}tǽ;kaNr׻UIcP,Ϯ+lw gij6$g$%z Wʹ{B^Ъw̪yO=rd{4eW0h|kJ]2w@[pts7BS?#@V2d@v`#R6jH5` W{Bl8Fh:vnhlemGIخ#>"N JZl;' |n3"޴VfG[M@2X>{<-pS3ܪ.*Rv@Zz4Wt3ȉ .ɾ@̐@V~UUWap<#j ;ou|* \IX.Xx}zUbܨ3eÔOmhvlQ$"\JqU m׫y-AX #d.^{'d:s>tMN$fbgڵ1 OWR{"tvdMd`W:&F'WUnL;B d9`4|+t%uPJj@QUҒ4BYO=-^>#18{hMn_0u # \uȤx[<@6)Jh@`"Y[>8SY^Spk5{kle0y: .y^|-BUC]z/S0C@a2/Vpk]`@W_E/~VUB&HlӰ`, &/(mn! }BzKdzӽqRmT"F!@P\@l H}`fdaf>b<{o)1x(IҦc[f@ *~_7*X>ͥ?FjV K @'׭ȗZ: G75ہe3}dGf` 8Ck˴pd>Xz蝺d0eC iݫA5bʞ)Q.OKoSV@߉.+\K'M] &WNdM@Ce'/ Qsm P` fn㕵*`7@߫mUp>>Yol˚Tq]8=@2qRwV]Lmn+(K6 )I }&;Fࣛ@+r@'آ}5 Z2nfVWk@fxh#gHUHL VVTCe PĬU8/t7w2ͨTڔ /V4>@ 4Q8ە0i~}F9cemF}!+.D裂CN>]8/w&z<,G+!-kGy~6P(EC"r"m兼wb`v~޾LbJ@u^,cmQWOBjL }w|O 6o++-XT#ΦA42"2DO`b@u,aT-%H ;&jd3v*:~AcwŖ1[>sonqkp9$d/A /}鈗SA$+4=^(ǡ>4A)Q/@14i'~2+̨0 U׽n{X9F5ZihdOxORҎ6 Y>\m`*V$-8+ b?rfJx#Ek}k[ k郮+}f0=my h y#.%{<ںD@2~|pӫw^V֩7"`!:F"K}SWyL 3M +URБ%.DA+y @7 gXZ" T po@2fPH@hq<tP>ʃ}I |ÜQ'R; fF>:MtK[LJ^I@R mFsKQcdF]g`U$7,>h=DXtoDغ2c5aGx'(tp7F}uu~v 3 ,5{N`{@4||0p'ؘs% kaN(dnhj;`&"_r?iwV:B~Y!h+.Z?~H ȿgV%Qus2 @ PO |DTL 3ص-{w%@U.֍;#4!D4Y^ OSG< 鱇)ϟ3paU2APVI |ӛy`xJ @5$-"$?q_GxTErű6*OP 00Hu}?uSw^t~ iJ o0W?Ā5{u(U26 )VaPT__+?G*z=fX*@{lh {kY?B+*JAClLź8 6` J)@ܲ @{D"Ea z[礡JFu@xss3@ؖufps뻃p~ۦmA=!/@B |(ɘ Dao >Tc{0i `A7(0# {GkF(&If`%?s^|Ps>}ُNK GyxsOंU\$I؎n?`$V3VAp?%7=@?:#ESli/R& OCdtI|c?Ac{Lxf2EmH]1}J⏢B! B?쩽_عNYZI 0PDƃ@`0 }LqIe?AA=$f#Q͌LoB\`# дǺ= YL6vJd]};w0+b[0"`o0~hNpYB 䫧R?}>#O~fL5zu܉0;moB@@0WlMysKOk/>0P#OB@O_95!{ Ժ2ƌm}'Ѥ-[E8W(d7Ey#yRޑǟ|<Y#v> oɩ  '4yށ :Cm6Xܐؒf4ѸE?NzW?^>-_^;ǜAFڷY9 _Дnn h`ς|G?~{~6 'AE,8:`xK \K[r@bq@xk.2A>')B_c}~ ;vڻ4/4tKN YNXɸI n5ɔwk ^]iGi< '~GoC'7 D_;'`#*I't9(vK#{x>":| 'p([rbE=;C9<7; pc_}h>VwdSXΊ. /ئ58ښe8ܛ8۪{x ǎ|}}` H$a\McР3i~2 a7Oymmm<Ic9& ni7 +@i:F X(hcPm^q.n"͢*ϗbY_}`߅9D0hF8K7$ ^66@`{^<3ݫa~GrE7x`$QU|[^_{S O?uO5eלߍH{?6ESǓ. iT wO/Ku;rq(3Be5Ё7_z[ \~ X_o\)?Y<ʅѣFwlՇ<M`64a[zz*e!c3)׾z<tOz|_@|u!?̀cs6+IGҡ9t& QرccOt{XIl&O_5[˅:oGGGk}#c|=o bwlÇ̿:öa=;ٜ%rcNYhp2ym;O>7 {ݭwOdBe Mybς' 3քh8CSUwf*tHc6l-zvܞGlmWIO>{#q2UCgY(1:NV jҲe2"0sth3 Р?ZT9Np&p{k<!:|ֈe>wS0+aaCDMixnhpFM^cj+^ =y+LJ{ځ}C1 N>Izy<ԩ)b mCL4Sljw##>P貑|kpݽM_O9C>oN`菐qxrwXpؓHvʁUo#>a>q{?3C'w2#X΃? GO{FVc0#_ka#8 G<:=` R}p4rC?4@<~]}scܼ,%tEXtdate:create2017-10-21T15:12:47+00:00%6s%tEXtdate:modify2017-10-21T15:12:47+00:00uxIENDB`wait4x-wait4x-7fb9e45/waiter/000077500000000000000000000000001507553353000161005ustar00rootroot00000000000000wait4x-wait4x-7fb9e45/waiter/utils.go000066400000000000000000000020471507553353000175720ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package waiter provides the Waiter for the Wait4X application. package waiter import ( "math" "time" ) // exponentialBackoff calculates the exponential backoff duration func exponentialBackoff(retries int, backoffCoefficient float64, initialInterval, maxInterval time.Duration) time.Duration { interval := initialInterval * time.Duration(math.Pow(backoffCoefficient, float64(retries))) if interval > maxInterval { return maxInterval } return interval } wait4x-wait4x-7fb9e45/waiter/waiter.go000066400000000000000000000142441507553353000177270ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package waiter provides the Waiter for the Wait4X application. package waiter import ( "context" "errors" "fmt" "reflect" "sync" "time" "github.com/go-logr/logr" "wait4x.dev/v3/checker" ) // Constants representing the available backoff policies for retry mechanisms const ( // BackoffPolicyLinear indicates a linear backoff policy, BackoffPolicyLinear = "linear" // BackoffPolicyExponential indicates an exponential backoff policy. BackoffPolicyExponential = "exponential" ) // Check represents the checker's check method type Check func(ctx context.Context) error // Option configures an options for the Waiter type Option func(s *options) // options represents the Waiter options type options struct { timeout time.Duration interval time.Duration invertCheck bool logger logr.Logger backoffPolicy string backoffExponentialMaxInterval time.Duration backoffCoefficient float64 } // WithTimeout configures a time limit for whole of checking func WithTimeout(timeout time.Duration) Option { return func(o *options) { o.timeout = timeout } } // WithInterval configures time duration for each of checking interval func WithInterval(interval time.Duration) Option { return func(o *options) { o.interval = interval } } // WithInvertCheck configures invert checking func WithInvertCheck(invertCheck bool) Option { return func(o *options) { o.invertCheck = invertCheck } } // WithLogger configures waiter logger func WithLogger(logger logr.Logger) Option { return func(o *options) { o.logger = logger } } // WithBackoffPolicy returns an Option that sets the backoff policy for retries func WithBackoffPolicy(backoffPolicy string) Option { return func(o *options) { o.backoffPolicy = backoffPolicy } } // WithBackoffExponentialMaxInterval is a function that returns an Option which sets the // maximum interval time duration of the exponential backoff algorithm. func WithBackoffExponentialMaxInterval(backoffExponentialMaxInterval time.Duration) Option { return func(o *options) { o.backoffExponentialMaxInterval = backoffExponentialMaxInterval } } // WithBackoffCoefficient sets the backoffCoefficient for use in retry backoff calculations. func WithBackoffCoefficient(backoffCoefficient float64) Option { return func(o *options) { o.backoffCoefficient = backoffCoefficient } } // WaitParallel waits for end up all of checks execution. func WaitParallel(checkers []checker.Checker, opts ...Option) error { return WaitParallelContext(context.Background(), checkers, opts...) } // WaitParallelContext waits for end up all of checks execution. func WaitParallelContext(ctx context.Context, checkers []checker.Checker, opts ...Option) error { // Make channels to pass wgErrors in WaitGroup wgErrors := make(chan error) wgDone := make(chan bool) var wg sync.WaitGroup for _, chr := range checkers { wg.Add(1) go func(chr checker.Checker) { defer wg.Done() err := WaitContext(ctx, chr, opts...) if err != nil { wgErrors <- err } }(chr) } // Important final goroutine to wait until WaitGroup is done go func() { wg.Wait() close(wgDone) }() // Wait until either WaitGroup is done or an error is received through the channel select { case <-wgDone: return nil case err := <-wgErrors: close(wgErrors) return err } } // Wait waits for end up of check execution. func Wait(checker checker.Checker, opts ...Option) error { return WaitContext(context.Background(), checker, opts...) } // WaitContext waits for end up of check execution. func WaitContext(ctx context.Context, chk checker.Checker, opts ...Option) error { options := &options{ timeout: 10 * time.Second, interval: time.Second, invertCheck: false, logger: logr.Discard(), backoffPolicy: BackoffPolicyLinear, backoffExponentialMaxInterval: 5 * time.Second, backoffCoefficient: 2.0, } // apply the list of options to waiter for _, opt := range opts { opt(options) } // Ignore timeout context when the timeout is unlimited if options.timeout != 0 { var cancel func() ctx, cancel = context.WithTimeout(ctx, options.timeout) defer cancel() } var chkName string if t := reflect.TypeOf(chk); t.Kind() == reflect.Ptr { chkName = t.Elem().Name() } else { chkName = t.Name() } chkID, err := chk.Identity() if err != nil { return err } //This is a counter for exponential backoff retries := 0 for { options.logger.Info(fmt.Sprintf("[%s] Checking %s ...", chkName, chkID)) err := chk.Check(ctx) if err != nil { var expectedError *checker.ExpectedError if errors.As(err, &expectedError) { options.logger.Error(expectedError, "Expectation failed", expectedError.Details()...) } else { if !errors.Is(err, context.DeadlineExceeded) { options.logger.Error(err, "Error occurred") } } } var waitDuration time.Duration if options.backoffPolicy == BackoffPolicyExponential { waitDuration = exponentialBackoff(retries, options.backoffCoefficient, options.interval, options.backoffExponentialMaxInterval) } else if options.backoffPolicy == BackoffPolicyLinear { waitDuration = options.interval } else { return fmt.Errorf("invalid backoff policy: %s", options.backoffPolicy) } if options.invertCheck == true { if err == nil { goto CONTINUE } break } if err == nil { break } CONTINUE: retries++ select { case <-ctx.Done(): return ctx.Err() case <-time.After(waitDuration): } } return nil } wait4x-wait4x-7fb9e45/waiter/waiter_test.go000066400000000000000000000113221507553353000207600ustar00rootroot00000000000000// Copyright 2019-2025 The Wait4X Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package waiter provides the Waiter for the Wait4X application. package waiter import ( "bytes" "context" "errors" "fmt" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/tonglil/buflogr" "wait4x.dev/v3/checker" ) // TestMain is the main function for the Waiter. func TestMain(m *testing.M) { os.Exit(m.Run()) } // TestWaitSuccessful tests the Waiter with a successful check. func TestWaitSuccessful(t *testing.T) { mockChecker := new(checker.MockChecker) mockChecker.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) err := Wait(mockChecker, WithInterval(time.Second)) assert.Nil(t, err) mockChecker.AssertExpectations(t) } // TestWaitTimedOut tests the Waiter with a timed out check. func TestWaitTimedOut(t *testing.T) { mockChecker := new(checker.MockChecker) mockChecker.On("Check", mock.Anything).Return(fmt.Errorf("error")). On("Identity").Return("ID", nil) err := Wait(mockChecker, WithTimeout(time.Second)) assert.Equal(t, context.DeadlineExceeded, err) mockChecker.AssertExpectations(t) } // TestWaitInvalidIdentity tests the Waiter with an invalid identity. func TestWaitInvalidIdentity(t *testing.T) { invalidIdentityError := errors.New("invalid identity") mockChecker := new(checker.MockChecker) mockChecker.On("Identity").Return(mock.Anything, invalidIdentityError) err := Wait(mockChecker) assert.Equal(t, invalidIdentityError, err) mockChecker.AssertExpectations(t) } // TestWaitLogger tests the Waiter with a logger. func TestWaitLogger(t *testing.T) { mockChecker := new(checker.MockChecker) mockChecker.On("Check", mock.Anything). Return(fmt.Errorf("error message")). On("Identity").Return("ID", nil) var buf bytes.Buffer var log = buflogr.NewWithBuffer(&buf) err := WaitContext(context.TODO(), mockChecker, WithLogger(log), WithTimeout(time.Second)) assert.Equal(t, context.DeadlineExceeded, err) assert.Contains(t, buf.String(), "INFO [MockChecker] Checking ID ...") assert.Contains(t, buf.String(), "error message") mockChecker.AssertExpectations(t) } // TestWaitInvertCheck tests the Waiter with an inverted check. func TestWaitInvertCheck(t *testing.T) { alwaysTrue := new(checker.MockChecker) alwaysTrue.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) err := Wait(alwaysTrue, WithTimeout(time.Second*3), WithInvertCheck(true)) assert.Equal(t, context.DeadlineExceeded, err) alwaysTrue.AssertExpectations(t) alwaysFalse := new(checker.MockChecker) alwaysFalse.On("Check", mock.Anything).Return(fmt.Errorf("error")). On("Identity").Return("ID", nil) err = Wait(alwaysFalse, WithTimeout(time.Second), WithInvertCheck(true)) assert.Nil(t, err) alwaysFalse.AssertExpectations(t) } // TestWaitParallelSuccessful tests the Waiter with a parallel successful check. func TestWaitParallelSuccessful(t *testing.T) { alwaysTrueFirst := new(checker.MockChecker) alwaysTrueFirst.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) alwaysTrueSecond := new(checker.MockChecker) alwaysTrueSecond.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) err := WaitParallel([]checker.Checker{alwaysTrueFirst, alwaysTrueSecond}, WithTimeout(time.Second*3)) assert.Nil(t, err) alwaysTrueFirst.AssertExpectations(t) alwaysTrueSecond.AssertExpectations(t) } // TestWaitParallelFail tests the Waiter with a parallel failed check. func TestWaitParallelFail(t *testing.T) { alwaysTrueFirst := new(checker.MockChecker) alwaysTrueFirst.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) alwaysTrueSecond := new(checker.MockChecker) alwaysTrueSecond.On("Check", mock.Anything).Return(nil). On("Identity").Return("ID", nil) alwaysError := new(checker.MockChecker) alwaysError.On("Check", mock.Anything).Return(fmt.Errorf("error")). On("Identity").Return("ID", nil) err := WaitParallel([]checker.Checker{alwaysTrueFirst, alwaysTrueSecond, alwaysError}, WithTimeout(time.Second*3)) assert.Equal(t, context.DeadlineExceeded, err) alwaysTrueFirst.AssertExpectations(t) alwaysTrueSecond.AssertExpectations(t) alwaysError.AssertExpectations(t) }