diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b9c0f981..193ee8c6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ that issue here in this description (not in the title of the PR). Before creating a PR, run through this checklist and mark each as complete. -- [ ] I have read the [CONTRIBUTING](https://github.com/nginxinc/nginx-plus-go-client/blob/main/CONTRIBUTING.md) doc +- [ ] I have read the [CONTRIBUTING](https://github.com/nginx/nginx-plus-go-client/blob/main/CONTRIBUTING.md) doc - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have checked that all unit tests pass after adding my changes - [ ] I have updated necessary documentation diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 524f7914..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - - - package-ecosystem: docker - directory: /docker - schedule: - interval: weekly diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index b4337344..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,27 +0,0 @@ -change: - - head-branch: ["^change/"] - -enhancement: - - head-branch: ["^feature/", "^feat/", "^enhancement/", "^enh/"] - -bug: - - head-branch: ["^fix/", "^bug/"] - -chore: - - head-branch: ["^chore/"] - -tests: - - head-branch: ["^tests/", "^test/"] - - changed-files: - - any-glob-to-any-file: "tests/**/*" - -documentation: - - head-branch: ["^docs/", "^doc/"] - - changed-files: - - any-glob-to-any-file: "**/*.md" - -dependencies: - - head-branch: - ["^deps/", "^dep/", "^dependabot/", "pre-commit-ci-update-config"] - - changed-files: - - any-glob-to-any-file: ["go.mod", "go.sum"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa78f902..73fe03f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,9 @@ on: - main schedule: - cron: "0 5 * * *" # runs every day at 5am UTC + merge_group: + types: + - checks_requested defaults: run: @@ -26,34 +29,31 @@ permissions: jobs: unit-test: name: Unit Test - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: read - strategy: - fail-fast: false - matrix: - go-version: [stable] steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Golang Environment - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: ${{ matrix.go-version }} + go-version-file: go.mod - name: Run Unit Tests run: make unit-test build: name: Build Client - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: write + issues: write if: ${{ github.event.repository.fork == false }} steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Switch Repository (Nightly) if: (github.event_name == 'schedule') @@ -65,10 +65,10 @@ jobs: sed -i 's|\${NGINX_PLUS_VERSION}/||g' docker/Dockerfile - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Build Plus Docker Image - uses: docker/build-push-action@94f8f8c2eec4bc3f1d78c1755580779804cb87b2 # v6.0.1 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: file: docker/Dockerfile tags: nginx-plus @@ -86,7 +86,7 @@ jobs: run: docker compose up test-no-stream --exit-code-from test-no-stream - name: Create/Update Draft - uses: lucacome/draft-release@8a63d32c79a171ae6048e614a8988f0ac3ed56d4 # v1.1.0 + uses: lucacome/draft-release@fd099feb33710d1fa27b915a08a7acd6a1fb7fd2 # v2.0.0 id: release-notes with: minor-label: "enhancement" @@ -96,16 +96,16 @@ jobs: if: ${{ github.event_name == 'push' }} - name: Setup Golang Environment - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: stable + go-version-file: go.mod + if: ${{ github.ref_type == 'tag' }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: - version: latest + version: v2.12.5 # renovate: datasource=github-tags depName=goreleaser/goreleaser args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_COMMUNITY }} if: ${{ github.ref_type == 'tag' }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 29cae928..e4c2f6ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: "CodeQL" +name: CodeQL on: push: @@ -10,6 +10,9 @@ on: - main schedule: - cron: "33 16 * * 3" # run every Wednesday at 16:33 UTC + merge_group: + types: + - checks_requested concurrency: group: ${{ github.ref_name }}-codeql @@ -21,30 +24,44 @@ permissions: jobs: analyze: name: Analyze - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: - actions: read - contents: read - security-events: write - + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report + packages: read # required to fetch internal or private CodeQL packs + if: ${{ github.event_name != 'merge_group' }} strategy: fail-fast: false matrix: - language: ["go"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + include: + - language: go + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup Golang Environment + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: stable + if: matrix.language == 'go' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/init@a8d1ac45b9a34d11fe398d5503176af0d06b303e # v3.30.7 with: languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. @@ -52,27 +69,7 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - - name: Setup Golang Environment - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 - with: - go-version: stable - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/analyze@a8d1ac45b9a34d11fe398d5503176af0d06b303e # v3.30.7 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml deleted file mode 100644 index 21ca570b..00000000 --- a/.github/workflows/dependabot-auto-merge.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Dependabot auto-merge - -on: pull_request_target - -permissions: - contents: read - -jobs: - dependabot: - runs-on: ubuntu-22.04 - permissions: - pull-requests: write - contents: write - if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} - steps: - - name: Dependabot metadata - id: dependabot-metadata - uses: dependabot/fetch-metadata@v2.1.0 - - - name: Enable auto-merge for Dependabot PRs - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GITHUB_TOKEN: ${{ secrets.NGINX_PAT }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index bcaafe65..019d08b6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,8 +1,11 @@ -name: "Dependency Review" +name: Dependency Review on: pull_request: branches: - main + merge_group: + types: + - checks_requested concurrency: group: ${{ github.ref_name }}-deps-review @@ -13,15 +16,17 @@ permissions: jobs: dependency-review: - runs-on: ubuntu-22.04 + name: Dependency Review + runs-on: ubuntu-24.04 permissions: contents: read # for actions/checkout pull-requests: write # for actions/dependency-review-action to post comments + if: ${{ github.event_name != 'merge_group' }} steps: - - name: "Checkout Repository" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Checkout Repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: "Dependency Review" - uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 + - name: Dependency Review + uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0 with: - config-file: "nginxinc/k8s-common/dependency-review-config.yml@main" + config-file: "nginx/k8s-common/dependency-review-config.yml@main" diff --git a/.github/workflows/f5-cla.yml b/.github/workflows/f5-cla.yml new file mode 100644 index 00000000..7f2c1d6c --- /dev/null +++ b/.github/workflows/f5-cla.yml @@ -0,0 +1,51 @@ +name: F5 CLA + +on: + issue_comment: + types: + - created + pull_request_target: + types: + - opened + - synchronize + - reopened + +concurrency: + group: ${{ github.ref_name }}-cla + +permissions: + contents: read + +jobs: + f5-cla: + name: F5 CLA + runs-on: ubuntu-24.04 + permissions: + actions: write + contents: read + pull-requests: write + statuses: write + steps: + - name: Run F5 Contributor License Agreement (CLA) assistant + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have hereby read the F5 CLA and agree to its terms') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 + with: + # Any pull request targeting the following branch will trigger a CLA check. + branch: "main" + # Path to the CLA document. + path-to-document: "https://github.com/f5/.github/blob/main/CLA/cla-markdown.md" + # Custom CLA messages. + custom-notsigned-prcomment: "🎉 Thank you for your contribution! It appears you have not yet signed the F5 Contributor License Agreement (CLA), which is required for your changes to be incorporated into an F5 Open Source Software (OSS) project. Please kindly read the [F5 CLA](https://github.com/f5/.github/blob/main/CLA/cla-markdown.md) and reply on a new comment with the following text to agree:" + custom-pr-sign-comment: "I have hereby read the F5 CLA and agree to its terms" + custom-allsigned-prcomment: "✅ All required contributors have signed the F5 CLA for this PR. Thank you!" + # Remote repository storing CLA signatures. + remote-organization-name: "f5" + remote-repository-name: "f5-cla-data" + path-to-signatures: "signatures/beta/signatures.json" + # Comma separated list of usernames for maintainers or any other individuals who should not be prompted for a CLA. + allowlist: bot* + # Do not lock PRs after a merge. + lock-pullrequest-aftermerge: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.F5_CLA_TOKEN }} diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 3b1e67a1..dff10f4b 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -17,13 +17,13 @@ permissions: jobs: scan: name: Fossa - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ github.event.repository.fork == false }} steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Scan - uses: fossas/fossa-action@47ef11b1e1e3812e88dae436ccbd2d0cbd1adab0 # v1.3.3 + uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 with: api-key: ${{ secrets.FOSSA_TOKEN }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d62990da..76280fb1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,9 +10,17 @@ jobs: permissions: contents: read pull-requests: write # for actions/labeler to add labels - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + sparse-checkout: | + labeler.yml + sparse-checkout-cone-mode: false + repository: nginx/k8s-common + + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true + configuration-path: labeler.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 062b0f90..ae45a4a6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - main + merge_group: + types: + - checks_requested defaults: run: @@ -18,39 +21,43 @@ permissions: jobs: lint: - name: Lint - runs-on: ubuntu-22.04 + name: Go Lint + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Golang Environment - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: stable - - name: Lint Code - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 + - name: Lint Go + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: v2.5.0 # renovate: datasource=github-tags depName=golangci/golangci-lint actionlint: name: Actionlint - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: reviewdog/action-actionlint@2927e858b45218240af952feb1d702cf6365f39a # v1.50.0 + - name: Lint Actions + uses: reviewdog/action-actionlint@95395aac8c053577d0bc67eb7b74936c660c6f66 # v1.67.0 with: actionlint_flags: -shellcheck "" markdown-lint: name: Markdown Lint - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8 # v16.0.0 + - name: Lint Markdown + uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0 with: config: .markdownlint-cli2.yaml globs: "**/*.md" @@ -58,12 +65,9 @@ jobs: yaml-lint: name: YAML lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - - name: Install yamllint - run: pip install yamllint + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Lint YAML files - run: yamllint . + - name: Lint YAML + uses: reviewdog/action-yamllint@f01d8a48fd8d89f89895499fca2cff09f9e9e8c0 # v1.21.0 diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml index c5bed0d1..22a891b2 100644 --- a/.github/workflows/notifications.yml +++ b/.github/workflows/notifications.yml @@ -16,14 +16,14 @@ permissions: jobs: on-failure: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.repository.fork == false }} permissions: contents: read actions: read # for 8398a7/action-slack steps: - name: Data - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 continue-on-error: true id: data with: @@ -44,7 +44,7 @@ jobs: } - name: Send Notification - uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 # v3.16.2 + uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e # v3.19.0 with: status: custom custom_payload: | diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9beee6c7..5aa8b84e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,16 +26,16 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif - repo_token: ${{ secrets.SCORECARD_TOKEN }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers @@ -49,7 +49,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -57,6 +57,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/upload-sarif@a8d1ac45b9a34d11fe398d5503176af0d06b303e # v3.30.7 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 14d3a0c1..71389e86 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,9 +11,9 @@ jobs: permissions: issues: write # for actions/stale to close stale issues pull-requests: write # for actions/stale to close stale PRs - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days." @@ -22,5 +22,7 @@ jobs: close-pr-message: "This PR was closed because it has been stalled for 7 days with no activity." stale-issue-label: "stale" stale-pr-label: "stale" + exempt-all-issue-milestones: true + exempt-issue-labels: "backlog, backlog candidate, epic" exempt-all-pr-assignees: true operations-per-run: 100 diff --git a/.gitignore b/.gitignore index 7b57f9e7..0e534a13 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ # Visual Studio Code settings .vscode +# Goland settings +.idea/ + dist diff --git a/.golangci.yml b/.golangci.yml index bc1fa939..07d01b78 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,74 +1,48 @@ -linters-settings: - misspell: - locale: US - revive: - ignore-generated-header: true - rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: empty-block - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - - name: exported - - name: increment-decrement - - name: indent-error-flow - - name: package-comments - - name: range - - name: receiver-naming - - name: redefines-builtin-id - - name: superfluous-else - - name: time-naming - - name: unexported-return - - name: unreachable-code - - name: unused-parameter - - name: var-declaration - - name: var-naming - govet: - enable-all: true +version: "2" linters: + default: none enable: - asasalint - asciicheck - bidichk + - containedctx + - contextcheck + - copyloopvar - dupword + - durationcheck + - err113 - errcheck + - errchkjson - errname - errorlint - - exportloopref - fatcontext - forcetypeassert - gocheckcompilerdirectives + - gochecksumtype + - gocritic - godot - - gofmt - - gofumpt - - goimports - gosec - - gosimple - gosmopolitan - govet - ineffassign - intrange - makezero + - mirror - misspell + - musttag - nilerr - noctx - nolintlint + - paralleltest - perfsprint - prealloc - predeclared - reassign - revive - staticcheck - - stylecheck - tagalign - - tenv - thelper - tparallel - - typecheck - unconvert - unparam - unused @@ -76,9 +50,67 @@ linters: - wastedassign - whitespace - wrapcheck - disable-all: true + settings: + govet: + enable-all: true + misspell: + locale: US + revive: + rules: + - name: blank-imports + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: defer + - name: dot-imports + - name: duplicated-imports + - name: empty-block + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + - name: package-comments + - name: range + - name: range-val-address + - name: range-val-in-closure + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: superfluous-else + - name: time-naming + - name: unchecked-type-assertion + - name: unexported-return + - name: unnecessary-stmt + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: var-naming + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 -run: - timeout: 5m +formatters: + enable: + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 79aefaa1..0514898c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,8 +5,5 @@ builds: changelog: disable: true -announce: - slack: - enabled: true - channel: "#announcements" - message_template: "NGINX Plus Go Client {{ .Tag }} is out! Check it out: {{ .ReleaseURL }}" +milestones: + - close: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d22c42f..f45a86af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -11,27 +11,44 @@ repos: - id: check-added-large-files - id: check-merge-conflict - id: check-shebang-scripts-are-executable + - id: check-executables-have-shebangs - id: check-case-conflict - id: check-vcs-permalinks + - id: check-json + - id: pretty-format-json + args: [--autofix, --no-ensure-ascii] - id: mixed-line-ending args: [--fix=lf] - id: no-commit-to-branch - id: fix-byte-order-marker + - id: detect-private-key - repo: https://github.com/golangci/golangci-lint - rev: v1.59.1 + rev: v2.5.0 hooks: - id: golangci-lint-full - repo: https://github.com/gitleaks/gitleaks - rev: v8.18.4 + rev: v8.28.0 hooks: - id: gitleaks - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.13.0 + rev: v0.18.1 hooks: - id: markdownlint-cli2 + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.37.1 + hooks: + - id: yamllint + + - repo: https://github.com/thlorenz/doctoc + rev: v2.2.0 + hooks: + - id: doctoc + args: [--update-only, --title, "## Table of Contents"] + ci: skip: [golangci-lint-full] + autoupdate_schedule: quarterly # We use renovate for more frequent updates and there's no way to disable autoupdate diff --git a/.yamllint.yaml b/.yamllint.yaml index 7d0320c4..dd2afab7 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -1,8 +1,4 @@ --- -yaml-files: - - "*.yaml" - - "*.yml" - ignore-from-file: .gitignore extends: default diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd9115d..88c57ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,14 @@ # Changelog -Starting with version 0.8.0, an automatically generated list of changes can be found on the [GitHub Releases page](https://github.com/nginxinc/nginx-plus-go-client/releases). +Starting with version 0.8.0, an automatically generated list of changes can be found on the [GitHub Releases page](https://github.com/nginx/nginx-plus-go-client/releases). ## 0.7.0 (Jul 10, 2020) FEATURES: -- [38](https://github.com/nginxinc/nginx-plus-go-client/pull/38): *Support for /slabs API endpoint*. The client now +- [38](https://github.com/nginx/nginx-plus-go-client/pull/38): _Support for /slabs API endpoint_. The client now supports retrieving shared memory zone usage info. -- [41](https://github.com/nginxinc/nginx-plus-go-client/pull/41): *Support for /processes API endpoint*. The client now +- [41](https://github.com/nginx/nginx-plus-go-client/pull/41): _Support for /processes API endpoint_. The client now supports retrieving processes info. CHANGES: @@ -20,7 +20,7 @@ CHANGES: FEATURES: -- [34](https://github.com/nginxinc/nginx-plus-go-client/pull/34): *Support for updating upstream servers parameters*. +- [34](https://github.com/nginx/nginx-plus-go-client/pull/34): _Support for updating upstream servers parameters_. The client now supports updating upstream parameters of servers that already exist in NGINX Plus. CHANGES: @@ -35,14 +35,14 @@ CHANGES: FEATURES: -- [30](https://github.com/nginxinc/nginx-plus-go-client/pull/30): *Support additional upstream server parameters*. The -client now supports configuring `route`, `backup`, `down`, `drain`, `weight` and `service` parameters for http -upstreams and `backup`, `down`, `weight` and `service` parameters for stream upstreams. -- [31](https://github.com/nginxinc/nginx-plus-go-client/pull/31): *Support location zones and resolver metrics*. +- [30](https://github.com/nginx/nginx-plus-go-client/pull/30): _Support additional upstream server parameters_. The + client now supports configuring `route`, `backup`, `down`, `drain`, `weight` and `service` parameters for http + upstreams and `backup`, `down`, `weight` and `service` parameters for stream upstreams. +- [31](https://github.com/nginx/nginx-plus-go-client/pull/31): _Support location zones and resolver metrics_. FIXES: -- [29](https://github.com/nginxinc/nginx-plus-go-client/pull/29): *Fix max_fails parameter in upstream servers*. +- [29](https://github.com/nginx/nginx-plus-go-client/pull/29): _Fix max_fails parameter in upstream servers_. Previously, if the MaxFails field was not explicitly set, the client would incorrectly configure an upstream with the value `0` instead of the correct value `1`. @@ -55,36 +55,36 @@ CHANGES: FEATURES: -- [24](https://github.com/nginxinc/nginx-plus-go-client/pull/24): *Support `MaxConns` in upstream servers*. +- [24](https://github.com/nginx/nginx-plus-go-client/pull/24): _Support `MaxConns` in upstream servers_. FIXES: -- [25](https://github.com/nginxinc/nginx-plus-go-client/pull/25): *Fix session metrics for stream server zones*. Session +- [25](https://github.com/nginx/nginx-plus-go-client/pull/25): _Fix session metrics for stream server zones_. Session metrics with a status of `4xx` or `5xx` are now correctly reported. Previously they were always reported as `0`. ## 0.3.1 (June 10, 2019) CHANGES: -- [22](https://github.com/nginxinc/nginx-plus-go-client/pull/22): *Change in stream zone sync metrics*. `StreamZoneSync` +- [22](https://github.com/nginx/nginx-plus-go-client/pull/22): _Change in stream zone sync metrics_. `StreamZoneSync` field of the `Stats` type is now a pointer. It will be nil if NGINX Plus doesn't report any zone sync stats. ## 0.3 (May 29, 2019) FEATURES: -- [20](https://github.com/nginxinc/nginx-plus-go-client/pull/20): *Support for stream zone sync metrics*. The client +- [20](https://github.com/nginx/nginx-plus-go-client/pull/20): _Support for stream zone sync metrics_. The client `GetStats` method now additionally returns stream zone sync metrics. -- [13](https://github.com/nginxinc/nginx-plus-go-client/pull/13): *Support for key-value endpoints*. The client +- [13](https://github.com/nginx/nginx-plus-go-client/pull/13): _Support for key-value endpoints_. The client implements a set of methods to create/modify/delete key-val pairs for both http and stream contexts. -- [12](https://github.com/nginxinc/nginx-plus-go-client/pull/12) *Support for NGINX status info*. The client `GetStats` +- [12](https://github.com/nginx/nginx-plus-go-client/pull/12) _Support for NGINX status info_. The client `GetStats` method now additionally returns NGINX status metrics. Thanks to [jthurman42](https://github.com/jthurman42). CHANGES: - The repository was renamed to `nginx-plus-go-client` instead of `nginx-plus-go-sdk`. If the client is used as a dependency, this name needs to be changed in the import section (`import - "github.com/nginxinc/nginx-plus-go-client/client"`). +"github.com/nginxinc/nginx-plus-go-client/client"`). - The version of the API was changed to 4. - The version of NGINX Plus for e2e testing was changed to R18. @@ -92,8 +92,8 @@ CHANGES: FEATURES: -- [7](https://github.com/nginxinc/nginx-plus-go-sdk/pull/7): *Support for stream server zone and stream upstream - metrics*. The client `GetStats` method now additionally returns stream server zone and stream upstream metrics. +- [7](https://github.com/nginx/nginx-plus-go-client/pull/7): _Support for stream server zone and stream upstream + metrics_. The client `GetStats` method now additionally returns stream server zone and stream upstream metrics. CHANGES: diff --git a/CODEOWNERS b/CODEOWNERS index 77ed024e..47d9e595 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @nginxinc/integrations +* @nginx/integrations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d21b1b5..284d66a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,24 +3,27 @@ The following is a set of guidelines for contributing to the NGINX Plus Go Client. We really appreciate that you are considering contributing! -## Table Of Contents - -[Ask a Question](#ask-a-question) - -[Getting Started](#getting-started) - -[Contributing](#contributing) - -[Style Guides](#style-guides) - -- [Git Style Guide](#git-style-guide) -- [Go Style Guide](#go-style-guide) - -[Code of Conduct](CODE_OF_CONDUCT.md) + + +## Table of Contents + +- [Ask a Question](#ask-a-question) +- [Getting Started](#getting-started) +- [Contributing](#contributing) + - [Report a Bug](#report-a-bug) + - [Suggest an Enhancement](#suggest-an-enhancement) + - [Open a Pull Request](#open-a-pull-request) + - [Issue lifecycle](#issue-lifecycle) + - [F5 Contributor License Agreement (CLA)](#f5-contributor-license-agreement-cla) +- [Style Guides](#style-guides) + - [Git Style Guide](#git-style-guide) + - [Go Style Guide](#go-style-guide) + + ## Ask a Question -To ask a question please use [Github Discussions](https://github.com/nginxinc/nginx-plus-go-client/discussions). +To ask a question please use [Github Discussions](https://github.com/nginx/nginx-plus-go-client/discussions). You can also join our [Community Slack](https://community.nginx.org/joinslack) which has a wider NGINX audience. @@ -58,6 +61,15 @@ issue template. type of issue it is (bug, feature request, etc) and to determine the milestone. Please see the [Issue Lifecycle](ISSUE_LIFECYCLE.md) document for more information. +### F5 Contributor License Agreement (CLA) + +F5 requires all external contributors to agree to the terms of the [F5 CLA](https://github.com/f5/.github/blob/main/CLA/cla-markdown.md) +before any of their changes can be incorporated into an F5 Open Source repository. + +If you have not yet agreed to the F5 CLA terms and submit a PR to this repository, a bot will prompt you to view and +agree to the F5 CLA. You will have to agree to the F5 CLA terms through a comment in the PR before any of your changes +can be merged. Your agreement signature will be safely stored by F5 and no longer be required in future PRs. + ## Style Guides ### Git Style Guide @@ -78,6 +90,6 @@ issue template. - Run `gofmt` over your code to automatically resolve a lot of style issues. Most editors support this running automatically when saving a code file. - Run `go lint` and `go vet` on your code too to catch any other issues. -- Follow this guide on some good practice and idioms for Go - +- Follow this guide on some good practice and idioms for Go - - To check for extra issues, install [golangci-lint](https://github.com/golangci/golangci-lint) and run `make lint` or `golangci-lint run` diff --git a/Makefile b/Makefile index 1c61ea77..ae4f7742 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ +# renovate: datasource=github-tags depName=golangci/golangci-lint +GOLANGCI_LINT_VERSION = v2.5.0 + test: unit-test test-integration test-integration-no-stream-block clean lint: - docker run --pull always --rm -v $(shell pwd):/nginx-plus-go-client -w /nginx-plus-go-client -v $(shell go env GOCACHE):/cache/go -e GOCACHE=/cache/go -e GOLANGCI_LINT_CACHE=/cache/go -v $(shell go env GOPATH)/pkg:/go/pkg golangci/golangci-lint:latest golangci-lint --color always run + go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run --fix unit-test: go test -v -shuffle=on -race client/*.go diff --git a/README.md b/README.md index 652cea16..06e78ded 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,35 @@ - -[![OpenSSFScorecard](https://api.securityscorecards.dev/projects/github.com/nginxinc/nginx-plus-go-client/badge)](https://api.securityscorecards.dev/projects/github.com/nginxinc/nginx-plus-go-client) -[![Continuous Integration](https://github.com/nginxinc/nginx-plus-go-client/workflows/Continuous%20Integration/badge.svg)](https://github.com/nginxinc/nginx-plus-go-client/actions) +[![OpenSSFScorecard](https://api.securityscorecards.dev/projects/github.com/nginx/nginx-plus-go-client/badge)](https://scorecard.dev/viewer/?uri=github.com/nginx/nginx-plus-go-client) +[![Continuous Integration](https://github.com/nginx/nginx-plus-go-client/workflows/Continuous%20Integration/badge.svg)](https://github.com/nginx/nginx-plus-go-client/actions) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Go Report Card](https://goreportcard.com/badge/github.com/nginxinc/nginx-plus-go-client)](https://goreportcard.com/report/github.com/nginxinc/nginx-plus-go-client) -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B5618%2Fgithub.com%2Fnginxinc%2Fnginx-plus-go-client.svg?type=shield)](https://app.fossa.com/projects/custom%2B5618%2Fgithub.com%2Fnginxinc%2Fnginx-plus-go-client?ref=badge_shield) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nginxinc/nginx-plus-go-client?logo=github&sort=semver)](https://github.com/nginxinc/nginx-plus-go-client/releases/latest) -![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/nginxinc/nginx-plus-go-client?logo=go) +[![Go Report Card](https://goreportcard.com/badge/github.com/nginx/nginx-plus-go-client)](https://goreportcard.com/report/github.com/nginx/nginx-plus-go-client) +[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B5618%2Fgithub.com%2Fnginx%2Fnginx-plus-go-client.svg?type=shield)](https://app.fossa.com/projects/custom%2B5618%2Fgithub.com%2Fnginx%2Fnginx-plus-go-client?ref=badge_shield) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nginx/nginx-plus-go-client?logo=github&sort=semver)](https://github.com/nginx/nginx-plus-go-client/releases/latest) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/nginx/nginx-plus-go-client?logo=go) [![Slack](https://img.shields.io/badge/slack-nginxcommunity-green?logo=slack)](https://nginxcommunity.slack.com) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) -[![Community Support](https://badgen.net/badge/support/community/cyan?icon=awesome)](https://github.com/nginxinc/nginx-plus-go-client/blob/main/SECURITY.md) +[![Community Support](https://badgen.net/badge/support/community/cyan?icon=awesome)](https://github.com/nginx/nginx-plus-go-client/blob/main/SECURITY.md) # NGINX Plus Go Client This project includes a client library for working with NGINX Plus API. + + +## Table of Contents + +- [About the Client](#about-the-client) +- [Compatibility](#compatibility) +- [Using the Client](#using-the-client) +- [Testing](#testing) + - [Unit tests](#unit-tests) + - [Integration tests](#integration-tests) +- [Contacts](#contacts) +- [Contributing](#contributing) +- [Support](#support) + + + ## About the Client `client/nginx.go` includes functions and data structures for working with NGINX Plus API as well as some helper @@ -26,17 +41,17 @@ This Client works against versions 4 to 9 of the NGINX Plus API. The table below the API was first introduced. | API version | NGINX Plus version | -|-------------|--------------------| -| 4 | R18 | -| 5 | R19 | -| 6 | R20 | -| 7 | R25 | -| 8 | R27 | -| 9 | R30 | +| ----------- | ------------------ | +| 4 | R18 | +| 5 | R19 | +| 6 | R20 | +| 7 | R25 | +| 8 | R27 | +| 9 | R30 | ## Using the Client -1. Import `github.com/nginxinc/nginx-plus-go-client/client` into your go project. +1. Import `github.com/nginx/nginx-plus-go-client/client` into your go project. 2. Use your favorite vendor tool to add this to your `/vendor` directory in your project. ## Testing diff --git a/client/nginx.go b/client/nginx.go index 0d8cce0e..c6b96b32 100644 --- a/client/nginx.go +++ b/client/nginx.go @@ -9,9 +9,14 @@ import ( "io" "net/http" "reflect" + "regexp" "slices" + "strconv" "strings" + "sync" "time" + + "golang.org/x/sync/errgroup" ) const ( @@ -37,8 +42,24 @@ var ( defaultWeight = 1 ) -// ErrUnsupportedVer means that client's API version is not supported by NGINX plus API. -var ErrUnsupportedVer = errors.New("API version of the client is not supported by running NGINX Plus") +var ( + ErrParameterRequired = errors.New("parameter is required") + ErrServerNotFound = errors.New("server not found") + ErrServerExists = errors.New("server already exists") + ErrNotSupported = errors.New("not supported") + ErrInvalidTimeout = errors.New("invalid timeout") + ErrParameterMismatch = errors.New("encountered duplicate server with different parameters") + ErrPlusVersionNotFound = errors.New("plus version not found in the input string") +) + +// StatusError is an interface that defines our API with consumers of the plus client errors. +// The error will return a http status code and an NGINX error code. +type StatusError interface { + Status() int + Code() string +} + +var _ StatusError = (*internalError)(nil) // NginxClient lets you access NGINX Plus API. type NginxClient struct { @@ -83,9 +104,9 @@ type StreamUpstreamServer struct { } type apiErrorResponse struct { - RequestID string `json:"request_id"` - Href string - Error apiError + RequestID string `json:"request_id"` + Href string `json:"href"` + Error apiError `json:"error"` } func (resp *apiErrorResponse) toString() string { @@ -94,14 +115,24 @@ func (resp *apiErrorResponse) toString() string { } type apiError struct { - Text string - Code string - Status int + Text string `json:"text"` + Code string `json:"code"` + Status int `json:"status"` } type internalError struct { - err string - apiError + err string + apiError apiError +} + +// Status returns the HTTP status code of the error. +func (internalError *internalError) Status() int { + return internalError.apiError.Status +} + +// Status returns the NGINX error code on the response. +func (internalError *internalError) Code() string { + return internalError.apiError.Code } // Error allows internalError to match the Error interface. @@ -116,6 +147,40 @@ func (internalError *internalError) Wrap(err string) *internalError { return internalError } +// this is an internal representation of the Stats object including endpoint and streamEndpoint lists. +type extendedStats struct { + endpoints []string + streamEndpoints []string + Stats +} + +func defaultStats() *extendedStats { + return &extendedStats{ + endpoints: []string{}, + streamEndpoints: []string{}, + Stats: Stats{ + Upstreams: map[string]Upstream{}, + ServerZones: map[string]ServerZone{}, + StreamServerZones: map[string]StreamServerZone{}, + StreamUpstreams: map[string]StreamUpstream{}, + Slabs: map[string]Slab{}, + Caches: map[string]HTTPCache{}, + HTTPLimitConnections: map[string]LimitConnection{}, + StreamLimitConnections: map[string]LimitConnection{}, + HTTPLimitRequests: map[string]HTTPLimitRequest{}, + Resolvers: map[string]Resolver{}, + LocationZones: map[string]LocationZone{}, + StreamZoneSync: nil, + Workers: []*Workers{}, + NginxInfo: NginxInfo{}, + SSL: SSL{}, + Connections: Connections{}, + HTTPRequests: HTTPRequests{}, + Processes: Processes{}, + }, + } +} + // Stats represents NGINX Plus stats fetched from the NGINX Plus API. // https://nginx.org/en/docs/http/ngx_http_api_module.html type Stats struct { @@ -151,6 +216,20 @@ type NginxInfo struct { ParentProcessID uint64 `json:"ppid"` } +// LicenseReporting contains information about license status for NGINX Plus. +type LicenseReporting struct { + Healthy bool + Fails uint64 + Grace uint64 +} + +// NginxLicense contains licensing information about NGINX Plus. +type NginxLicense struct { + Reporting *LicenseReporting + ActiveTill uint64 `json:"active_till"` + Eval bool +} + // Caches is a map of cache stats by cache zone. type Caches = map[string]HTTPCache @@ -183,10 +262,10 @@ type ExtendedCacheStats struct { // Connections represents connection related stats. type Connections struct { - Accepted uint64 - Dropped uint64 - Active uint64 - Idle uint64 + Accepted int64 + Dropped int64 + Active int64 + Idle int64 } // Slabs is map of slab stats by zone name. @@ -358,11 +437,11 @@ type Upstreams map[string]Upstream // Upstream represents upstream related stats. type Upstream struct { - Zone string - Peers []Peer - Queue Queue - Keepalives int - Zombies int + Zone string + Peers []Peer + Queue Queue + Keepalive int + Zombies int } // StreamUpstreams is a map of stream upstream stats by upstream name. @@ -546,6 +625,19 @@ func WithCheckAPI() Option { } } +// WithMaxAPIVersion sets the API version to the max API version. +func WithMaxAPIVersion() Option { + return func(o *NginxClient) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + version, err := o.GetMaxAPIVersion(ctx) + if err != nil { + return + } + o.apiVersion = version + } +} + // NewNginxClient creates a new NginxClient. func NewNginxClient(apiEndpoint string, opts ...Option) (*NginxClient, error) { c := &NginxClient{ @@ -560,15 +652,17 @@ func NewNginxClient(apiEndpoint string, opts ...Option) (*NginxClient, error) { } if c.httpClient == nil { - return nil, errors.New("http client is not set") + return nil, fmt.Errorf("http client: %w", ErrParameterRequired) } if !versionSupported(c.apiVersion) { - return nil, fmt.Errorf("API version %v is not supported by the client", c.apiVersion) + return nil, fmt.Errorf("API version %v: %w by the client", c.apiVersion, ErrNotSupported) } if c.checkAPI { - versions, err := getAPIVersions(c.httpClient, apiEndpoint) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + versions, err := c.getAPIVersions(ctx, c.httpClient, apiEndpoint) if err != nil { return nil, fmt.Errorf("error accessing the API: %w", err) } @@ -580,7 +674,7 @@ func NewNginxClient(apiEndpoint string, opts ...Option) (*NginxClient, error) { } } if !found { - return nil, fmt.Errorf("API version %v is not supported by the server", c.apiVersion) + return nil, fmt.Errorf("API version %v: %w by the server", c.apiVersion, ErrNotSupported) } } @@ -596,10 +690,24 @@ func versionSupported(n int) bool { return false } -func getAPIVersions(httpClient *http.Client, endpoint string) (*versions, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() +// GetMaxAPIVersion returns the maximum API version supported by the server and the client. +func (client *NginxClient) GetMaxAPIVersion(ctx context.Context) (int, error) { + serverVersions, err := client.getAPIVersions(ctx, client.httpClient, client.apiEndpoint) + if err != nil { + return 0, fmt.Errorf("failed to get max API version: %w", err) + } + + maxServerVersion := slices.Max(*serverVersions) + maxClientVersion := slices.Max(supportedAPIVersions) + + if maxServerVersion > maxClientVersion { + return maxClientVersion, nil + } + + return maxServerVersion, nil +} +func (client *NginxClient) getAPIVersions(ctx context.Context, httpClient *http.Client, endpoint string) (*versions, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create a get request: %w", err) @@ -611,7 +719,9 @@ func getAPIVersions(httpClient *http.Client, endpoint string) (*versions, error) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%v is not accessible: expected %v response, got %v", endpoint, http.StatusOK, resp.StatusCode) + return nil, createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( + "failed to get endpoint %q, expected %v response, got %v", + endpoint, http.StatusOK, resp.StatusCode)) } body, err := io.ReadAll(resp.Body) @@ -658,17 +768,17 @@ func readAPIErrorResponse(respBody io.ReadCloser) (*apiErrorResponse, error) { } // CheckIfUpstreamExists checks if the upstream exists in NGINX. If the upstream doesn't exist, it returns the error. -func (client *NginxClient) CheckIfUpstreamExists(upstream string) error { - _, err := client.GetHTTPServers(upstream) +func (client *NginxClient) CheckIfUpstreamExists(ctx context.Context, upstream string) error { + _, err := client.GetHTTPServers(ctx, upstream) return err } // GetHTTPServers returns the servers of the upstream from NGINX. -func (client *NginxClient) GetHTTPServers(upstream string) ([]UpstreamServer, error) { +func (client *NginxClient) GetHTTPServers(ctx context.Context, upstream string) ([]UpstreamServer, error) { path := fmt.Sprintf("http/upstreams/%v/servers", upstream) var servers []UpstreamServer - err := client.get(path, &servers) + err := client.get(ctx, path, &servers) if err != nil { return nil, fmt.Errorf("failed to get the HTTP servers of upstream %v: %w", upstream, err) } @@ -677,17 +787,21 @@ func (client *NginxClient) GetHTTPServers(upstream string) ([]UpstreamServer, er } // AddHTTPServer adds the server to the upstream. -func (client *NginxClient) AddHTTPServer(upstream string, server UpstreamServer) error { - id, err := client.getIDOfHTTPServer(upstream, server.Server) +func (client *NginxClient) AddHTTPServer(ctx context.Context, upstream string, server UpstreamServer) error { + id, err := client.getIDOfHTTPServer(ctx, upstream, server.Server) if err != nil { return fmt.Errorf("failed to add %v server to %v upstream: %w", server.Server, upstream, err) } if id != -1 { - return fmt.Errorf("failed to add %v server to %v upstream: server already exists", server.Server, upstream) + return fmt.Errorf("failed to add %v server to %v upstream: %w", server.Server, upstream, ErrServerExists) } + err = client.addHTTPServer(ctx, upstream, server) + return err +} +func (client *NginxClient) addHTTPServer(ctx context.Context, upstream string, server UpstreamServer) error { path := fmt.Sprintf("http/upstreams/%v/servers/", upstream) - err = client.post(path, &server) + err := client.post(ctx, path, &server) if err != nil { return fmt.Errorf("failed to add %v server to %v upstream: %w", server.Server, upstream, err) } @@ -696,17 +810,21 @@ func (client *NginxClient) AddHTTPServer(upstream string, server UpstreamServer) } // DeleteHTTPServer the server from the upstream. -func (client *NginxClient) DeleteHTTPServer(upstream string, server string) error { - id, err := client.getIDOfHTTPServer(upstream, server) +func (client *NginxClient) DeleteHTTPServer(ctx context.Context, upstream string, server string) error { + id, err := client.getIDOfHTTPServer(ctx, upstream, server) if err != nil { return fmt.Errorf("failed to remove %v server from %v upstream: %w", server, upstream, err) } if id == -1 { - return fmt.Errorf("failed to remove %v server from %v upstream: server doesn't exist", server, upstream) + return fmt.Errorf("failed to remove %v server from %v upstream: %w", server, upstream, ErrServerNotFound) } + err = client.deleteHTTPServer(ctx, upstream, server, id) + return err +} - path := fmt.Sprintf("http/upstreams/%v/servers/%v", upstream, id) - err = client.delete(path, http.StatusOK) +func (client *NginxClient) deleteHTTPServer(ctx context.Context, upstream, server string, serverID int) error { + path := fmt.Sprintf("http/upstreams/%v/servers/%v", upstream, serverID) + err := client.delete(ctx, path, http.StatusOK) if err != nil { return fmt.Errorf("failed to remove %v server from %v upstream: %w", server, upstream, err) } @@ -718,8 +836,11 @@ func (client *NginxClient) DeleteHTTPServer(upstream string, server string) erro // Servers that are in the slice, but don't exist in NGINX will be added to NGINX. // Servers that aren't in the slice, but exist in NGINX, will be removed from NGINX. // Servers that are in the slice and exist in NGINX, but have different parameters, will be updated. -func (client *NginxClient) UpdateHTTPServers(upstream string, servers []UpstreamServer) (added []UpstreamServer, deleted []UpstreamServer, updated []UpstreamServer, err error) { - serversInNginx, err := client.GetHTTPServers(upstream) +// The client will attempt to update all servers, returning all the errors that occurred. +// If there are duplicate servers with equivalent parameters, the duplicates will be ignored. +// If there are duplicate servers with different parameters, those server entries will be ignored and an error returned. +func (client *NginxClient) UpdateHTTPServers(ctx context.Context, upstream string, servers []UpstreamServer) (added []UpstreamServer, deleted []UpstreamServer, updated []UpstreamServer, err error) { + serversInNginx, err := client.GetHTTPServers(ctx, upstream) if err != nil { return nil, nil, nil, fmt.Errorf("failed to update servers of %v upstream: %w", upstream, err) } @@ -731,72 +852,120 @@ func (client *NginxClient) UpdateHTTPServers(upstream string, servers []Upstream formattedServers = append(formattedServers, server) } + formattedServers, err = deduplicateServers(upstream, formattedServers) + toAdd, toDelete, toUpdate := determineUpdates(formattedServers, serversInNginx) for _, server := range toAdd { - err := client.AddHTTPServer(upstream, server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update servers of %v upstream: %w", upstream, err) + addErr := client.addHTTPServer(ctx, upstream, server) + if addErr != nil { + err = errors.Join(err, addErr) + continue } + added = append(added, server) } for _, server := range toDelete { - err := client.DeleteHTTPServer(upstream, server.Server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update servers of %v upstream: %w", upstream, err) + deleteErr := client.deleteHTTPServer(ctx, upstream, server.Server, server.ID) + if deleteErr != nil { + err = errors.Join(err, deleteErr) + continue } + deleted = append(deleted, server) } for _, server := range toUpdate { - err := client.UpdateHTTPServer(upstream, server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update servers of %v upstream: %w", upstream, err) + updateErr := client.UpdateHTTPServer(ctx, upstream, server) + if updateErr != nil { + err = errors.Join(err, updateErr) + continue } + updated = append(updated, server) + } + + if err != nil { + err = fmt.Errorf("failed to update servers of %s upstream: %w", upstream, err) } - return toAdd, toDelete, toUpdate, nil + return added, deleted, updated, err } -// haveSameParameters checks if a given server has the same parameters as a server already present in NGINX. Order matters. -func haveSameParameters(newServer UpstreamServer, serverNGX UpstreamServer) bool { - newServer.ID = serverNGX.ID +func deduplicateServers(upstream string, servers []UpstreamServer) ([]UpstreamServer, error) { + type serverCheck struct { + server UpstreamServer + valid bool + } - if serverNGX.MaxConns != nil && newServer.MaxConns == nil { - newServer.MaxConns = &defaultMaxConns + serverMap := make(map[string]*serverCheck, len(servers)) + var err error + for _, server := range servers { + if prev, ok := serverMap[server.Server]; ok { + if !prev.valid { + continue + } + if !server.hasSameParametersAs(prev.server) { + prev.valid = false + err = errors.Join(err, fmt.Errorf( + "failed to update %s server to %s upstream: %w", + server.Server, upstream, ErrParameterMismatch)) + } + continue + } + serverMap[server.Server] = &serverCheck{server, true} } + retServers := make([]UpstreamServer, 0, len(serverMap)) + for _, server := range servers { + if check, ok := serverMap[server.Server]; ok && check.valid { + retServers = append(retServers, server) + delete(serverMap, server.Server) + } + } + return retServers, err +} + +// hasSameParametersAs checks if a given server has the same parameters. +func (s UpstreamServer) hasSameParametersAs(compareServer UpstreamServer) bool { + s.ID = compareServer.ID + s.applyDefaults() + compareServer.applyDefaults() + return reflect.DeepEqual(s, compareServer) +} - if serverNGX.MaxFails != nil && newServer.MaxFails == nil { - newServer.MaxFails = &defaultMaxFails +func (s *UpstreamServer) applyDefaults() { + if s.MaxConns == nil { + s.MaxConns = &defaultMaxConns } - if serverNGX.FailTimeout != "" && newServer.FailTimeout == "" { - newServer.FailTimeout = defaultFailTimeout + if s.MaxFails == nil { + s.MaxFails = &defaultMaxFails } - if serverNGX.SlowStart != "" && newServer.SlowStart == "" { - newServer.SlowStart = defaultSlowStart + if s.FailTimeout == "" { + s.FailTimeout = defaultFailTimeout } - if serverNGX.Backup != nil && newServer.Backup == nil { - newServer.Backup = &defaultBackup + if s.SlowStart == "" { + s.SlowStart = defaultSlowStart } - if serverNGX.Down != nil && newServer.Down == nil { - newServer.Down = &defaultDown + if s.Backup == nil { + s.Backup = &defaultBackup } - if serverNGX.Weight != nil && newServer.Weight == nil { - newServer.Weight = &defaultWeight + if s.Down == nil { + s.Down = &defaultDown } - return reflect.DeepEqual(newServer, serverNGX) + if s.Weight == nil { + s.Weight = &defaultWeight + } } func determineUpdates(updatedServers []UpstreamServer, nginxServers []UpstreamServer) (toAdd []UpstreamServer, toRemove []UpstreamServer, toUpdate []UpstreamServer) { for _, server := range updatedServers { updateFound := false for _, serverNGX := range nginxServers { - if server.Server == serverNGX.Server && !haveSameParameters(server, serverNGX) { + if server.Server == serverNGX.Server && !server.hasSameParametersAs(serverNGX) { server.ID = serverNGX.ID updateFound = true break @@ -833,11 +1002,11 @@ func determineUpdates(updatedServers []UpstreamServer, nginxServers []UpstreamSe } } - return + return toAdd, toRemove, toUpdate } -func (client *NginxClient) getIDOfHTTPServer(upstream string, name string) (int, error) { - servers, err := client.GetHTTPServers(upstream) +func (client *NginxClient) getIDOfHTTPServer(ctx context.Context, upstream string, name string) (int, error) { + servers, err := client.GetHTTPServers(ctx, upstream) if err != nil { return -1, fmt.Errorf("error getting id of server %v of upstream %v: %w", name, upstream, err) } @@ -851,10 +1020,7 @@ func (client *NginxClient) getIDOfHTTPServer(upstream string, name string) (int, return -1, nil } -func (client *NginxClient) get(path string, data interface{}) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - +func (client *NginxClient) get(ctx context.Context, path string, data interface{}) error { url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, client.apiVersion, path) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -885,10 +1051,7 @@ func (client *NginxClient) get(path string, data interface{}) error { return nil } -func (client *NginxClient) post(path string, input interface{}) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - +func (client *NginxClient) post(ctx context.Context, path string, input interface{}) error { url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, client.apiVersion, path) jsonInput, err := json.Marshal(input) @@ -917,10 +1080,7 @@ func (client *NginxClient) post(path string, input interface{}) error { return nil } -func (client *NginxClient) delete(path string, expectedStatusCode int) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - +func (client *NginxClient) delete(ctx context.Context, path string, expectedStatusCode int) error { path = fmt.Sprintf("%v/%v/%v/", client.apiEndpoint, client.apiVersion, path) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil) @@ -942,10 +1102,7 @@ func (client *NginxClient) delete(path string, expectedStatusCode int) error { return nil } -func (client *NginxClient) patch(path string, input interface{}, expectedStatusCode int) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - +func (client *NginxClient) patch(ctx context.Context, path string, input interface{}, expectedStatusCode int) error { path = fmt.Sprintf("%v/%v/%v/", client.apiEndpoint, client.apiVersion, path) jsonInput, err := json.Marshal(input) @@ -957,6 +1114,7 @@ func (client *NginxClient) patch(path string, input interface{}, expectedStatusC if err != nil { return fmt.Errorf("failed to create a patch request: %w", err) } + req.Header.Set("Content-Type", "application/json") resp, err := client.httpClient.Do(req) if err != nil { @@ -973,17 +1131,17 @@ func (client *NginxClient) patch(path string, input interface{}, expectedStatusC } // CheckIfStreamUpstreamExists checks if the stream upstream exists in NGINX. If the upstream doesn't exist, it returns the error. -func (client *NginxClient) CheckIfStreamUpstreamExists(upstream string) error { - _, err := client.GetStreamServers(upstream) +func (client *NginxClient) CheckIfStreamUpstreamExists(ctx context.Context, upstream string) error { + _, err := client.GetStreamServers(ctx, upstream) return err } // GetStreamServers returns the stream servers of the upstream from NGINX. -func (client *NginxClient) GetStreamServers(upstream string) ([]StreamUpstreamServer, error) { +func (client *NginxClient) GetStreamServers(ctx context.Context, upstream string) ([]StreamUpstreamServer, error) { path := fmt.Sprintf("stream/upstreams/%v/servers", upstream) var servers []StreamUpstreamServer - err := client.get(path, &servers) + err := client.get(ctx, path, &servers) if err != nil { return nil, fmt.Errorf("failed to get stream servers of upstream server %v: %w", upstream, err) } @@ -991,17 +1149,21 @@ func (client *NginxClient) GetStreamServers(upstream string) ([]StreamUpstreamSe } // AddStreamServer adds the stream server to the upstream. -func (client *NginxClient) AddStreamServer(upstream string, server StreamUpstreamServer) error { - id, err := client.getIDOfStreamServer(upstream, server.Server) +func (client *NginxClient) AddStreamServer(ctx context.Context, upstream string, server StreamUpstreamServer) error { + id, err := client.getIDOfStreamServer(ctx, upstream, server.Server) if err != nil { return fmt.Errorf("failed to add %v stream server to %v upstream: %w", server.Server, upstream, err) } if id != -1 { - return fmt.Errorf("failed to add %v stream server to %v upstream: server already exists", server.Server, upstream) + return fmt.Errorf("failed to add %v stream server to %v upstream: %w", server.Server, upstream, ErrServerExists) } + err = client.addStreamServer(ctx, upstream, server) + return err +} +func (client *NginxClient) addStreamServer(ctx context.Context, upstream string, server StreamUpstreamServer) error { path := fmt.Sprintf("stream/upstreams/%v/servers/", upstream) - err = client.post(path, &server) + err := client.post(ctx, path, &server) if err != nil { return fmt.Errorf("failed to add %v stream server to %v upstream: %w", server.Server, upstream, err) } @@ -1009,17 +1171,21 @@ func (client *NginxClient) AddStreamServer(upstream string, server StreamUpstrea } // DeleteStreamServer the server from the upstream. -func (client *NginxClient) DeleteStreamServer(upstream string, server string) error { - id, err := client.getIDOfStreamServer(upstream, server) +func (client *NginxClient) DeleteStreamServer(ctx context.Context, upstream string, server string) error { + id, err := client.getIDOfStreamServer(ctx, upstream, server) if err != nil { return fmt.Errorf("failed to remove %v stream server from %v upstream: %w", server, upstream, err) } if id == -1 { - return fmt.Errorf("failed to remove %v stream server from %v upstream: server doesn't exist", server, upstream) + return fmt.Errorf("failed to remove %v stream server from %v upstream: %w", server, upstream, ErrServerNotFound) } + err = client.deleteStreamServer(ctx, upstream, server, id) + return err +} - path := fmt.Sprintf("stream/upstreams/%v/servers/%v", upstream, id) - err = client.delete(path, http.StatusOK) +func (client *NginxClient) deleteStreamServer(ctx context.Context, upstream, server string, serverID int) error { + path := fmt.Sprintf("stream/upstreams/%v/servers/%v", upstream, serverID) + err := client.delete(ctx, path, http.StatusOK) if err != nil { return fmt.Errorf("failed to remove %v stream server from %v upstream: %w", server, upstream, err) } @@ -1030,8 +1196,11 @@ func (client *NginxClient) DeleteStreamServer(upstream string, server string) er // Servers that are in the slice, but don't exist in NGINX will be added to NGINX. // Servers that aren't in the slice, but exist in NGINX, will be removed from NGINX. // Servers that are in the slice and exist in NGINX, but have different parameters, will be updated. -func (client *NginxClient) UpdateStreamServers(upstream string, servers []StreamUpstreamServer) (added []StreamUpstreamServer, deleted []StreamUpstreamServer, updated []StreamUpstreamServer, err error) { - serversInNginx, err := client.GetStreamServers(upstream) +// The client will attempt to update all servers, returning all the errors that occurred. +// If there are duplicate servers with equivalent parameters, the duplicates will be ignored. +// If there are duplicate servers with different parameters, those server entries will be ignored and an error returned. +func (client *NginxClient) UpdateStreamServers(ctx context.Context, upstream string, servers []StreamUpstreamServer) (added []StreamUpstreamServer, deleted []StreamUpstreamServer, updated []StreamUpstreamServer, err error) { + serversInNginx, err := client.GetStreamServers(ctx, upstream) if err != nil { return nil, nil, nil, fmt.Errorf("failed to update stream servers of %v upstream: %w", upstream, err) } @@ -1042,34 +1211,46 @@ func (client *NginxClient) UpdateStreamServers(upstream string, servers []Stream formattedServers = append(formattedServers, server) } + formattedServers, err = deduplicateStreamServers(upstream, formattedServers) + toAdd, toDelete, toUpdate := determineStreamUpdates(formattedServers, serversInNginx) for _, server := range toAdd { - err := client.AddStreamServer(upstream, server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update stream servers of %v upstream: %w", upstream, err) + addErr := client.addStreamServer(ctx, upstream, server) + if addErr != nil { + err = errors.Join(err, addErr) + continue } + added = append(added, server) } for _, server := range toDelete { - err := client.DeleteStreamServer(upstream, server.Server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update stream servers of %v upstream: %w", upstream, err) + deleteErr := client.deleteStreamServer(ctx, upstream, server.Server, server.ID) + if deleteErr != nil { + err = errors.Join(err, deleteErr) + continue } + deleted = append(deleted, server) } for _, server := range toUpdate { - err := client.UpdateStreamServer(upstream, server) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to update stream servers of %v upstream: %w", upstream, err) + updateErr := client.UpdateStreamServer(ctx, upstream, server) + if updateErr != nil { + err = errors.Join(err, updateErr) + continue } + updated = append(updated, server) } - return toAdd, toDelete, toUpdate, nil + if err != nil { + err = fmt.Errorf("failed to update stream servers of %s upstream: %w", upstream, err) + } + + return added, deleted, updated, err } -func (client *NginxClient) getIDOfStreamServer(upstream string, name string) (int, error) { - servers, err := client.GetStreamServers(upstream) +func (client *NginxClient) getIDOfStreamServer(ctx context.Context, upstream string, name string) (int, error) { + servers, err := client.GetStreamServers(ctx, upstream) if err != nil { return -1, fmt.Errorf("error getting id of stream server %v of upstream %v: %w", name, upstream, err) } @@ -1083,45 +1264,82 @@ func (client *NginxClient) getIDOfStreamServer(upstream string, name string) (in return -1, nil } -// haveSameParametersForStream checks if a given server has the same parameters as a server already present in NGINX. Order matters. -func haveSameParametersForStream(newServer StreamUpstreamServer, serverNGX StreamUpstreamServer) bool { - newServer.ID = serverNGX.ID - if serverNGX.MaxConns != nil && newServer.MaxConns == nil { - newServer.MaxConns = &defaultMaxConns +func deduplicateStreamServers(upstream string, servers []StreamUpstreamServer) ([]StreamUpstreamServer, error) { + type serverCheck struct { + server StreamUpstreamServer + valid bool + } + + serverMap := make(map[string]*serverCheck, len(servers)) + var err error + for _, server := range servers { + if prev, ok := serverMap[server.Server]; ok { + if !prev.valid { + continue + } + if !server.hasSameParametersAs(prev.server) { + prev.valid = false + err = errors.Join(err, fmt.Errorf( + "failed to update stream %s server to %s upstream: %w", + server.Server, upstream, ErrParameterMismatch)) + } + continue + } + serverMap[server.Server] = &serverCheck{server, true} } + retServers := make([]StreamUpstreamServer, 0, len(serverMap)) + for _, server := range servers { + if check, ok := serverMap[server.Server]; ok && check.valid { + retServers = append(retServers, server) + delete(serverMap, server.Server) + } + } + return retServers, err +} + +// hasSameParametersAs checks if a given server has the same parameters. +func (s StreamUpstreamServer) hasSameParametersAs(compareServer StreamUpstreamServer) bool { + s.ID = compareServer.ID + s.applyDefaults() + compareServer.applyDefaults() + return reflect.DeepEqual(s, compareServer) +} - if serverNGX.MaxFails != nil && newServer.MaxFails == nil { - newServer.MaxFails = &defaultMaxFails +func (s *StreamUpstreamServer) applyDefaults() { + if s.MaxConns == nil { + s.MaxConns = &defaultMaxConns } - if serverNGX.FailTimeout != "" && newServer.FailTimeout == "" { - newServer.FailTimeout = defaultFailTimeout + if s.MaxFails == nil { + s.MaxFails = &defaultMaxFails } - if serverNGX.SlowStart != "" && newServer.SlowStart == "" { - newServer.SlowStart = defaultSlowStart + if s.FailTimeout == "" { + s.FailTimeout = defaultFailTimeout } - if serverNGX.Backup != nil && newServer.Backup == nil { - newServer.Backup = &defaultBackup + if s.SlowStart == "" { + s.SlowStart = defaultSlowStart } - if serverNGX.Down != nil && newServer.Down == nil { - newServer.Down = &defaultDown + if s.Backup == nil { + s.Backup = &defaultBackup } - if serverNGX.Weight != nil && newServer.Weight == nil { - newServer.Weight = &defaultWeight + if s.Down == nil { + s.Down = &defaultDown } - return reflect.DeepEqual(newServer, serverNGX) + if s.Weight == nil { + s.Weight = &defaultWeight + } } func determineStreamUpdates(updatedServers []StreamUpstreamServer, nginxServers []StreamUpstreamServer) (toAdd []StreamUpstreamServer, toRemove []StreamUpstreamServer, toUpdate []StreamUpstreamServer) { for _, server := range updatedServers { updateFound := false for _, serverNGX := range nginxServers { - if server.Server == serverNGX.Server && !haveSameParametersForStream(server, serverNGX) { + if server.Server == serverNGX.Server && !server.hasSameParametersAs(serverNGX) { server.ID = serverNGX.ID updateFound = true break @@ -1158,246 +1376,432 @@ func determineStreamUpdates(updatedServers []StreamUpstreamServer, nginxServers } } - return + return toAdd, toRemove, toUpdate } // GetStats gets process, slab, connection, request, ssl, zone, stream zone, upstream and stream upstream related stats from the NGINX Plus API. -func (client *NginxClient) GetStats() (*Stats, error) { - endpoints, err := client.GetAvailableEndpoints() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } +func (client *NginxClient) GetStats(ctx context.Context) (*Stats, error) { + initialGroup, initialCtx := errgroup.WithContext(ctx) + var mu sync.Mutex + stats := defaultStats() + // Collecting initial stats + initialGroup.Go(func() error { + endpoints, err := client.GetAvailableEndpoints(initialCtx) + if err != nil { + return fmt.Errorf("failed to get available Endpoints: %w", err) + } - info, err := client.GetNginxInfo() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.endpoints = endpoints + mu.Unlock() + return nil + }) - caches, err := client.GetCaches() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + initialGroup.Go(func() error { + nginxInfo, err := client.GetNginxInfo(initialCtx) + if err != nil { + return fmt.Errorf("failed to get NGINX info: %w", err) + } - processes, err := client.GetProcesses() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.NginxInfo = *nginxInfo + mu.Unlock() - slabs, err := client.GetSlabs() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + return nil + }) - cons, err := client.GetConnections() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + initialGroup.Go(func() error { + caches, err := client.GetCaches(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Caches: %w", err) + } - requests, err := client.GetHTTPRequests() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.Caches = *caches + mu.Unlock() - ssl, err := client.GetSSL() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + return nil + }) - zones, err := client.GetServerZones() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + initialGroup.Go(func() error { + processes, err := client.GetProcesses(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Process information: %w", err) + } - upstreams, err := client.GetUpstreams() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.Processes = *processes + mu.Unlock() - locationZones, err := client.GetLocationZones() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + return nil + }) - resolvers, err := client.GetResolvers() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + initialGroup.Go(func() error { + slabs, err := client.GetSlabs(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Slabs: %w", err) + } - limitReqs, err := client.GetHTTPLimitReqs() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.Slabs = *slabs + mu.Unlock() - limitConnsHTTP, err := client.GetHTTPConnectionsLimit() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + return nil + }) - workers, err := client.GetWorkers() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + initialGroup.Go(func() error { + httpRequests, err := client.GetHTTPRequests(initialCtx) + if err != nil { + return fmt.Errorf("failed to get HTTP Requests: %w", err) + } - streamZones := &StreamServerZones{} - streamUpstreams := &StreamUpstreams{} - limitConnsStream := &StreamLimitConnections{} - var streamZoneSync *StreamZoneSync + mu.Lock() + stats.HTTPRequests = *httpRequests + mu.Unlock() - if slices.Contains(endpoints, "stream") { - streamEndpoints, err := client.GetAvailableStreamEndpoints() + return nil + }) + + initialGroup.Go(func() error { + ssl, err := client.GetSSL(initialCtx) if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) + return fmt.Errorf("failed to get SSL: %w", err) } - if slices.Contains(streamEndpoints, "server_zones") { - streamZones, err = client.GetStreamServerZones() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.SSL = *ssl + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + serverZones, err := client.GetServerZones(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Server Zones: %w", err) } - if slices.Contains(streamEndpoints, "upstreams") { - streamUpstreams, err = client.GetStreamUpstreams() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.ServerZones = *serverZones + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + upstreams, err := client.GetUpstreams(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Upstreams: %w", err) } - if slices.Contains(streamEndpoints, "limit_conns") { - limitConnsStream, err = client.GetStreamConnectionsLimit() - if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) - } + mu.Lock() + stats.Upstreams = *upstreams + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + locationZones, err := client.GetLocationZones(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Location Zones: %w", err) + } + + mu.Lock() + stats.LocationZones = *locationZones + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + resolvers, err := client.GetResolvers(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Resolvers: %w", err) } - if slices.Contains(streamEndpoints, "zone_sync") { - streamZoneSync, err = client.GetStreamZoneSync() + mu.Lock() + stats.Resolvers = *resolvers + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + httpLimitRequests, err := client.GetHTTPLimitReqs(initialCtx) + if err != nil { + return fmt.Errorf("failed to get HTTPLimitRequests: %w", err) + } + + mu.Lock() + stats.HTTPLimitRequests = *httpLimitRequests + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + httpLimitConnections, err := client.GetHTTPConnectionsLimit(initialCtx) + if err != nil { + return fmt.Errorf("failed to get HTTPLimitConnections: %w", err) + } + + mu.Lock() + stats.HTTPLimitConnections = *httpLimitConnections + mu.Unlock() + + return nil + }) + + initialGroup.Go(func() error { + workers, err := client.GetWorkers(initialCtx) + if err != nil { + return fmt.Errorf("failed to get Workers: %w", err) + } + + mu.Lock() + stats.Workers = workers + mu.Unlock() + + return nil + }) + + if err := initialGroup.Wait(); err != nil { + return nil, fmt.Errorf("error returned from contacting Plus API: %w", err) + } + + // Process stream endpoints if they exist + if slices.Contains(stats.endpoints, "stream") { + availableStreamGroup, asgCtx := errgroup.WithContext(ctx) + + availableStreamGroup.Go(func() error { + streamEndpoints, err := client.GetAvailableStreamEndpoints(asgCtx) if err != nil { - return nil, fmt.Errorf("failed to get stats: %w", err) + return fmt.Errorf("failed to get available Stream Endpoints: %w", err) } + + mu.Lock() + stats.streamEndpoints = streamEndpoints + mu.Unlock() + + return nil + }) + + if err := availableStreamGroup.Wait(); err != nil { + return nil, fmt.Errorf("no useful metrics found in stream stats: %w", err) + } + + streamGroup, sgCtx := errgroup.WithContext(ctx) + + if slices.Contains(stats.streamEndpoints, "server_zones") { + streamGroup.Go(func() error { + streamServerZones, err := client.GetStreamServerZones(sgCtx) + if err != nil { + return fmt.Errorf("failed to get streamServerZones: %w", err) + } + + mu.Lock() + stats.StreamServerZones = *streamServerZones + mu.Unlock() + + return nil + }) + } + + if slices.Contains(stats.streamEndpoints, "upstreams") { + streamGroup.Go(func() error { + streamUpstreams, err := client.GetStreamUpstreams(sgCtx) + if err != nil { + return fmt.Errorf("failed to get StreamUpstreams: %w", err) + } + + mu.Lock() + stats.StreamUpstreams = *streamUpstreams + mu.Unlock() + + return nil + }) + } + + if slices.Contains(stats.streamEndpoints, "limit_conns") { + streamGroup.Go(func() error { + streamConnectionsLimit, err := client.GetStreamConnectionsLimit(sgCtx) + if err != nil { + return fmt.Errorf("failed to get StreamLimitConnections: %w", err) + } + + mu.Lock() + stats.StreamLimitConnections = *streamConnectionsLimit + mu.Unlock() + + return nil + }) + + streamGroup.Go(func() error { + streamZoneSync, err := client.GetStreamZoneSync(sgCtx) + if err != nil { + return fmt.Errorf("failed to get StreamZoneSync: %w", err) + } + + mu.Lock() + stats.StreamZoneSync = streamZoneSync + mu.Unlock() + + return nil + }) + } + + if err := streamGroup.Wait(); err != nil { + return nil, fmt.Errorf("no useful metrics found in stream stats: %w", err) } } - return &Stats{ - NginxInfo: *info, - Caches: *caches, - Processes: *processes, - Slabs: *slabs, - Connections: *cons, - HTTPRequests: *requests, - SSL: *ssl, - ServerZones: *zones, - StreamServerZones: *streamZones, - Upstreams: *upstreams, - StreamUpstreams: *streamUpstreams, - StreamZoneSync: streamZoneSync, - LocationZones: *locationZones, - Resolvers: *resolvers, - HTTPLimitRequests: *limitReqs, - HTTPLimitConnections: *limitConnsHTTP, - StreamLimitConnections: *limitConnsStream, - Workers: workers, - }, nil + // Report connection metrics separately so it does not influence the results + connectionsGroup, cgCtx := errgroup.WithContext(ctx) + + connectionsGroup.Go(func() error { + // replace this call with a context specific call + connections, err := client.GetConnections(cgCtx) + if err != nil { + return fmt.Errorf("failed to get connections: %w", err) + } + + mu.Lock() + stats.Connections = *connections + mu.Unlock() + + return nil + }) + + if err := connectionsGroup.Wait(); err != nil { + return nil, fmt.Errorf("connections metrics not found: %w", err) + } + + return &stats.Stats, nil } // GetAvailableEndpoints returns available endpoints in the API. -func (client *NginxClient) GetAvailableEndpoints() ([]string, error) { +func (client *NginxClient) GetAvailableEndpoints(ctx context.Context) ([]string, error) { var endpoints []string - err := client.get("", &endpoints) + err := client.get(ctx, "", &endpoints) if err != nil { return nil, fmt.Errorf("failed to get endpoints: %w", err) } return endpoints, nil } -// GetAvailableStreamEndpoints returns available stream endpoints in the API. -func (client *NginxClient) GetAvailableStreamEndpoints() ([]string, error) { +// GetAvailableStreamEndpoints returns available stream endpoints in the API with a context. +func (client *NginxClient) GetAvailableStreamEndpoints(ctx context.Context) ([]string, error) { var endpoints []string - err := client.get("stream", &endpoints) + err := client.get(ctx, "stream", &endpoints) if err != nil { return nil, fmt.Errorf("failed to get endpoints: %w", err) } return endpoints, nil } -// GetNginxInfo returns Nginx stats. -func (client *NginxClient) GetNginxInfo() (*NginxInfo, error) { +// GetNginxInfo returns Nginx stats with a context. +func (client *NginxClient) GetNginxInfo(ctx context.Context) (*NginxInfo, error) { var info NginxInfo - err := client.get("nginx", &info) + err := client.get(ctx, "nginx", &info) if err != nil { return nil, fmt.Errorf("failed to get info: %w", err) } return &info, nil } -// GetCaches returns Cache stats. -func (client *NginxClient) GetCaches() (*Caches, error) { +// GetNginxLicense returns Nginx License data with a context. +func (client *NginxClient) GetNginxLicense(ctx context.Context) (*NginxLicense, error) { + var data NginxLicense + + info, err := client.GetNginxInfo(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get nginx info: %w", err) + } + release, err := extractPlusVersionValues(info.Build) + if err != nil { + return nil, fmt.Errorf("failed to get nginx plus release: %w", err) + } + + if (client.apiVersion < 9) || (release < 33) { + return &data, nil + } + + err = client.get(ctx, "license", &data) + if err != nil { + return nil, fmt.Errorf("failed to get license: %w", err) + } + return &data, nil +} + +// GetCaches returns Cache stats with a context. +func (client *NginxClient) GetCaches(ctx context.Context) (*Caches, error) { var caches Caches - err := client.get("http/caches", &caches) + err := client.get(ctx, "http/caches", &caches) if err != nil { return nil, fmt.Errorf("failed to get caches: %w", err) } return &caches, nil } -// GetSlabs returns Slabs stats. -func (client *NginxClient) GetSlabs() (*Slabs, error) { +// GetSlabs returns Slabs stats with a context. +func (client *NginxClient) GetSlabs(ctx context.Context) (*Slabs, error) { var slabs Slabs - err := client.get("slabs", &slabs) + err := client.get(ctx, "slabs", &slabs) if err != nil { return nil, fmt.Errorf("failed to get slabs: %w", err) } return &slabs, nil } -// GetConnections returns Connections stats. -func (client *NginxClient) GetConnections() (*Connections, error) { +// GetConnections returns Connections stats with a context. +func (client *NginxClient) GetConnections(ctx context.Context) (*Connections, error) { var cons Connections - err := client.get("connections", &cons) + err := client.get(ctx, "connections", &cons) if err != nil { return nil, fmt.Errorf("failed to get connections: %w", err) } return &cons, nil } -// GetHTTPRequests returns http/requests stats. -func (client *NginxClient) GetHTTPRequests() (*HTTPRequests, error) { +// GetHTTPRequests returns http/requests stats with a context. +func (client *NginxClient) GetHTTPRequests(ctx context.Context) (*HTTPRequests, error) { var requests HTTPRequests - err := client.get("http/requests", &requests) + err := client.get(ctx, "http/requests", &requests) if err != nil { return nil, fmt.Errorf("failed to get http requests: %w", err) } return &requests, nil } -// GetSSL returns SSL stats. -func (client *NginxClient) GetSSL() (*SSL, error) { +// GetSSL returns SSL stats with a context. +func (client *NginxClient) GetSSL(ctx context.Context) (*SSL, error) { var ssl SSL - err := client.get("ssl", &ssl) + err := client.get(ctx, "ssl", &ssl) if err != nil { return nil, fmt.Errorf("failed to get ssl: %w", err) } return &ssl, nil } -// GetServerZones returns http/server_zones stats. -func (client *NginxClient) GetServerZones() (*ServerZones, error) { +// GetServerZones returns http/server_zones stats with a context. +func (client *NginxClient) GetServerZones(ctx context.Context) (*ServerZones, error) { var zones ServerZones - err := client.get("http/server_zones", &zones) + err := client.get(ctx, "http/server_zones", &zones) if err != nil { return nil, fmt.Errorf("failed to get server zones: %w", err) } return &zones, err } -// GetStreamServerZones returns stream/server_zones stats. -func (client *NginxClient) GetStreamServerZones() (*StreamServerZones, error) { +// GetStreamServerZones returns stream/server_zones stats with a context. +func (client *NginxClient) GetStreamServerZones(ctx context.Context) (*StreamServerZones, error) { var zones StreamServerZones - err := client.get("stream/server_zones", &zones) + err := client.get(ctx, "stream/server_zones", &zones) if err != nil { var ie *internalError if errors.As(err, &ie) { - if ie.Code == pathNotFoundCode { + if ie.Code() == pathNotFoundCode { return &zones, nil } } @@ -1406,24 +1810,24 @@ func (client *NginxClient) GetStreamServerZones() (*StreamServerZones, error) { return &zones, err } -// GetUpstreams returns http/upstreams stats. -func (client *NginxClient) GetUpstreams() (*Upstreams, error) { +// GetUpstreams returns http/upstreams stats with a context. +func (client *NginxClient) GetUpstreams(ctx context.Context) (*Upstreams, error) { var upstreams Upstreams - err := client.get("http/upstreams", &upstreams) + err := client.get(ctx, "http/upstreams", &upstreams) if err != nil { return nil, fmt.Errorf("failed to get upstreams: %w", err) } return &upstreams, nil } -// GetStreamUpstreams returns stream/upstreams stats. -func (client *NginxClient) GetStreamUpstreams() (*StreamUpstreams, error) { +// GetStreamUpstreams returns stream/upstreams stats with a context. +func (client *NginxClient) GetStreamUpstreams(ctx context.Context) (*StreamUpstreams, error) { var upstreams StreamUpstreams - err := client.get("stream/upstreams", &upstreams) + err := client.get(ctx, "stream/upstreams", &upstreams) if err != nil { var ie *internalError if errors.As(err, &ie) { - if ie.Code == pathNotFoundCode { + if ie.Code() == pathNotFoundCode { return &upstreams, nil } } @@ -1432,14 +1836,14 @@ func (client *NginxClient) GetStreamUpstreams() (*StreamUpstreams, error) { return &upstreams, nil } -// GetStreamZoneSync returns stream/zone_sync stats. -func (client *NginxClient) GetStreamZoneSync() (*StreamZoneSync, error) { +// GetStreamZoneSync returns stream/zone_sync stats with a context. +func (client *NginxClient) GetStreamZoneSync(ctx context.Context) (*StreamZoneSync, error) { var streamZoneSync StreamZoneSync - err := client.get("stream/zone_sync", &streamZoneSync) + err := client.get(ctx, "stream/zone_sync", &streamZoneSync) if err != nil { var ie *internalError if errors.As(err, &ie) { - if ie.Code == pathNotFoundCode { + if ie.Code() == pathNotFoundCode { return nil, nil } } @@ -1449,13 +1853,13 @@ func (client *NginxClient) GetStreamZoneSync() (*StreamZoneSync, error) { return &streamZoneSync, err } -// GetLocationZones returns http/location_zones stats. -func (client *NginxClient) GetLocationZones() (*LocationZones, error) { +// GetLocationZones returns http/location_zones stats with a context. +func (client *NginxClient) GetLocationZones(ctx context.Context) (*LocationZones, error) { var locationZones LocationZones if client.apiVersion < 5 { return &locationZones, nil } - err := client.get("http/location_zones", &locationZones) + err := client.get(ctx, "http/location_zones", &locationZones) if err != nil { return nil, fmt.Errorf("failed to get location zones: %w", err) } @@ -1463,13 +1867,13 @@ func (client *NginxClient) GetLocationZones() (*LocationZones, error) { return &locationZones, err } -// GetResolvers returns Resolvers stats. -func (client *NginxClient) GetResolvers() (*Resolvers, error) { +// GetResolvers returns Resolvers stats with a context. +func (client *NginxClient) GetResolvers(ctx context.Context) (*Resolvers, error) { var resolvers Resolvers if client.apiVersion < 5 { return &resolvers, nil } - err := client.get("resolvers", &resolvers) + err := client.get(ctx, "resolvers", &resolvers) if err != nil { return nil, fmt.Errorf("failed to get resolvers: %w", err) } @@ -1477,10 +1881,10 @@ func (client *NginxClient) GetResolvers() (*Resolvers, error) { return &resolvers, err } -// GetProcesses returns Processes stats. -func (client *NginxClient) GetProcesses() (*Processes, error) { +// GetProcesses returns Processes stats with a context. +func (client *NginxClient) GetProcesses(ctx context.Context) (*Processes, error) { var processes Processes - err := client.get("processes", &processes) + err := client.get(ctx, "processes", &processes) if err != nil { return nil, fmt.Errorf("failed to get processes: %w", err) } @@ -1495,27 +1899,27 @@ type KeyValPairs map[string]string type KeyValPairsByZone map[string]KeyValPairs // GetKeyValPairs fetches key/value pairs for a given HTTP zone. -func (client *NginxClient) GetKeyValPairs(zone string) (KeyValPairs, error) { - return client.getKeyValPairs(zone, httpContext) +func (client *NginxClient) GetKeyValPairs(ctx context.Context, zone string) (KeyValPairs, error) { + return client.getKeyValPairs(ctx, zone, httpContext) } // GetStreamKeyValPairs fetches key/value pairs for a given Stream zone. -func (client *NginxClient) GetStreamKeyValPairs(zone string) (KeyValPairs, error) { - return client.getKeyValPairs(zone, streamContext) +func (client *NginxClient) GetStreamKeyValPairs(ctx context.Context, zone string) (KeyValPairs, error) { + return client.getKeyValPairs(ctx, zone, streamContext) } -func (client *NginxClient) getKeyValPairs(zone string, stream bool) (KeyValPairs, error) { +func (client *NginxClient) getKeyValPairs(ctx context.Context, zone string, stream bool) (KeyValPairs, error) { base := "http" if stream { base = "stream" } if zone == "" { - return nil, errors.New("zone required") + return nil, fmt.Errorf("zone: %w", ErrParameterRequired) } path := fmt.Sprintf("%v/keyvals/%v", base, zone) var keyValPairs KeyValPairs - err := client.get(path, &keyValPairs) + err := client.get(ctx, path, &keyValPairs) if err != nil { return nil, fmt.Errorf("failed to get keyvals for %v/%v zone: %w", base, zone, err) } @@ -1523,16 +1927,16 @@ func (client *NginxClient) getKeyValPairs(zone string, stream bool) (KeyValPairs } // GetAllKeyValPairs fetches all key/value pairs for all HTTP zones. -func (client *NginxClient) GetAllKeyValPairs() (KeyValPairsByZone, error) { - return client.getAllKeyValPairs(httpContext) +func (client *NginxClient) GetAllKeyValPairs(ctx context.Context) (KeyValPairsByZone, error) { + return client.getAllKeyValPairs(ctx, httpContext) } // GetAllStreamKeyValPairs fetches all key/value pairs for all Stream zones. -func (client *NginxClient) GetAllStreamKeyValPairs() (KeyValPairsByZone, error) { - return client.getAllKeyValPairs(streamContext) +func (client *NginxClient) GetAllStreamKeyValPairs(ctx context.Context) (KeyValPairsByZone, error) { + return client.getAllKeyValPairs(ctx, streamContext) } -func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, error) { +func (client *NginxClient) getAllKeyValPairs(ctx context.Context, stream bool) (KeyValPairsByZone, error) { base := "http" if stream { base = "stream" @@ -1540,7 +1944,7 @@ func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, er path := fmt.Sprintf("%v/keyvals", base) var keyValPairsByZone KeyValPairsByZone - err := client.get(path, &keyValPairsByZone) + err := client.get(ctx, path, &keyValPairsByZone) if err != nil { return nil, fmt.Errorf("failed to get keyvals for all %v zones: %w", base, err) } @@ -1548,27 +1952,27 @@ func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, er } // AddKeyValPair adds a new key/value pair to a given HTTP zone. -func (client *NginxClient) AddKeyValPair(zone string, key string, val string) error { - return client.addKeyValPair(zone, key, val, httpContext) +func (client *NginxClient) AddKeyValPair(ctx context.Context, zone string, key string, val string) error { + return client.addKeyValPair(ctx, zone, key, val, httpContext) } // AddStreamKeyValPair adds a new key/value pair to a given Stream zone. -func (client *NginxClient) AddStreamKeyValPair(zone string, key string, val string) error { - return client.addKeyValPair(zone, key, val, streamContext) +func (client *NginxClient) AddStreamKeyValPair(ctx context.Context, zone string, key string, val string) error { + return client.addKeyValPair(ctx, zone, key, val, streamContext) } -func (client *NginxClient) addKeyValPair(zone string, key string, val string, stream bool) error { +func (client *NginxClient) addKeyValPair(ctx context.Context, zone string, key string, val string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { - return errors.New("zone required") + return fmt.Errorf("zone: %w", ErrParameterRequired) } path := fmt.Sprintf("%v/keyvals/%v", base, zone) input := KeyValPairs{key: val} - err := client.post(path, &input) + err := client.post(ctx, path, &input) if err != nil { return fmt.Errorf("failed to add key value pair for %v/%v zone: %w", base, zone, err) } @@ -1576,27 +1980,27 @@ func (client *NginxClient) addKeyValPair(zone string, key string, val string, st } // ModifyKeyValPair modifies the value of an existing key in a given HTTP zone. -func (client *NginxClient) ModifyKeyValPair(zone string, key string, val string) error { - return client.modifyKeyValPair(zone, key, val, httpContext) +func (client *NginxClient) ModifyKeyValPair(ctx context.Context, zone string, key string, val string) error { + return client.modifyKeyValPair(ctx, zone, key, val, httpContext) } // ModifyStreamKeyValPair modifies the value of an existing key in a given Stream zone. -func (client *NginxClient) ModifyStreamKeyValPair(zone string, key string, val string) error { - return client.modifyKeyValPair(zone, key, val, streamContext) +func (client *NginxClient) ModifyStreamKeyValPair(ctx context.Context, zone string, key string, val string) error { + return client.modifyKeyValPair(ctx, zone, key, val, streamContext) } -func (client *NginxClient) modifyKeyValPair(zone string, key string, val string, stream bool) error { +func (client *NginxClient) modifyKeyValPair(ctx context.Context, zone string, key string, val string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { - return errors.New("zone required") + return fmt.Errorf("zone: %w", ErrParameterRequired) } path := fmt.Sprintf("%v/keyvals/%v", base, zone) input := KeyValPairs{key: val} - err := client.patch(path, &input, http.StatusNoContent) + err := client.patch(ctx, path, &input, http.StatusNoContent) if err != nil { return fmt.Errorf("failed to update key value pair for %v/%v zone: %w", base, zone, err) } @@ -1604,24 +2008,24 @@ func (client *NginxClient) modifyKeyValPair(zone string, key string, val string, } // DeleteKeyValuePair deletes the key/value pair for a key in a given HTTP zone. -func (client *NginxClient) DeleteKeyValuePair(zone string, key string) error { - return client.deleteKeyValuePair(zone, key, httpContext) +func (client *NginxClient) DeleteKeyValuePair(ctx context.Context, zone string, key string) error { + return client.deleteKeyValuePair(ctx, zone, key, httpContext) } // DeleteStreamKeyValuePair deletes the key/value pair for a key in a given Stream zone. -func (client *NginxClient) DeleteStreamKeyValuePair(zone string, key string) error { - return client.deleteKeyValuePair(zone, key, streamContext) +func (client *NginxClient) DeleteStreamKeyValuePair(ctx context.Context, zone string, key string) error { + return client.deleteKeyValuePair(ctx, zone, key, streamContext) } // To delete a key/value pair you set the value to null via the API, // then NGINX+ will delete the key. -func (client *NginxClient) deleteKeyValuePair(zone string, key string, stream bool) error { +func (client *NginxClient) deleteKeyValuePair(ctx context.Context, zone string, key string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { - return errors.New("zone required") + return fmt.Errorf("zone: %w", ErrParameterRequired) } // map[string]string can't have a nil value so we use a different type here. @@ -1629,7 +2033,7 @@ func (client *NginxClient) deleteKeyValuePair(zone string, key string, stream bo keyval[key] = nil path := fmt.Sprintf("%v/keyvals/%v", base, zone) - err := client.patch(path, &keyval, http.StatusNoContent) + err := client.patch(ctx, path, &keyval, http.StatusNoContent) if err != nil { return fmt.Errorf("failed to remove key values pair for %v/%v zone: %w", base, zone, err) } @@ -1637,37 +2041,41 @@ func (client *NginxClient) deleteKeyValuePair(zone string, key string, stream bo } // DeleteKeyValPairs deletes all the key-value pairs in a given HTTP zone. -func (client *NginxClient) DeleteKeyValPairs(zone string) error { - return client.deleteKeyValPairs(zone, httpContext) +func (client *NginxClient) DeleteKeyValPairs(ctx context.Context, zone string) error { + return client.deleteKeyValPairs(ctx, zone, httpContext) } // DeleteStreamKeyValPairs deletes all the key-value pairs in a given Stream zone. -func (client *NginxClient) DeleteStreamKeyValPairs(zone string) error { - return client.deleteKeyValPairs(zone, streamContext) +func (client *NginxClient) DeleteStreamKeyValPairs(ctx context.Context, zone string) error { + return client.deleteKeyValPairs(ctx, zone, streamContext) } -func (client *NginxClient) deleteKeyValPairs(zone string, stream bool) error { +func (client *NginxClient) deleteKeyValPairs(ctx context.Context, zone string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { - return errors.New("zone required") + return fmt.Errorf("zone: %w", ErrParameterRequired) } path := fmt.Sprintf("%v/keyvals/%v", base, zone) - err := client.delete(path, http.StatusNoContent) + err := client.delete(ctx, path, http.StatusNoContent) if err != nil { return fmt.Errorf("failed to remove all key value pairs for %v/%v zone: %w", base, zone, err) } return nil } -// UpdateHTTPServer updates the server of the upstream. -func (client *NginxClient) UpdateHTTPServer(upstream string, server UpstreamServer) error { +// UpdateHTTPServer updates the server of the upstream with the matching server ID. +func (client *NginxClient) UpdateHTTPServer(ctx context.Context, upstream string, server UpstreamServer) error { path := fmt.Sprintf("http/upstreams/%v/servers/%v", upstream, server.ID) + // The server ID is expected in the URI, but not expected in the body. + // The NGINX API will return + // {"error":{"status":400,"text":"unknown parameter \"id\"","code":"UpstreamConfFormatError"} + // if the ID field is present. server.ID = 0 - err := client.patch(path, &server, http.StatusOK) + err := client.patch(ctx, path, &server, http.StatusOK) if err != nil { return fmt.Errorf("failed to update %v server to %v upstream: %w", server.Server, upstream, err) } @@ -1675,11 +2083,15 @@ func (client *NginxClient) UpdateHTTPServer(upstream string, server UpstreamServ return nil } -// UpdateStreamServer updates the stream server of the upstream. -func (client *NginxClient) UpdateStreamServer(upstream string, server StreamUpstreamServer) error { +// UpdateStreamServer updates the stream server of the upstream with the matching server ID. +func (client *NginxClient) UpdateStreamServer(ctx context.Context, upstream string, server StreamUpstreamServer) error { path := fmt.Sprintf("stream/upstreams/%v/servers/%v", upstream, server.ID) + // The server ID is expected in the URI, but not expected in the body. + // The NGINX API will return + // {"error":{"status":400,"text":"unknown parameter \"id\"","code":"UpstreamConfFormatError"} + // if the ID field is present. server.ID = 0 - err := client.patch(path, &server, http.StatusOK) + err := client.patch(ctx, path, &server, http.StatusOK) if err != nil { return fmt.Errorf("failed to update %v stream server to %v upstream: %w", server.Server, upstream, err) } @@ -1708,43 +2120,43 @@ func addPortToServer(server string) string { return fmt.Sprintf("%v:%v", server, defaultServerPort) } -// GetHTTPLimitReqs returns http/limit_reqs stats. -func (client *NginxClient) GetHTTPLimitReqs() (*HTTPLimitRequests, error) { +// GetHTTPLimitReqs returns http/limit_reqs stats with a context. +func (client *NginxClient) GetHTTPLimitReqs(ctx context.Context) (*HTTPLimitRequests, error) { var limitReqs HTTPLimitRequests if client.apiVersion < 6 { return &limitReqs, nil } - err := client.get("http/limit_reqs", &limitReqs) + err := client.get(ctx, "http/limit_reqs", &limitReqs) if err != nil { return nil, fmt.Errorf("failed to get http limit requests: %w", err) } return &limitReqs, nil } -// GetHTTPConnectionsLimit returns http/limit_conns stats. -func (client *NginxClient) GetHTTPConnectionsLimit() (*HTTPLimitConnections, error) { +// GetHTTPConnectionsLimit returns http/limit_conns stats with a context. +func (client *NginxClient) GetHTTPConnectionsLimit(ctx context.Context) (*HTTPLimitConnections, error) { var limitConns HTTPLimitConnections if client.apiVersion < 6 { return &limitConns, nil } - err := client.get("http/limit_conns", &limitConns) + err := client.get(ctx, "http/limit_conns", &limitConns) if err != nil { return nil, fmt.Errorf("failed to get http connections limit: %w", err) } return &limitConns, nil } -// GetStreamConnectionsLimit returns stream/limit_conns stats. -func (client *NginxClient) GetStreamConnectionsLimit() (*StreamLimitConnections, error) { +// GetStreamConnectionsLimit returns stream/limit_conns stats with a context. +func (client *NginxClient) GetStreamConnectionsLimit(ctx context.Context) (*StreamLimitConnections, error) { var limitConns StreamLimitConnections if client.apiVersion < 6 { return &limitConns, nil } - err := client.get("stream/limit_conns", &limitConns) + err := client.get(ctx, "stream/limit_conns", &limitConns) if err != nil { var ie *internalError if errors.As(err, &ie) { - if ie.Code == pathNotFoundCode { + if ie.Code() == pathNotFoundCode { return &limitConns, nil } } @@ -1754,14 +2166,33 @@ func (client *NginxClient) GetStreamConnectionsLimit() (*StreamLimitConnections, } // GetWorkers returns workers stats. -func (client *NginxClient) GetWorkers() ([]*Workers, error) { +func (client *NginxClient) GetWorkers(ctx context.Context) ([]*Workers, error) { var workers []*Workers if client.apiVersion < 9 { return workers, nil } - err := client.get("workers", &workers) + err := client.get(ctx, "workers", &workers) if err != nil { return nil, fmt.Errorf("failed to get workers: %w", err) } return workers, nil } + +var rePlus = regexp.MustCompile(`-r(\d+)`) + +// extractPlusVersionValues. +func extractPlusVersionValues(input string) (int, error) { + var rValue int + matches := rePlus.FindStringSubmatch(input) + + if len(matches) < 1 { + return 0, fmt.Errorf("%w [%s]", ErrPlusVersionNotFound, input) + } + + rValue, err := strconv.Atoi(matches[1]) + if err != nil { + return 0, fmt.Errorf("failed to convert NGINX Plus release to integer: %w", err) + } + + return rValue, nil +} diff --git a/client/nginx_test.go b/client/nginx_test.go index e2e8ab3b..f4fd569f 100644 --- a/client/nginx_test.go +++ b/client/nginx_test.go @@ -1,10 +1,14 @@ package client import ( + "context" + "encoding/json" + "errors" "net/http" "net/http/httptest" "reflect" "strings" + "sync" "testing" ) @@ -12,6 +16,7 @@ func TestDetermineUpdates(t *testing.T) { t.Parallel() maxConns := 1 tests := []struct { + name string updated []UpstreamServer nginx []UpstreamServer expectedToAdd []UpstreamServer @@ -55,6 +60,7 @@ func TestDetermineUpdates(t *testing.T) { Server: "10.0.0.2:80", }, }, + name: "replace all", }, { updated: []UpstreamServer{ @@ -93,6 +99,7 @@ func TestDetermineUpdates(t *testing.T) { Server: "10.0.0.1:80", }, }, + name: "add and delete", }, { updated: []UpstreamServer{ @@ -117,6 +124,7 @@ func TestDetermineUpdates(t *testing.T) { Server: "10.0.0.3:80", }, }, + name: "same", }, { // empty values @@ -151,14 +159,18 @@ func TestDetermineUpdates(t *testing.T) { MaxConns: &maxConns, }, }, + name: "update field and delete", }, } for _, test := range tests { - toAdd, toDelete, toUpdate := determineUpdates(test.updated, test.nginx) - if !reflect.DeepEqual(toAdd, test.expectedToAdd) || !reflect.DeepEqual(toDelete, test.expectedToDelete) || !reflect.DeepEqual(toUpdate, test.expectedToUpdate) { - t.Errorf("determineUpdates(%v, %v) = (%v, %v, %v)", test.updated, test.nginx, toAdd, toDelete, toUpdate) - } + t.Run(test.name, func(t *testing.T) { + t.Parallel() + toAdd, toDelete, toUpdate := determineUpdates(test.updated, test.nginx) + if !reflect.DeepEqual(toAdd, test.expectedToAdd) || !reflect.DeepEqual(toDelete, test.expectedToDelete) || !reflect.DeepEqual(toUpdate, test.expectedToUpdate) { + t.Errorf("determineUpdates(%v, %v) = (%v, %v, %v)", test.updated, test.nginx, toAdd, toDelete, toUpdate) + } + }) } } @@ -166,6 +178,7 @@ func TestStreamDetermineUpdates(t *testing.T) { t.Parallel() maxConns := 1 tests := []struct { + name string updated []StreamUpstreamServer nginx []StreamUpstreamServer expectedToAdd []StreamUpstreamServer @@ -209,6 +222,7 @@ func TestStreamDetermineUpdates(t *testing.T) { Server: "10.0.0.2:80", }, }, + name: "replace all", }, { updated: []StreamUpstreamServer{ @@ -247,6 +261,7 @@ func TestStreamDetermineUpdates(t *testing.T) { Server: "10.0.0.1:80", }, }, + name: "add and delete", }, { updated: []StreamUpstreamServer{ @@ -274,6 +289,7 @@ func TestStreamDetermineUpdates(t *testing.T) { Server: "10.0.0.3:80", }, }, + name: "same", }, { // empty values @@ -308,14 +324,18 @@ func TestStreamDetermineUpdates(t *testing.T) { MaxConns: &maxConns, }, }, + name: "update field and delete", }, } for _, test := range tests { - toAdd, toDelete, toUpdate := determineStreamUpdates(test.updated, test.nginx) - if !reflect.DeepEqual(toAdd, test.expectedToAdd) || !reflect.DeepEqual(toDelete, test.expectedToDelete) || !reflect.DeepEqual(toUpdate, test.expectedToUpdate) { - t.Errorf("determiteUpdates(%v, %v) = (%v, %v, %v)", test.updated, test.nginx, toAdd, toDelete, toUpdate) - } + t.Run(test.name, func(t *testing.T) { + t.Parallel() + toAdd, toDelete, toUpdate := determineStreamUpdates(test.updated, test.nginx) + if !reflect.DeepEqual(toAdd, test.expectedToAdd) || !reflect.DeepEqual(toDelete, test.expectedToDelete) || !reflect.DeepEqual(toUpdate, test.expectedToUpdate) { + t.Errorf("determiteUpdates(%v, %v) = (%v, %v, %v)", test.updated, test.nginx, toAdd, toDelete, toUpdate) + } + }) } } @@ -365,16 +385,20 @@ func TestAddPortToServer(t *testing.T) { } for _, test := range tests { - result := addPortToServer(test.address) - if result != test.expected { - t.Errorf("addPortToServer(%v) returned %v but expected %v for %v", test.address, result, test.expected, test.msg) - } + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + result := addPortToServer(test.address) + if result != test.expected { + t.Errorf("addPortToServer(%v) returned %v but expected %v for %v", test.address, result, test.expected, test.msg) + } + }) } } func TestHaveSameParameters(t *testing.T) { t.Parallel() tests := []struct { + msg string server UpstreamServer serverNGX UpstreamServer expected bool @@ -383,11 +407,13 @@ func TestHaveSameParameters(t *testing.T) { server: UpstreamServer{}, serverNGX: UpstreamServer{}, expected: true, + msg: "empty", }, { server: UpstreamServer{ID: 2}, serverNGX: UpstreamServer{ID: 3}, expected: true, + msg: "different ID", }, { server: UpstreamServer{}, @@ -401,6 +427,7 @@ func TestHaveSameParameters(t *testing.T) { Down: &defaultDown, }, expected: true, + msg: "default values", }, { server: UpstreamServer{ @@ -426,35 +453,43 @@ func TestHaveSameParameters(t *testing.T) { Down: &defaultDown, }, expected: true, + msg: "same values", }, { server: UpstreamServer{SlowStart: "10s"}, serverNGX: UpstreamServer{}, expected: false, + msg: "different SlowStart", }, { server: UpstreamServer{}, serverNGX: UpstreamServer{SlowStart: "10s"}, expected: false, + msg: "different SlowStart 2", }, { server: UpstreamServer{SlowStart: "20s"}, serverNGX: UpstreamServer{SlowStart: "10s"}, expected: false, + msg: "different SlowStart 3", }, } for _, test := range tests { - result := haveSameParameters(test.server, test.serverNGX) - if result != test.expected { - t.Errorf("haveSameParameters(%v, %v) returned %v but expected %v", test.server, test.serverNGX, result, test.expected) - } + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + result := test.server.hasSameParametersAs(test.serverNGX) + if result != test.expected { + t.Errorf("(%v) hasSameParametersAs (%v) returned %v but expected %v", test.server, test.serverNGX, result, test.expected) + } + }) } } func TestHaveSameParametersForStream(t *testing.T) { t.Parallel() tests := []struct { + msg string server StreamUpstreamServer serverNGX StreamUpstreamServer expected bool @@ -463,11 +498,13 @@ func TestHaveSameParametersForStream(t *testing.T) { server: StreamUpstreamServer{}, serverNGX: StreamUpstreamServer{}, expected: true, + msg: "empty", }, { server: StreamUpstreamServer{ID: 2}, serverNGX: StreamUpstreamServer{ID: 3}, expected: true, + msg: "different ID", }, { server: StreamUpstreamServer{}, @@ -481,6 +518,7 @@ func TestHaveSameParametersForStream(t *testing.T) { Down: &defaultDown, }, expected: true, + msg: "default values", }, { server: StreamUpstreamServer{ @@ -506,24 +544,30 @@ func TestHaveSameParametersForStream(t *testing.T) { Down: &defaultDown, }, expected: true, + msg: "same values", }, { server: StreamUpstreamServer{}, serverNGX: StreamUpstreamServer{SlowStart: "10s"}, expected: false, + msg: "different SlowStart", }, { server: StreamUpstreamServer{SlowStart: "20s"}, serverNGX: StreamUpstreamServer{SlowStart: "10s"}, expected: false, + msg: "different SlowStart 2", }, } for _, test := range tests { - result := haveSameParametersForStream(test.server, test.serverNGX) - if result != test.expected { - t.Errorf("haveSameParametersForStream(%v, %v) returned %v but expected %v", test.server, test.serverNGX, result, test.expected) - } + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + result := test.server.hasSameParametersAs(test.serverNGX) + if result != test.expected { + t.Errorf("(%v) hasSameParametersAs (%v) returned %v but expected %v", test.server, test.serverNGX, result, test.expected) + } + }) } } @@ -599,22 +643,92 @@ func TestClientWithHTTPClient(t *testing.T) { } } +func TestClientWithMaxAPI(t *testing.T) { + t.Parallel() + tests := []struct { + name string + apiVersions string + expected int + }{ + { + name: "Test 1: API versions contains invalid version", + apiVersions: `[4, 5, 6, 7, 8, 9, 25]`, + expected: APIVersion, + }, + { + name: "Test 2: No API versions, default API Version is used", + apiVersions: ``, + expected: APIVersion, + }, + { + name: "Test 3: API version lower than default", + apiVersions: `[4, 5, 6, 7]`, + expected: 7, + }, + { + name: "Test 4: No API versions, default API version is used", + apiVersions: `[""]`, + expected: APIVersion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Test creating a new client with max API version + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/": + _, err := w.Write([]byte(tt.apiVersions)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + defer ts.Close() + + client, err := NewNginxClient(ts.URL, WithMaxAPIVersion()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatalf("client is nil") + } + if client.apiVersion != tt.expected { + t.Fatalf("expected client.apiVersion to be %v, but got %v", tt.expected, client.apiVersion) + } + }) + } +} + func TestGetStats_NoStreamEndpoint(t *testing.T) { t.Parallel() + var writeLock sync.Mutex + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/" { + writeLock.Lock() + defer writeLock.Unlock() + + switch { + case r.RequestURI == "/": + _, err := w.Write([]byte(`[4, 5, 6, 7, 8, 9]`)) if err != nil { t.Fatalf("unexpected error: %v", err) } - } else if r.RequestURI == "/7/" { + case r.RequestURI == "/7/": _, err := w.Write([]byte(`["nginx","processes","connections","slabs","http","resolvers","ssl"]`)) if err != nil { t.Fatalf("unexpected error: %v", err) } - } else if strings.HasPrefix(r.RequestURI, "/7/stream") { + case strings.HasPrefix(r.RequestURI, "/7/stream"): t.Fatal("Stream endpoint should not be called since it does not exist.") - } else { + default: _, err := w.Write([]byte(`{}`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -632,7 +746,7 @@ func TestGetStats_NoStreamEndpoint(t *testing.T) { t.Fatalf("client is nil") } - stats, err := client.GetStats() + stats, err := client.GetStats(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -651,20 +765,81 @@ func TestGetStats_NoStreamEndpoint(t *testing.T) { } } +func TestGetStats_Connections(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/": + _, err := w.Write([]byte(`[4, 5, 6, 7, 8, 9]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case r.RequestURI == "/8/": + _, err := w.Write([]byte(`["nginx","processes","connections","slabs","http","resolvers","ssl","workers"]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case strings.HasPrefix(r.RequestURI, "/8/connections"): + _, err := w.Write([]byte(`{ + "active": -1, + "idle": 3, + "accepted": 5, + "dropped": 2 + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + + defer ts.Close() + + // Test creating a new client with a supported API version on the server + client, err := NewNginxClient(ts.URL, WithAPIVersion(8), WithCheckAPI()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatalf("client is nil") + } + + stats, err := client.GetStats(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + testStats := Connections{ + Accepted: 5, + Dropped: 2, + Active: -1, + Idle: 3, + } + + if !reflect.DeepEqual(stats.Connections, testStats) { + t.Fatalf("Connection stats: expected %v, actual %v", testStats, stats.Connections) + } +} + func TestGetStats_SSL(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/" { + switch { + case r.RequestURI == "/": _, err := w.Write([]byte(`[4, 5, 6, 7, 8, 9]`)) if err != nil { t.Fatalf("unexpected error: %v", err) } - } else if r.RequestURI == "/8/" { + case r.RequestURI == "/8/": _, err := w.Write([]byte(`["nginx","processes","connections","slabs","http","resolvers","ssl","workers"]`)) if err != nil { t.Fatalf("unexpected error: %v", err) } - } else if strings.HasPrefix(r.RequestURI, "/8/ssl") { + case strings.HasPrefix(r.RequestURI, "/8/ssl"): _, err := w.Write([]byte(`{ "handshakes" : 79572, "handshakes_failed" : 21025, @@ -684,7 +859,12 @@ func TestGetStats_SSL(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - } else { + case strings.HasPrefix(r.RequestURI, "/8/stream"): + _, err := w.Write([]byte(`[""]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: _, err := w.Write([]byte(`{}`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -702,7 +882,7 @@ func TestGetStats_SSL(t *testing.T) { t.Fatalf("client is nil") } - stats, err := client.GetStats() + stats, err := client.GetStats(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -728,3 +908,807 @@ func TestGetStats_SSL(t *testing.T) { t.Fatalf("SSL stats: expected %v, actual %v", testStats, stats.SSL) } } + +func TestGetMaxAPIVersionServer(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/": + _, err := w.Write([]byte(`[4, 5, 6, 7]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + defer ts.Close() + + c, err := NewNginxClient(ts.URL) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + maxVer, err := c.GetMaxAPIVersion(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if maxVer != 7 { + t.Fatalf("expected 7, got %v", maxVer) + } +} + +func TestGetMaxAPIVersionClient(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/": + _, err := w.Write([]byte(`[4, 5, 6, 7, 8, 9, 25]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + defer ts.Close() + + c, err := NewNginxClient(ts.URL) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + maxVer, err := c.GetMaxAPIVersion(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if maxVer != c.apiVersion { + t.Fatalf("expected %v, got %v", c.apiVersion, maxVer) + } +} + +func TestExtractPlusVersion(t *testing.T) { + t.Parallel() + tests := []struct { + name string + version string + expected int + }{ + { + name: "r32", + version: "nginx-plus-r32", + expected: 32, + }, + { + name: "r32p1", + version: "nginx-plus-r32-p1", + expected: 32, + }, + { + name: "r32p2", + version: "nginx-plus-r32-p2", + expected: 32, + }, + { + name: "r33", + version: "nginx-plus-r33", + expected: 33, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + version, err := extractPlusVersionValues(test.version) + if err != nil { + t.Error(err) + } + if version != test.expected { + t.Errorf("values do not match, got: %d, expected %d)", version, test.expected) + } + }) + } +} + +func TestExtractPlusVersionNegativeCase(t *testing.T) { + t.Parallel() + tests := []struct { + name string + version string + }{ + { + name: "no-number", + version: "nginx-plus-rxx", + }, + { + name: "extra-chars", + version: "nginx-plus-rxx4343", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, err := extractPlusVersionValues(test.version) + if err == nil { + t.Errorf("Expected error but got %v", err) + } + }) + } +} + +func TestUpdateHTTPServers(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + reqServers []UpstreamServer + responses []response + expAdded, expDeleted, expUpdated int + expErr bool + }{ + "successfully add 1 server": { + reqServers: []UpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + }, + // response for addHTTPServer POST server for http server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + }, + "successfully update 1 server": { + reqServers: []UpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{ + {ID: 1, Server: "127.0.0.1:80", Route: "/test"}, + }, + }, + // response for UpdateHTTPServer PATCH server for http server + { + statusCode: http.StatusOK, + }, + }, + expUpdated: 1, + }, + "successfully delete 1 server": { + reqServers: []UpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{ + {ID: 1, Server: "127.0.0.1:80"}, + {ID: 2, Server: "127.0.0.2:80"}, + }, + }, + // response for deleteHTTPServer DELETE server for http server + { + statusCode: http.StatusOK, + }, + }, + expDeleted: 1, + }, + "successfully add 1 server, update 1 server, delete 1 server": { + reqServers: []UpstreamServer{ + {Server: "127.0.0.1:80", Route: "/test"}, + {Server: "127.0.0.2:80"}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{ + {ID: 1, Server: "127.0.0.1:80"}, + {ID: 2, Server: "127.0.0.3:80"}, + }, + }, + // response for addHTTPServer POST server for http server + { + statusCode: http.StatusCreated, + }, + // response for deleteHTTPServer DELETE server for http server + { + statusCode: http.StatusOK, + }, + // response for UpdateHTTPServer PATCH server for http server + { + statusCode: http.StatusOK, + }, + }, + expAdded: 1, + expUpdated: 1, + expDeleted: 1, + }, + "successfully add 1 server with ignored identical duplicate": { + reqServers: []UpstreamServer{ + {Server: "127.0.0.1:80", Route: "/test"}, + {Server: "127.0.0.1", Route: "/test"}, + {Server: "127.0.0.1:80", Route: "/test", MaxConns: &defaultMaxConns}, + {Server: "127.0.0.1:80", Route: "/test", Backup: &defaultBackup}, + {Server: "127.0.0.1", Route: "/test", SlowStart: defaultSlowStart}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addHTTPServer POST server for http server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + }, + "successfully add 1 server, receive 1 error for non-identical duplicates": { + reqServers: []UpstreamServer{ + {Server: "127.0.0.1:80", Route: "/test"}, + {Server: "127.0.0.1:80", Route: "/test"}, + {Server: "127.0.0.2:80", Route: "/test1"}, + {Server: "127.0.0.2:80", Route: "/test2"}, + {Server: "127.0.0.2:80", Route: "/test3"}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addHTTPServer POST server for http server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + expErr: true, + }, + "successfully add 1 server, receive 1 error": { + reqServers: []UpstreamServer{ + {Server: "127.0.0.1:80"}, + {Server: "127.0.0.1:443"}, + }, + responses: []response{ // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addHTTPServer POST server for server1 + { + statusCode: http.StatusInternalServerError, + servers: []UpstreamServer{}, + }, + // response for addHTTPServer POST server for server2 + { + statusCode: http.StatusCreated, + servers: []UpstreamServer{}, + }, + }, + expAdded: 1, + expErr: true, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var requests []*http.Request + handler := &fakeHandler{ + func(w http.ResponseWriter, r *http.Request) { + requests = append(requests, r) + + if len(tc.responses) == 0 { + t.Fatal("ran out of responses") + } + if r.Method == http.MethodPost || r.Method == http.MethodPut { + contentType, ok := r.Header["Content-Type"] + if !ok { + t.Fatalf("expected request type %s to have a Content-Type header", r.Method) + } + if len(contentType) != 1 || contentType[0] != "application/json" { + t.Fatalf("expected request type %s to have a Content-Type header value of 'application/json'", r.Method) + } + } + + re := tc.responses[0] + tc.responses = tc.responses[1:] + + w.WriteHeader(re.statusCode) + + resp, err := json.Marshal(re.servers) + if err != nil { + t.Fatal(err) + } + _, err = w.Write(resp) + if err != nil { + t.Fatal(err) + } + }, + } + + server := httptest.NewServer(handler) + defer server.Close() + + client, err := NewNginxClient(server.URL, WithHTTPClient(&http.Client{})) + if err != nil { + t.Fatal(err) + } + + added, deleted, updated, err := client.UpdateHTTPServers(context.Background(), "fakeUpstream", tc.reqServers) + if tc.expErr && err == nil { + t.Fatal("expected to receive an error") + } + if !tc.expErr && err != nil { + t.Fatalf("received an unexpected error: %v", err) + } + + if len(added) != tc.expAdded { + t.Fatalf("expected to get %d added server(s), instead got %d", tc.expAdded, len(added)) + } + if len(deleted) != tc.expDeleted { + t.Fatalf("expected to get %d deleted server(s), instead got %d", tc.expDeleted, len(deleted)) + } + if len(updated) != tc.expUpdated { + t.Fatalf("expected to get %d updated server(s), instead got %d", tc.expUpdated, len(updated)) + } + if len(tc.responses) != 0 { + t.Fatalf("did not use all expected responses, %d unused", len(tc.responses)) + } + }) + } +} + +func TestUpdateStreamServers(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + reqServers []StreamUpstreamServer + responses []response + expAdded, expDeleted, expUpdated int + expErr bool + }{ + "successfully add 1 server": { + reqServers: []StreamUpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + }, + // response for addStreamServer POST server for stream server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + }, + "successfully update 1 server": { + reqServers: []StreamUpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []StreamUpstreamServer{ + {ID: 1, Server: "127.0.0.1:80", SlowStart: "30s"}, + }, + }, + // response for UpdateStreamServer PATCH server for stream server + { + statusCode: http.StatusOK, + }, + }, + expUpdated: 1, + }, + "successfully delete 1 server": { + reqServers: []StreamUpstreamServer{{Server: "127.0.0.1:80"}}, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []StreamUpstreamServer{ + {ID: 1, Server: "127.0.0.1:80"}, + {ID: 2, Server: "127.0.0.2:80"}, + }, + }, + // response for deleteStreamServer DELETE server for stream server + { + statusCode: http.StatusOK, + }, + }, + expDeleted: 1, + }, + "successfully add 1 server, update 1 server, delete 1 server": { + reqServers: []StreamUpstreamServer{ + {Server: "127.0.0.1:80", SlowStart: "30s"}, + {Server: "127.0.0.2:80"}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []StreamUpstreamServer{ + {ID: 1, Server: "127.0.0.1:80"}, + {ID: 2, Server: "127.0.0.3:80"}, + }, + }, + // response for addStreamServer POST server for stream server + { + statusCode: http.StatusCreated, + }, + // response for deleteStreamServer DELETE server for stream server + { + statusCode: http.StatusOK, + }, + // response for UpdateStreamServer PATCH server for stream server + { + statusCode: http.StatusOK, + }, + }, + expAdded: 1, + expUpdated: 1, + expDeleted: 1, + }, + "successfully add 1 server with ignored identical duplicate": { + reqServers: []StreamUpstreamServer{ + {Server: "127.0.0.1:80", SlowStart: "30s"}, + {Server: "127.0.0.1", SlowStart: "30s"}, + {Server: "127.0.0.1:80", SlowStart: "30s", MaxConns: &defaultMaxConns}, + {Server: "127.0.0.1", SlowStart: "30s", MaxFails: &defaultMaxFails}, + {Server: "127.0.0.1", SlowStart: "30s", FailTimeout: defaultFailTimeout}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addStreamServer POST server for stream server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + }, + "successfully add 1 server, receive 1 error for non-identical duplicates": { + reqServers: []StreamUpstreamServer{ + {Server: "127.0.0.1:80", SlowStart: "30s"}, + {Server: "127.0.0.1:80", SlowStart: "30s"}, + {Server: "127.0.0.2:80", SlowStart: "10s"}, + {Server: "127.0.0.2:80", SlowStart: "20s"}, + {Server: "127.0.0.2:80", SlowStart: "30s"}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addStreamServer POST server for stream server + { + statusCode: http.StatusCreated, + }, + }, + expAdded: 1, + expErr: true, + }, + "successfully add 1 server, receive 1 error": { + reqServers: []StreamUpstreamServer{ + {Server: "127.0.0.1:2000"}, + {Server: "127.0.0.1:3000"}, + }, + responses: []response{ + // response for first serversInNginx GET servers + { + statusCode: http.StatusOK, + servers: []UpstreamServer{}, + }, + // response for addStreamServer POST server for server1 + { + statusCode: http.StatusInternalServerError, + servers: []UpstreamServer{}, + }, + // response for addStreamServer POST server for server2 + { + statusCode: http.StatusCreated, + servers: []UpstreamServer{}, + }, + }, + expAdded: 1, + expErr: true, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var requests []*http.Request + handler := &fakeHandler{ + func(w http.ResponseWriter, r *http.Request) { + requests = append(requests, r) + + if len(tc.responses) == 0 { + t.Fatal("ran out of responses") + } + if r.Method == http.MethodPost || r.Method == http.MethodPut { + contentType, ok := r.Header["Content-Type"] + if !ok { + t.Fatalf("expected request type %s to have a Content-Type header", r.Method) + } + if len(contentType) != 1 || contentType[0] != "application/json" { + t.Fatalf("expected request type %s to have a Content-Type header value of 'application/json'", r.Method) + } + } + + re := tc.responses[0] + tc.responses = tc.responses[1:] + + w.WriteHeader(re.statusCode) + + resp, err := json.Marshal(re.servers) + if err != nil { + t.Fatal(err) + } + _, err = w.Write(resp) + if err != nil { + t.Fatal(err) + } + }, + } + + server := httptest.NewServer(handler) + defer server.Close() + + client, err := NewNginxClient(server.URL, WithHTTPClient(&http.Client{})) + if err != nil { + t.Fatal(err) + } + + added, deleted, updated, err := client.UpdateStreamServers(context.Background(), "fakeUpstream", tc.reqServers) + if tc.expErr && err == nil { + t.Fatal("expected to receive an error") + } + if !tc.expErr && err != nil { + t.Fatalf("received an unexpected error: %v", err) + } + if len(added) != tc.expAdded { + t.Fatalf("expected to get %d added server(s), instead got %d", tc.expAdded, len(added)) + } + if len(deleted) != tc.expDeleted { + t.Fatalf("expected to get %d deleted server(s), instead got %d", tc.expDeleted, len(deleted)) + } + if len(updated) != tc.expUpdated { + t.Fatalf("expected to get %d updated server(s), instead got %d", tc.expUpdated, len(updated)) + } + if len(tc.responses) != 0 { + t.Fatalf("did not use all expected responses, %d unused", len(tc.responses)) + } + }) + } +} + +func TestInternalError(t *testing.T) { + t.Parallel() + + // mimic a user-defined interface type + type TestStatusError interface { + Status() int + Code() string + } + + //nolint // ignore golangci-lint err113 sugggestion to create package level static error + anotherErr := errors.New("another error") + + notFoundErr := &internalError{ + err: "not found error", + apiError: apiError{ + Text: "not found error", + Status: http.StatusNotFound, + Code: "not found code", + }, + } + + testcases := map[string]struct { + inputErr error + expectedCode string + expectedStatus int + }{ + "simple not found": { + inputErr: notFoundErr, + expectedStatus: http.StatusNotFound, + expectedCode: "not found code", + }, + "not found joined with another error": { + inputErr: errors.Join(notFoundErr, anotherErr), + expectedStatus: http.StatusNotFound, + expectedCode: "not found code", + }, + "not found wrapped with another error": { + inputErr: notFoundErr.Wrap("some error"), + expectedStatus: http.StatusNotFound, + expectedCode: "not found code", + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var se TestStatusError + ok := errors.As(tc.inputErr, &se) + if !ok { + t.Fatalf("could not cast error %v as StatusError", tc.inputErr) + } + + if se.Status() != tc.expectedStatus { + t.Fatalf("expected status %d, got status %d", tc.expectedStatus, se.Status()) + } + + if se.Code() != tc.expectedCode { + t.Fatalf("expected code %s, got code %s", tc.expectedCode, se.Code()) + } + }) + } +} + +func TestLicenseWithReporting(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/": + _, err := w.Write([]byte(`[1,2,3,4,5,6,7,8,9]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case r.RequestURI == "/9/": + _, err := w.Write([]byte(`["nginx","processes","connections","slabs","http","resolvers","ssl","license","workers"]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case strings.HasPrefix(r.RequestURI, "/9/nginx"): + _, err := w.Write([]byte(`{ + "version": "1.29.0", + "build": "nginx-plus-r34" + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case strings.HasPrefix(r.RequestURI, "/9/license"): + _, err := w.Write([]byte(`{ + "active_till" : 428250000, + "eval": false, + "reporting": { + "healthy": true, + "fails": 42, + "grace": 86400 + } + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + defer ts.Close() + + client, err := NewNginxClient(ts.URL, WithAPIVersion(9), WithCheckAPI()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatalf("client is nil") + } + + license, err := client.GetNginxLicense(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + testReporting := LicenseReporting{ + Healthy: true, + Fails: 42, + Grace: 86400, + } + + testLicense := NginxLicense{ + ActiveTill: 428250000, + Eval: false, + Reporting: &testReporting, + } + + if !reflect.DeepEqual(license, &testLicense) { + t.Fatalf("NGINX license: expected %v, actual %v; NGINX reporting: expected %v, actual %v", testLicense, license, testReporting, license.Reporting) + } +} + +func TestLicenseWithoutReporting(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.RequestURI == "/": + _, err := w.Write([]byte(`[1,2,3,4,5,6,7,8,9]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case r.RequestURI == "/9/": + _, err := w.Write([]byte(`["nginx","processes","connections","slabs","http","resolvers","ssl","license","workers"]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case strings.HasPrefix(r.RequestURI, "/9/nginx"): + _, err := w.Write([]byte(`{ + "version": "1.29.0", + "build": "nginx-plus-r34" + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case strings.HasPrefix(r.RequestURI, "/9/license"): + _, err := w.Write([]byte(`{ + "active_till" : 428250000, + "eval": false + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + default: + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + })) + defer ts.Close() + + client, err := NewNginxClient(ts.URL, WithAPIVersion(9), WithCheckAPI()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatalf("client is nil") + } + + license, err := client.GetNginxLicense(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + testLicense := NginxLicense{ + ActiveTill: 428250000, + Eval: false, + Reporting: nil, + } + + if !reflect.DeepEqual(license, &testLicense) { + t.Fatalf("NGINX license: expected %v, actual %v", testLicense, license) + } +} + +type response struct { + servers interface{} + statusCode int +} + +type fakeHandler struct { + handler func(w http.ResponseWriter, r *http.Request) +} + +func (h *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.handler(w, r) +} diff --git a/compose.yaml b/compose.yaml index d432656e..189267f7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -29,12 +29,12 @@ services: service: nginx test: - image: golang:1.21 + image: golang:1.25 volumes: - type: bind source: ./ - target: /go/src/github.com/nginxinc/nginx-plus-go-client - working_dir: /go/src/github.com/nginxinc/nginx-plus-go-client + target: /go/src/github.com/nginx/nginx-plus-go-client + working_dir: /go/src/github.com/nginx/nginx-plus-go-client command: go test -v -shuffle=on -race tests/client_test.go depends_on: - nginx diff --git a/docker/Dockerfile b/docker/Dockerfile index f4ddb379..8e7b07b2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.7 +# syntax=docker/dockerfile:1.19 FROM debian:12-slim LABEL maintainer="NGINX Docker Maintainers " diff --git a/go.mod b/go.mod index 4fc51a45..c1a12e69 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module github.com/nginxinc/nginx-plus-go-client +module github.com/nginx/nginx-plus-go-client/v3 -go 1.21.2 +go 1.24.2 + +require golang.org/x/sync v0.17.0 diff --git a/go.sum b/go.sum index e69de29b..c02bc3ef 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= diff --git a/release-process.md b/release-process.md index 2ecfbdd6..bdb41cc4 100644 --- a/release-process.md +++ b/release-process.md @@ -2,6 +2,16 @@ This document outlines the steps involved in the release process for the NGINX Plus Go Client project. + + +## Table of Contents + +- [Versioning](#versioning) +- [Release Planning and Development](#release-planning-and-development) +- [Releasing a New Version](#releasing-a-new-version) + + + ## Versioning The project follows [Semantic Versioning](https://semver.org/) for versioning. @@ -9,7 +19,7 @@ The project follows [Semantic Versioning](https://semver.org/) for versioning. ## Release Planning and Development The features that will go into the next release are reflected in the -corresponding [milestone](https://github.com/nginxinc/nginx-plus-go-client/milestones). Refer to +corresponding [milestone](https://github.com/nginx/nginx-plus-go-client/milestones). Refer to the [Issue Lifecycle](/ISSUE_LIFECYCLE.md) document for information on issue creation and assignment to releases. ## Releasing a New Version @@ -17,13 +27,13 @@ the [Issue Lifecycle](/ISSUE_LIFECYCLE.md) document for information on issue cre 1. Create an issue to define and track release-related activities. Choose a title that follows the format `Release X.Y.Z`. 2. Stop merging any new work into the main branch. -3. Check the release draft under the [GitHub releases](https://github.com/nginxinc/nginx-plus-go-client/releases) page -to ensure that everything is in order. +3. Check the release draft under the [GitHub releases](https://github.com/nginx/nginx-plus-go-client/releases) page + to ensure that everything is in order. 4. Create and push the release tag in the format `vX.Y.Z`: - ```bash - git tag -a vX.Y.Z -m "Release vX.Y.Z" - git push origin vX.Y.Z - ``` + ```bash + git tag -a vX.Y.Z -m "Release vX.Y.Z" + git push origin vX.Y.Z + ``` - As a result, the CI/CD pipeline will publish the release and announce it in the community Slack. + As a result, the CI/CD pipeline will publish the release and announce it in the community Slack. diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..891b5869 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>nginx/k8s-common", + "schedule:earlyMondays" + ] +} diff --git a/tests/client_no_stream_test.go b/tests/client_no_stream_test.go index cb29d465..d6ce2f47 100644 --- a/tests/client_no_stream_test.go +++ b/tests/client_no_stream_test.go @@ -1,10 +1,11 @@ package tests import ( + "context" "testing" - "github.com/nginxinc/nginx-plus-go-client/client" - "github.com/nginxinc/nginx-plus-go-client/tests/helpers" + "github.com/nginx/nginx-plus-go-client/v3/client" + "github.com/nginx/nginx-plus-go-client/v3/tests/helpers" ) // TestStatsNoStream tests the peculiar behavior of getting Stream-related @@ -12,12 +13,13 @@ import ( // The API returns a special error code that we can use to determine if the API // is misconfigured or of the stream block is missing. func TestStatsNoStream(t *testing.T) { + t.Parallel() c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } - - stats, err := c.GetStats() + ctx := context.Background() + stats, err := c.GetStats(ctx) if err != nil { t.Errorf("Error getting stats: %v", err) } diff --git a/tests/client_test.go b/tests/client_test.go index 627631e1..0907fc89 100644 --- a/tests/client_test.go +++ b/tests/client_test.go @@ -1,13 +1,14 @@ package tests import ( + "context" "net" "reflect" "testing" "time" - "github.com/nginxinc/nginx-plus-go-client/client" - "github.com/nginxinc/nginx-plus-go-client/tests/helpers" + "github.com/nginx/nginx-plus-go-client/v3/client" + "github.com/nginx/nginx-plus-go-client/v3/tests/helpers" ) const ( @@ -32,6 +33,7 @@ var ( defaultWeight = 1 ) +//nolint:paralleltest func TestStreamClient(t *testing.T) { c, err := client.NewNginxClient( helpers.GetAPIEndpoint(), @@ -46,31 +48,63 @@ func TestStreamClient(t *testing.T) { } // test adding a stream server + ctx := context.Background() - err = c.AddStreamServer(streamUpstream, streamServer) + err = c.AddStreamServer(ctx, streamUpstream, streamServer) if err != nil { t.Fatalf("Error when adding a server: %v", err) } - err = c.AddStreamServer(streamUpstream, streamServer) + err = c.AddStreamServer(ctx, streamUpstream, streamServer) if err == nil { t.Errorf("Adding a duplicated server succeeded") } - // test deleting a stream server + // test updating a stream server + streamServers, err := c.GetStreamServers(ctx, streamUpstream) + if err != nil { + t.Errorf("Error getting stream servers: %v", err) + } + if len(streamServers) != 1 { + t.Errorf("Expected 1 servers, got %v", streamServers) + } - err = c.DeleteStreamServer(streamUpstream, streamServer.Server) + streamServers[0].SlowStart = "30s" + err = c.UpdateStreamServer(ctx, streamUpstream, streamServers[0]) + if err != nil { + t.Errorf("Error when updating a server: %v", err) + } + + streamServers, err = c.GetStreamServers(ctx, streamUpstream) + if err != nil { + t.Errorf("Error getting stream servers: %v", err) + } + if len(streamServers) != 1 { + t.Errorf("Expected 1 servers, got %v", streamServers) + } + if streamServers[0].SlowStart != "30s" { + t.Errorf("The server wasn't successfully updated: expected a 'SlowStart' of 30s, actual was %s", streamServers[0].SlowStart) + } + + streamServers[0].ID++ + err = c.UpdateStreamServer(ctx, streamUpstream, streamServers[0]) + if err == nil { + t.Errorf("Updating a server without a matching server ID succeeded") + } + + // test deleting a stream server + err = c.DeleteStreamServer(ctx, streamUpstream, streamServer.Server) if err != nil { t.Fatalf("Error when deleting a server: %v", err) } - err = c.DeleteStreamServer(streamUpstream, streamServer.Server) + err = c.DeleteStreamServer(ctx, streamUpstream, streamServer.Server) if err == nil { t.Errorf("Deleting a nonexisting server succeeded") } - streamServers, err := c.GetStreamServers(streamUpstream) + streamServers, err = c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Errorf("Error getting stream servers: %v", err) } @@ -91,7 +125,7 @@ func TestStreamClient(t *testing.T) { }, } - streamAdded, streamDeleted, streamUpdated, err := c.UpdateStreamServers(streamUpstream, streamServers1) + streamAdded, streamDeleted, streamUpdated, err := c.UpdateStreamServers(ctx, streamUpstream, streamServers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -107,7 +141,7 @@ func TestStreamClient(t *testing.T) { // test getting servers - streamServers, err = c.GetStreamServers(streamUpstream) + streamServers, err = c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -117,7 +151,7 @@ func TestStreamClient(t *testing.T) { // updating with the same servers - added, deleted, updated, err := c.UpdateStreamServers(streamUpstream, streamServers1) + added, deleted, updated, err := c.UpdateStreamServers(ctx, streamUpstream, streamServers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -144,7 +178,7 @@ func TestStreamClient(t *testing.T) { // updating one server with only one different parameter streamServers[1].SlowStart = newSlowStart - added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, streamServers) + added, deleted, updated, err = c.UpdateStreamServers(ctx, streamUpstream, streamServers) if err != nil { t.Fatalf("Error when updating server with different parameters: %v", err) } @@ -158,7 +192,7 @@ func TestStreamClient(t *testing.T) { t.Errorf("The number of updated servers %v != 2", len(updated)) } - streamServers, err = c.GetStreamServers(streamUpstream) + streamServers, err = c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -209,7 +243,7 @@ func TestStreamClient(t *testing.T) { // updating with 2 new servers, 1 existing - added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, streamServers2) + added, deleted, updated, err = c.UpdateStreamServers(ctx, streamUpstream, streamServers2) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -225,7 +259,7 @@ func TestStreamClient(t *testing.T) { // updating with zero servers - removing - added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) + added, deleted, updated, err = c.UpdateStreamServers(ctx, streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -241,7 +275,7 @@ func TestStreamClient(t *testing.T) { // test getting servers again - servers, err := c.GetStreamServers(streamUpstream) + servers, err := c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -252,6 +286,7 @@ func TestStreamClient(t *testing.T) { } func TestStreamUpstreamServer(t *testing.T) { + t.Parallel() c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) @@ -273,11 +308,13 @@ func TestStreamUpstreamServer(t *testing.T) { Backup: &backup, Down: &down, } - err = c.AddStreamServer(streamUpstream, streamServer) + ctx := context.Background() + + err = c.AddStreamServer(ctx, streamUpstream, streamServer) if err != nil { t.Errorf("Error adding upstream server: %v", err) } - servers, err := c.GetStreamServers(streamUpstream) + servers, err := c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Fatalf("Error getting stream servers: %v", err) } @@ -292,12 +329,13 @@ func TestStreamUpstreamServer(t *testing.T) { } // remove stream upstream servers - _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) + _, _, _, err = c.UpdateStreamServers(ctx, streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %v", err) } } +//nolint:paralleltest func TestClient(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -305,13 +343,13 @@ func TestClient(t *testing.T) { } // test checking an upstream for existence - - err = c.CheckIfUpstreamExists(upstream) + ctx := context.Background() + err = c.CheckIfUpstreamExists(ctx, upstream) if err != nil { t.Fatalf("Error when checking an upstream for existence: %v", err) } - err = c.CheckIfUpstreamExists("random") + err = c.CheckIfUpstreamExists(ctx, "random") if err == nil { t.Errorf("Nonexisting upstream exists") } @@ -322,25 +360,56 @@ func TestClient(t *testing.T) { // test adding a http server - err = c.AddHTTPServer(upstream, server) + err = c.AddHTTPServer(ctx, upstream, server) if err != nil { t.Fatalf("Error when adding a server: %v", err) } - err = c.AddHTTPServer(upstream, server) + err = c.AddHTTPServer(ctx, upstream, server) if err == nil { t.Errorf("Adding a duplicated server succeeded") } - // test deleting a http server + // test updating an http server + servers, err := c.GetHTTPServers(ctx, upstream) + if err != nil { + t.Errorf("Error getting servers: %v", err) + } + if len(servers) != 1 { + t.Errorf("Expected 1 servers, got %v", servers) + } - err = c.DeleteHTTPServer(upstream, server.Server) + servers[0].SlowStart = "30s" + err = c.UpdateHTTPServer(ctx, upstream, servers[0]) + if err != nil { + t.Errorf("Error when updating a server: %v", err) + } + + servers, err = c.GetHTTPServers(ctx, upstream) + if err != nil { + t.Errorf("Error getting servers: %v", err) + } + if len(servers) != 1 { + t.Errorf("Expected 1 servers, got %v", servers) + } + if servers[0].SlowStart != "30s" { + t.Errorf("The server wasn't successfully updated: expected a 'SlowStart' of 30s, actual was %s", servers[0].SlowStart) + } + + servers[0].ID++ + err = c.UpdateHTTPServer(ctx, upstream, servers[0]) + if err == nil { + t.Errorf("Updating a server without a matching server ID succeeded") + } + + // test deleting a http server + err = c.DeleteHTTPServer(ctx, upstream, server.Server) if err != nil { t.Fatalf("Error when deleting a server: %v", err) } - err = c.DeleteHTTPServer(upstream, server.Server) + err = c.DeleteHTTPServer(ctx, upstream, server.Server) if err == nil { t.Errorf("Deleting a nonexisting server succeeded") } @@ -358,7 +427,7 @@ func TestClient(t *testing.T) { }, } - added, deleted, updated, err := c.UpdateHTTPServers(upstream, servers1) + added, deleted, updated, err := c.UpdateHTTPServers(ctx, upstream, servers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -374,7 +443,7 @@ func TestClient(t *testing.T) { // test getting servers - servers, err := c.GetHTTPServers(upstream) + servers, err = c.GetHTTPServers(ctx, upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -386,7 +455,7 @@ func TestClient(t *testing.T) { // updating with the same servers - added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers1) + added, deleted, updated, err = c.UpdateHTTPServers(ctx, upstream, servers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -413,7 +482,7 @@ func TestClient(t *testing.T) { // updating one server with only one different parameter servers[1].SlowStart = newSlowStart - added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers) + added, deleted, updated, err = c.UpdateHTTPServers(ctx, upstream, servers) if err != nil { t.Fatalf("Error when updating server with different parameters: %v", err) } @@ -427,7 +496,7 @@ func TestClient(t *testing.T) { t.Errorf("The number of updated servers %v != 2", len(updated)) } - servers, err = c.GetHTTPServers(upstream) + servers, err = c.GetHTTPServers(ctx, upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -478,7 +547,7 @@ func TestClient(t *testing.T) { // updating with 2 new servers, 1 existing - added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers2) + added, deleted, updated, err = c.UpdateHTTPServers(ctx, upstream, servers2) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -494,7 +563,7 @@ func TestClient(t *testing.T) { // updating with zero servers - removing - added, deleted, updated, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) + added, deleted, updated, err = c.UpdateHTTPServers(ctx, upstream, []client.UpstreamServer{}) if err != nil { t.Fatalf("Error when updating servers: %v", err) } @@ -510,7 +579,7 @@ func TestClient(t *testing.T) { // test getting servers again - servers, err = c.GetHTTPServers(upstream) + servers, err = c.GetHTTPServers(ctx, upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } @@ -520,6 +589,7 @@ func TestClient(t *testing.T) { } } +//nolint:paralleltest func TestUpstreamServer(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -543,11 +613,12 @@ func TestUpstreamServer(t *testing.T) { Backup: &backup, Down: &down, } - err = c.AddHTTPServer(upstream, server) + ctx := context.Background() + err = c.AddHTTPServer(ctx, upstream, server) if err != nil { t.Errorf("Error adding upstream server: %v", err) } - servers, err := c.GetHTTPServers(upstream) + servers, err := c.GetHTTPServers(ctx, upstream) if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) } @@ -562,12 +633,13 @@ func TestUpstreamServer(t *testing.T) { } // remove upstream servers - _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) + _, _, _, err = c.UpdateHTTPServers(ctx, upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %v", err) } } +//nolint:paralleltest func TestStats(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -577,12 +649,13 @@ func TestStats(t *testing.T) { server := client.UpstreamServer{ Server: "127.0.0.1:8080", } - err = c.AddHTTPServer(upstream, server) + ctx := context.Background() + err = c.AddHTTPServer(ctx, upstream, server) if err != nil { t.Errorf("Error adding upstream server: %v", err) } - stats, err := c.GetStats() + stats, err := c.GetStats(ctx) if err != nil { t.Errorf("Error getting stats: %v", err) } @@ -703,12 +776,13 @@ func TestStats(t *testing.T) { } // cleanup upstream servers - _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) + _, _, _, err = c.UpdateHTTPServers(ctx, upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %v", err) } } +//nolint:paralleltest func TestUpstreamServerDefaultParameters(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -733,11 +807,12 @@ func TestUpstreamServerDefaultParameters(t *testing.T) { Weight: &defaultWeight, Service: "", } - err = c.AddHTTPServer(upstream, server) + ctx := context.Background() + err = c.AddHTTPServer(ctx, upstream, server) if err != nil { t.Errorf("Error adding upstream server: %v", err) } - servers, err := c.GetHTTPServers(upstream) + servers, err := c.GetHTTPServers(ctx, upstream) if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) } @@ -752,12 +827,13 @@ func TestUpstreamServerDefaultParameters(t *testing.T) { } // remove upstream servers - _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) + _, _, _, err = c.UpdateHTTPServers(ctx, upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %v", err) } } +//nolint:paralleltest func TestStreamStats(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -767,13 +843,15 @@ func TestStreamStats(t *testing.T) { server := client.StreamUpstreamServer{ Server: "127.0.0.1:8080", } - err = c.AddStreamServer(streamUpstream, server) + ctx := context.Background() + err = c.AddStreamServer(ctx, streamUpstream, server) if err != nil { t.Errorf("Error adding stream upstream server: %v", err) } // make connection so we have stream server zone stats - ignore response - _, err = net.Dial("tcp", helpers.GetStreamAddress()) + d := &net.Dialer{} + _, err = d.DialContext(context.Background(), "tcp", helpers.GetStreamAddress()) if err != nil { t.Errorf("Error making tcp connection: %v", err) } @@ -781,7 +859,7 @@ func TestStreamStats(t *testing.T) { // wait for health checks time.Sleep(50 * time.Millisecond) - stats, err := c.GetStats() + stats, err := c.GetStats(ctx) if err != nil { t.Errorf("Error getting stats: %v", err) } @@ -829,12 +907,13 @@ func TestStreamStats(t *testing.T) { } // cleanup stream upstream servers - _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) + _, _, _, err = c.UpdateStreamServers(ctx, streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove stream servers: %v", err) } } +//nolint:paralleltest func TestStreamUpstreamServerDefaultParameters(t *testing.T) { c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { @@ -857,11 +936,12 @@ func TestStreamUpstreamServerDefaultParameters(t *testing.T) { Weight: &defaultWeight, Service: "", } - err = c.AddStreamServer(streamUpstream, streamServer) + ctx := context.Background() + err = c.AddStreamServer(ctx, streamUpstream, streamServer) if err != nil { t.Errorf("Error adding upstream server: %v", err) } - streamServers, err := c.GetStreamServers(streamUpstream) + streamServers, err := c.GetStreamServers(ctx, streamUpstream) if err != nil { t.Fatalf("Error getting stream servers: %v", err) } @@ -876,12 +956,13 @@ func TestStreamUpstreamServerDefaultParameters(t *testing.T) { } // cleanup stream upstream servers - _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) + _, _, _, err = c.UpdateStreamServers(ctx, streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove stream servers: %v", err) } } +//nolint:paralleltest func TestKeyValue(t *testing.T) { zoneName := "zone_one" c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) @@ -889,13 +970,14 @@ func TestKeyValue(t *testing.T) { t.Fatalf("Error connecting to nginx: %v", err) } - err = c.AddKeyValPair(zoneName, "key1", "val1") + ctx := context.Background() + err = c.AddKeyValPair(ctx, zoneName, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %v", err) } var keyValPairs client.KeyValPairs - keyValPairs, err = c.GetKeyValPairs(zoneName) + keyValPairs, err = c.GetKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) } @@ -906,7 +988,7 @@ func TestKeyValue(t *testing.T) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } - keyValuPairsByZone, err := c.GetAllKeyValPairs() + keyValuPairsByZone, err := c.GetAllKeyValPairs(ctx) if err != nil { t.Errorf("Couldn't get keyvals, %v", err) } @@ -919,12 +1001,12 @@ func TestKeyValue(t *testing.T) { // modify keyval expectedKeyValPairs["key1"] = "valModified1" - err = c.ModifyKeyValPair(zoneName, "key1", "valModified1") + err = c.ModifyKeyValPair(ctx, zoneName, "key1", "valModified1") if err != nil { t.Errorf("couldn't set keyval: %v", err) } - keyValPairs, err = c.GetKeyValPairs(zoneName) + keyValPairs, err = c.GetKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -933,17 +1015,17 @@ func TestKeyValue(t *testing.T) { } // error expected - err = c.AddKeyValPair(zoneName, "key1", "valModified1") + err = c.AddKeyValPair(ctx, zoneName, "key1", "valModified1") if err == nil { t.Errorf("adding same key/val should result in error") } - err = c.AddKeyValPair(zoneName, "key2", "val2") + err = c.AddKeyValPair(ctx, zoneName, "key2", "val2") if err != nil { t.Errorf("error adding another key/val pair: %v", err) } - err = c.DeleteKeyValuePair(zoneName, "key1") + err = c.DeleteKeyValuePair(ctx, zoneName, "key1") if err != nil { t.Errorf("error deleting key") } @@ -951,7 +1033,7 @@ func TestKeyValue(t *testing.T) { expectedKeyValPairs2 := client.KeyValPairs{ "key2": "val2", } - keyValPairs, err = c.GetKeyValPairs(zoneName) + keyValPairs, err = c.GetKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -959,12 +1041,12 @@ func TestKeyValue(t *testing.T) { t.Errorf("didn't delete key1 %+v", keyValPairs) } - err = c.DeleteKeyValPairs(zoneName) + err = c.DeleteKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't delete all: %v", err) } - keyValPairs, err = c.GetKeyValPairs(zoneName) + keyValPairs, err = c.GetKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -973,25 +1055,26 @@ func TestKeyValue(t *testing.T) { } // error expected - err = c.ModifyKeyValPair(zoneName, "key1", "val1") + err = c.ModifyKeyValPair(ctx, zoneName, "key1", "val1") if err == nil { t.Errorf("modifying nonexistent key/val should result in error") } } +//nolint:paralleltest func TestKeyValueStream(t *testing.T) { zoneName := "zone_one_stream" c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } - - err = c.AddStreamKeyValPair(zoneName, "key1", "val1") + ctx := context.Background() + err = c.AddStreamKeyValPair(ctx, zoneName, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %v", err) } - keyValPairs, err := c.GetStreamKeyValPairs(zoneName) + keyValPairs, err := c.GetStreamKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) } @@ -1002,7 +1085,7 @@ func TestKeyValueStream(t *testing.T) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } - keyValPairsByZone, err := c.GetAllStreamKeyValPairs() + keyValPairsByZone, err := c.GetAllStreamKeyValPairs(ctx) if err != nil { t.Errorf("Couldn't get keyvals, %v", err) } @@ -1016,12 +1099,12 @@ func TestKeyValueStream(t *testing.T) { // modify keyval expectedKeyValPairs["key1"] = "valModified1" - err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified1") + err = c.ModifyStreamKeyValPair(ctx, zoneName, "key1", "valModified1") if err != nil { t.Errorf("couldn't set keyval: %v", err) } - keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + keyValPairs, err = c.GetStreamKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -1030,22 +1113,22 @@ func TestKeyValueStream(t *testing.T) { } // error expected - err = c.AddStreamKeyValPair(zoneName, "key1", "valModified1") + err = c.AddStreamKeyValPair(ctx, zoneName, "key1", "valModified1") if err == nil { t.Errorf("adding same key/val should result in error") } - err = c.AddStreamKeyValPair(zoneName, "key2", "val2") + err = c.AddStreamKeyValPair(ctx, zoneName, "key2", "val2") if err != nil { t.Errorf("error adding another key/val pair: %v", err) } - err = c.DeleteStreamKeyValuePair(zoneName, "key1") + err = c.DeleteStreamKeyValuePair(ctx, zoneName, "key1") if err != nil { t.Errorf("error deleting key") } - keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + keyValPairs, err = c.GetStreamKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -1056,12 +1139,12 @@ func TestKeyValueStream(t *testing.T) { t.Errorf("didn't delete key1 %+v", keyValPairs) } - err = c.DeleteStreamKeyValPairs(zoneName) + err = c.DeleteStreamKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't delete all: %v", err) } - keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + keyValPairs, err = c.GetStreamKeyValPairs(ctx, zoneName) if err != nil { t.Errorf("couldn't get keyval: %v", err) } @@ -1070,13 +1153,14 @@ func TestKeyValueStream(t *testing.T) { } // error expected - err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified") + err = c.ModifyStreamKeyValPair(ctx, zoneName, "key1", "valModified") if err == nil { t.Errorf("modifying nonexistent key/val should result in error") } } func TestStreamZoneSync(t *testing.T) { + t.Parallel() c1, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) @@ -1086,8 +1170,8 @@ func TestStreamZoneSync(t *testing.T) { if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } - - err = c1.AddStreamKeyValPair(streamZoneSync, "key1", "val1") + ctx := context.Background() + err = c1.AddStreamKeyValPair(ctx, streamZoneSync, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %v", err) } @@ -1095,7 +1179,7 @@ func TestStreamZoneSync(t *testing.T) { // wait for nodes to sync information of synced zones time.Sleep(5 * time.Second) - statsC1, err := c1.GetStats() + statsC1, err := c1.GetStats(ctx) if err != nil { t.Errorf("Error getting stats: %v", err) } @@ -1135,7 +1219,7 @@ func TestStreamZoneSync(t *testing.T) { t.Errorf("Sync zone %v missing in stats", streamZoneSync) } - statsC2, err := c2.GetStats() + statsC2, err := c2.GetStats(ctx) if err != nil { t.Errorf("Error getting stats: %v", err) } @@ -1204,6 +1288,7 @@ func compareStreamUpstreamServers(x []client.StreamUpstreamServer, y []client.St } func TestUpstreamServerWithDrain(t *testing.T) { + t.Parallel() c, err := client.NewNginxClient(helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) @@ -1225,7 +1310,8 @@ func TestUpstreamServerWithDrain(t *testing.T) { } // Get existing upstream servers - servers, err := c.GetHTTPServers("test-drain") + ctx := context.Background() + servers, err := c.GetHTTPServers(ctx, "test-drain") if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) }