pax_global_header00006660000000000000000000000064141247140310014507gustar00rootroot0000000000000052 comment=3b97e596c602037152747421dad7dd7a95992cb1 nginx-plus-go-client-0.9.0/000077500000000000000000000000001412471403100155005ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/.github/000077500000000000000000000000001412471403100170405ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001412471403100212235ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010431412471403100237130ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior, such as: 1. Try adding upstream through the client 2. Returns a panic 3. Here is the stacktrace **Expected behavior** A clear and concise description of what you expected to happen. **Your environment** * Version of nginx-plus-go-client * Version of NGINX Plus * Version of the OS **Additional context** Add any other context about the problem here. nginx-plus-go-client-0.9.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000010411412471403100247440ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the feature request here. nginx-plus-go-client-0.9.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000013661412471403100226470ustar00rootroot00000000000000### Proposed changes Describe the use case and detail of the change. If this PR addresses an issue on GitHub, make sure to include a link to that issue here in this description (not in the title of the PR). ### Checklist 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/master/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 - [ ] I have rebased my branch onto master - [ ] I will ensure my PR is targeting the master branch and pulling from my branch from my own fork nginx-plus-go-client-0.9.0/.github/dependabot.yml000066400000000000000000000004341412471403100216710ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily reviewers: - "nginxinc/kic" - package-ecosystem: "docker" directory: "/docker" schedule: interval: daily reviewers: - "nginxinc/kic" nginx-plus-go-client-0.9.0/.github/release-drafter.yml000066400000000000000000000023771412471403100226410ustar00rootroot00000000000000name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - title: '🚀 Features' labels: - "enhancement" - title: '💣 Breaking Change' labels: - "change" - title: '🐛 Bug Fixes' labels: - "bug" - title: '📝 Documentation' labels: - "documentation" - title: '🔨 Maintenance' labels: - "chore" - title: '⬆️ Dependencies' labels: - "dependencies" version-resolver: major: labels: - 'change' minor: labels: - 'enhancement' patch: labels: - 'bug' - 'chore' - 'dependencies' - 'documentation' default: patch exclude-labels: - 'skip-changelog' autolabeler: - label: 'documentation' files: - '*.md' branch: - '/docs{0,1}\/.+/' - label: 'chore' branch: - '/chore\/.+/' - label: 'bug' branch: - '/fix\/.+/' title: - '/fix/i' - label: 'enhancement' branch: - '/enh\/.+/' - '/enhancement\/.+/' - '/feat\/.+/' - '/feature\/.+/' title: - '/feat/i' - label: 'dependencies' files: - 'go.mod' - 'go.sum' branch: - '/deps\/.+/' template: | ## New in NGINX Plus Go Client v$RESOLVED_VERSION $CHANGES nginx-plus-go-client-0.9.0/.github/workflows/000077500000000000000000000000001412471403100210755ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/.github/workflows/ci.yml000066400000000000000000000077571412471403100222330ustar00rootroot00000000000000name: Continuous Integration on: push: branches: - master paths-ignore: - '**.md' - 'LICENSE' pull_request: branches: - master paths-ignore: - '**.md' - 'LICENSE' schedule: - cron: '0 5 * * *' env: DOCKER_BUILDKIT: 1 DOCKER_NETWORK: ${{ github.run_id }} DOCKER_NETWORK_ALIAS: nginx-plus-test DOCKER_NGINX_PLUS: nginx-plus-${{ github.run_id }} DOCKER_NGINX_PLUS_HELPER: nginx-plus-helper-${{ github.run_id }} jobs: build: name: Build Client runs-on: ubuntu-20.04 if: github.event.pull_request.head.repo.full_name == 'nginxinc/nginx-plus-go-client' || github.event_name == 'push' || github.event_name == 'schedule' steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Determine NGINX Plus version run: echo "NGINX_PLUS_VERSION=$(cat Makefile | grep -m1 NGINX_PLUS_VERSION | cut -d "=" -f2)" >> $GITHUB_ENV - name: Switch Repository (Nightly) if: (github.event_name == 'schedule') run: | sed -i 's|pkgs.nginx.com|pkgs-test.nginx.com|g' docker/Dockerfile sed -i '16d' docker/Dockerfile sed -i "17i && sed -i 's|pkgs|pkgs-test|g' /etc/apt/apt.conf.d/90pkgs-nginx \\\ " docker/Dockerfile sed -i 's|deb https|deb [trusted=yes] https|g' docker/Dockerfile sed -i 's|nginx-plus-\${{ env.NGINX_PLUS_VERSION }}|nginx-plus|g' docker/Dockerfile - name: Build Plus Docker Image uses: docker/build-push-action@v2 with: file: docker/Dockerfile context: 'docker' tags: nginx-plus:${{ env.NGINX_PLUS_VERSION }} load: true secrets: | "nginx-repo.crt=${{ secrets.NGINX_CRT }}" "nginx-repo.key=${{ secrets.NGINX_KEY }}" build-args: NGINX_PLUS_VERSION=${{ env.NGINX_PLUS_VERSION }} - name: Test Client if: (github.event_name != 'schedule') run: make test - name: Test Client (Nightly) if: (github.event_name == 'schedule') run: make test env: NGINX_PLUS_VERSION: nightly notify: name: Notify runs-on: ubuntu-20.04 needs: build if: always() && github.ref == 'refs/heads/master' steps: - name: Workflow Status id: check uses: martialonline/workflow-status@v2 - name: Output Variables id: commit run: | echo "::set-output name=sha::$(echo ${GITHUB_SHA} | cut -c1-7)" echo "::set-output name=repo::$(echo ${GITHUB_REPOSITORY} | cut -d '/' -f 2)" - name: Send Notification uses: 8398a7/action-slack@v3 if: steps.check.outputs.status == 'failure' || steps.check.outputs.status == 'cancelled' with: status: custom custom_payload: | { username: 'Github', icon_emoji: ':octocat:', mention: 'channel', attachments: [{ title: '[${{ steps.commit.outputs.repo }}] ${{ github.workflow }} pipeline has failed', color: '${{ steps.check.outputs.status }}' == 'failure' ? 'danger' : 'warning', fields: [{ title: 'Commit Hash', value: '${{ steps.commit.outputs.sha }}', short: true }, { title: 'Author', value: '${{ github.actor }}', short: true }, { title: 'Commit Message', value: `${{ github.event.head_commit.message }}`, short: false }, { title: 'Pipeline URL', value: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}', short: false }] }] } env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} nginx-plus-go-client-0.9.0/.github/workflows/codeql-analysis.yml000066400000000000000000000044651412471403100247210ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '33 16 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # 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. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 nginx-plus-go-client-0.9.0/.github/workflows/fossa.yml000066400000000000000000000036031412471403100227350ustar00rootroot00000000000000name: Fossa on: push: branches: - master paths-ignore: - '**.md' jobs: scan: name: Fossa runs-on: ubuntu-20.04 steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Scan uses: fossas/fossa-action@v1 with: api-key: ${{ secrets.FOSSA_TOKEN }} notify: name: Notify runs-on: ubuntu-20.04 needs: scan if: always() steps: - name: Workflow Status id: check uses: martialonline/workflow-status@v2 - name: Output Variables id: commit run: | echo "::set-output name=sha::$(echo ${GITHUB_SHA} | cut -c1-7)" echo "::set-output name=repo::$(echo ${GITHUB_REPOSITORY} | cut -d '/' -f 2)" - name: Send Notification uses: 8398a7/action-slack@v3 if: steps.check.outputs.status == 'failure' with: status: custom custom_payload: | { username: 'Fossa Scan', icon_emoji: ':fossa:', mention: 'channel', attachments: [{ title: '${{ steps.commit.outputs.repo }} ${{ github.workflow }} license scan has failed', color: 'danger', fields: [{ title: 'Commit Hash', value: '${{ steps.commit.outputs.sha }}', short: true }, { title: 'Author', value: '${{ github.actor }}', short: true }, { title: 'Job URL', value: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}', short: false }] }] } env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} nginx-plus-go-client-0.9.0/.github/workflows/lint.yml000066400000000000000000000010051412471403100225620ustar00rootroot00000000000000name: Lint on: pull_request: branches: - master paths-ignore: - '**.md' - 'LICENSE' types: - opened - reopened - synchronize defaults: run: shell: bash env: GOLANGCI_TIMEOUT: 10m0s jobs: lint: name: Lint runs-on: ubuntu-20.04 steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Lint Code uses: golangci/golangci-lint-action@v2 with: args: --timeout ${{ env.GOLANGCI_TIMEOUT }} nginx-plus-go-client-0.9.0/.github/workflows/release-drafter.yml000066400000000000000000000004611412471403100246660ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master pull_request: types: [opened, reopened, synchronize] jobs: update_release_draft: runs-on: ubuntu-20.04 steps: - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} nginx-plus-go-client-0.9.0/.github/workflows/stale.yml000066400000000000000000000016251412471403100227340ustar00rootroot00000000000000name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' jobs: stale: runs-on: ubuntu-20.04 steps: - uses: actions/stale@v4 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.' stale-pr-message: 'This PR 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.' close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' 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-pr-assignees: true operations-per-run: 100 nginx-plus-go-client-0.9.0/.gitignore000066400000000000000000000001151412471403100174650ustar00rootroot00000000000000# NGINX Plus license files *.crt *.key # Visual Studio Code settings .vscodenginx-plus-go-client-0.9.0/.golangci.yml000066400000000000000000000023551412471403100200710ustar00rootroot00000000000000linters-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: if-return - 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 linters: enable: - asciicheck - deadcode - errcheck - errorlint - gofmt - gofumpt - goimports - gosec - gosimple - govet - ineffassign - makezero - misspell - nilerr - noctx - predeclared - revive - staticcheck - structcheck - typecheck - unconvert - unparam - unused - varcheck - wastedassign disable-all: true issues: max-issues-per-linter: 0 max-same-issues: 0 nginx-plus-go-client-0.9.0/CHANGELOG.md000066400000000000000000000077321412471403100173220ustar00rootroot00000000000000## 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 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 supports retrieving processes info. CHANGES: * The version of NGINX Plus for e2e testing was changed to R22. * The version of Go was changed to 1.14 ## 0.6.0 (Nov 8, 2019) FEATURES: * [34](https://github.com/nginxinc/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: * Public methods `UpdateHTTPServers` and `UpdateStreamServers` now return a third slice that includes the updated servers -- i.e. the servers that were already present in NGINX Plus but were updated with different parameters. * Client will assume port `80` in addresses of updated servers of `UpdateHTTPServers` and `UpdateStreamServers` if port is not explicitly set. * The version of Go was changed to 1.13 ## 0.5.0 (Sep 25, 2019) 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*. BUGFIXES: * [29](https://github.com/nginxinc/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`. CHANGES: * The version of NGINX Plus for e2e testing was changed to R19. * The version of the API was changed to 5. ## 0.4.0 (July 17, 2019) FEATURES: * [24](https://github.com/nginxinc/nginx-plus-go-client/pull/24): *Support `MaxConns` in upstream servers*. BUGFIXES: * [25](https://github.com/nginxinc/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` 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 `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 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` 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"`). * The version of the API was changed to 4. * The version of NGINX Plus for e2e testing was changed to R18. ## 0.2 (Sep 7, 2018) 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. CHANGES: * The version of NGINX Plus for e2e testing was changed to R16. ## 0.1 (July 30, 2018) Initial release nginx-plus-go-client-0.9.0/CODE_OF_CONDUCT.md000066400000000000000000000063511412471403100203040ustar00rootroot00000000000000# Code of Conduct This project and everyone participating in it is governed by this code. ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [mailto:nginx@nginx.org]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org nginx-plus-go-client-0.9.0/CONTRIBUTING.md000066400000000000000000000054131412471403100177340ustar00rootroot00000000000000# Contributing Guidelines 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](https://github.com/nginxinc/nginx-plus-go-client/blob/master/CODE_OF_CONDUCT.md) ## Ask a Question We will have a public forum soon where you can come and ask questions and have a discussion. For now please open an Issue on GitHub with the label `question`. ## Getting Started Read the usage and testing steps in the [README](README.md). ## Contributing ### Report a Bug To report a bug, open an issue on GitHub with the label `bug` using the available bug report issue template. Please ensure the issue has not already been reported. ### Suggest an Enhancement To suggest an enhancement, please create an issue on GitHub with the label `enhancement` using the available feature issue template. ### Open a Pull Request * Fork the repo, create a branch, submit a PR when your changes are tested and ready for review * You will be asked to fill in [our pull request template](.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md) Note: if you’d like to implement a new feature, please consider creating a feature request issue first to start a discussion about the feature. ## Style Guides ### Git Style Guide * Keep a clean, concise and meaningful git commit history on your branch, rebasing locally and squashing before submitting a PR * Follow the guidelines of writing a good commit message as described here https://chris.beams.io/posts/git-commit/ and summarised in the next few points * In the subject line, use the present tense ("Add feature" not "Added feature") * In the subject line, use the imperative mood ("Move cursor to..." not "Moves cursor to...") * Limit the subject line to 72 characters or less * Reference issues and pull requests liberally after the subject line * Add more detailed description in the body of the git message (`git commit -a` to give you more space and time in your text editor to write a good message instead of `git commit -am`) ### Go Style Guide * 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 - https://github.com/golang/go/wiki/CodeReviewComments * To check for extra issues, install [golangci-lint](https://github.com/golangci/golangci-lint) and run `make lint` or `golangci-lint run`nginx-plus-go-client-0.9.0/LICENSE000066400000000000000000000237771412471403100165250ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2018 Nginx, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. nginx-plus-go-client-0.9.0/Makefile000066400000000000000000000047241412471403100171470ustar00rootroot00000000000000NGINX_PLUS_VERSION=r25 DOCKER_NETWORK?=test DOCKER_NETWORK_ALIAS=nginx-plus-test DOCKER_NGINX_PLUS?=nginx-plus DOCKER_NGINX_PLUS_HELPER?=nginx-plus-helper GOLANG_CONTAINER=golang:1.17 export TEST_API_ENDPOINT=http://$(DOCKER_NGINX_PLUS):8080/api export TEST_API_ENDPOINT_OF_HELPER=http://$(DOCKER_NGINX_PLUS_HELPER):8080/api export TEST_UNAVAILABLE_STREAM_ADDRESS=$(DOCKER_NGINX_PLUS):8081 test: run-nginx-plus test-run configure-no-stream-block test-run-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 docker-build: docker build --secret id=nginx-repo.crt,src=docker/nginx-repo.crt --secret id=nginx-repo.key,src=docker/nginx-repo.key --build-arg NGINX_PLUS_VERSION=$(NGINX_PLUS_VERSION) -t nginx-plus:$(NGINX_PLUS_VERSION) docker run-nginx-plus: docker network create --driver bridge $(DOCKER_NETWORK) docker run --network=$(DOCKER_NETWORK) -d --name $(DOCKER_NGINX_PLUS) --network-alias=$(DOCKER_NETWORK_ALIAS) --rm -p 8080:8080 -p 8081:8081 nginx-plus:$(NGINX_PLUS_VERSION) docker run --network=$(DOCKER_NETWORK) -d --name $(DOCKER_NGINX_PLUS_HELPER) --network-alias=$(DOCKER_NETWORK_ALIAS) --rm -p 8090:8080 -p 8091:8081 nginx-plus:$(NGINX_PLUS_VERSION) test-run: docker run --rm \ --network=$(DOCKER_NETWORK) \ -e TEST_API_ENDPOINT \ -e TEST_API_ENDPOINT_OF_HELPER \ -e TEST_UNAVAILABLE_STREAM_ADDRESS \ -v $(shell pwd):/go/src/github.com/nginxinc/nginx-plus-go-client \ -w /go/src/github.com/nginxinc/nginx-plus-go-client \ $(GOLANG_CONTAINER) /bin/sh -c "go test client/*; go clean -testcache; go test tests/client_test.go" configure-no-stream-block: docker cp docker/nginx_no_stream.conf $(DOCKER_NGINX_PLUS):/etc/nginx/nginx.conf docker exec $(DOCKER_NGINX_PLUS) nginx -s reload test-run-no-stream-block: configure-no-stream-block docker run --rm \ --network=$(DOCKER_NETWORK) \ -e TEST_API_ENDPOINT \ -e TEST_API_ENDPOINT_OF_HELPER \ -e TEST_UNAVAILABLE_STREAM_ADDRESS \ -v $(shell pwd):/go/src/github.com/nginxinc/nginx-plus-go-client \ -w /go/src/github.com/nginxinc/nginx-plus-go-client \ $(GOLANG_CONTAINER) /bin/sh -c "go clean -testcache; go test tests/client_no_stream_test.go" clean: -docker kill $(DOCKER_NGINX_PLUS) -docker kill $(DOCKER_NGINX_PLUS_HELPER) -docker network rm $(DOCKER_NETWORK) nginx-plus-go-client-0.9.0/README.md000066400000000000000000000035631412471403100167660ustar00rootroot00000000000000 [![Continuous Integration](https://github.com/nginxinc/nginx-plus-go-client/workflows/Continuous%20Integration/badge.svg)](https://github.com/nginxinc/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) # NGINX Plus Go Client This project includes a client library for working with NGINX Plus API. ## About the Client `client/nginx.go` includes functions and data structures for working with NGINX Plus API as well as some helper functions. ## Compatibility This Client works against version 5 of NGINX Plus API. Version 5 was introduced in NGINX Plus R19. ## Using the Client 1. Import `github.com/nginxinc/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 ### Unit tests ``` $ cd client $ go test ``` ### Integration tests Prerequisites: * Docker * golang * Make * NGINX Plus license - put `nginx-repo.crt` and `nginx-repo.key` into the `docker` folder. Run Tests: ``` $ make docker-build && make test ``` This will build and run two NGINX Plus containers and create one docker network of type bridge, execute the client tests against both NGINX Plus APIs, and then clean up. If it fails and you want to clean up (i.e. stop the running containers and remove the docker network), please use `$ make clean` ## Support This project is not covered by the NGINX Plus support contract. nginx-plus-go-client-0.9.0/client/000077500000000000000000000000001412471403100167565ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/client/nginx.go000066400000000000000000001242171412471403100204370ustar00rootroot00000000000000package client import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "reflect" "strings" "time" ) const ( // APIVersion is the default version of NGINX Plus API supported by the client. APIVersion = 5 pathNotFoundCode = "PathNotFound" streamContext = true httpContext = false defaultServerPort = "80" ) var ( supportedAPIVersions = versions{4, 5} // Default values for servers in Upstreams. defaultMaxConns = 0 defaultMaxFails = 1 defaultFailTimeout = "10s" defaultSlowStart = "0s" defaultBackup = false defaultDown = false 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") // NginxClient lets you access NGINX Plus API. type NginxClient struct { version int apiEndpoint string httpClient *http.Client } type versions []int // UpstreamServer lets you configure HTTP upstreams. type UpstreamServer struct { ID int `json:"id,omitempty"` Server string `json:"server"` MaxConns *int `json:"max_conns,omitempty"` MaxFails *int `json:"max_fails,omitempty"` FailTimeout string `json:"fail_timeout,omitempty"` SlowStart string `json:"slow_start,omitempty"` Route string `json:"route,omitempty"` Backup *bool `json:"backup,omitempty"` Down *bool `json:"down,omitempty"` Drain bool `json:"drain,omitempty"` Weight *int `json:"weight,omitempty"` Service string `json:"service,omitempty"` } // StreamUpstreamServer lets you configure Stream upstreams. type StreamUpstreamServer struct { ID int `json:"id,omitempty"` Server string `json:"server"` MaxConns *int `json:"max_conns,omitempty"` MaxFails *int `json:"max_fails,omitempty"` FailTimeout string `json:"fail_timeout,omitempty"` SlowStart string `json:"slow_start,omitempty"` Backup *bool `json:"backup,omitempty"` Down *bool `json:"down,omitempty"` Weight *int `json:"weight,omitempty"` Service string `json:"service,omitempty"` } type apiErrorResponse struct { Error apiError RequestID string `json:"request_id"` Href string } func (resp *apiErrorResponse) toString() string { return fmt.Sprintf("error.status=%v; error.text=%v; error.code=%v; request_id=%v; href=%v", resp.Error.Status, resp.Error.Text, resp.Error.Code, resp.RequestID, resp.Href) } type apiError struct { Status int Text string Code string } type internalError struct { apiError err string } // Error allows internalError to match the Error interface. func (internalError *internalError) Error() string { return internalError.err } // Wrap is a way of including current context while preserving previous error information, // similar to `return fmt.Errorf("error doing foo, err: %v", err)` but for our internalError type. func (internalError *internalError) Wrap(err string) *internalError { internalError.err = fmt.Sprintf("%v. %v", err, internalError.err) return internalError } // 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 { NginxInfo NginxInfo Caches Caches Processes Processes Connections Connections Slabs Slabs HTTPRequests HTTPRequests SSL SSL ServerZones ServerZones Upstreams Upstreams StreamServerZones StreamServerZones StreamUpstreams StreamUpstreams StreamZoneSync *StreamZoneSync LocationZones LocationZones Resolvers Resolvers } // NginxInfo contains general information about NGINX Plus. type NginxInfo struct { Version string Build string Address string Generation uint64 LoadTimestamp string `json:"load_timestamp"` Timestamp string ProcessID uint64 `json:"pid"` ParentProcessID uint64 `json:"ppid"` } // Caches is a map of cache stats by cache zone type Caches = map[string]HTTPCache // HTTPCache represents a zone's HTTP Cache type HTTPCache struct { Size uint64 MaxSize uint64 `json:"max_size"` Cold bool Hit CacheStats Stale CacheStats Updating CacheStats Revalidated CacheStats Miss CacheStats Expired ExtendedCacheStats Bypass ExtendedCacheStats } // CacheStats are basic cache stats. type CacheStats struct { Responses uint64 Bytes uint64 } // ExtendedCacheStats are extended cache stats. type ExtendedCacheStats struct { CacheStats ResponsesWritten uint64 `json:"responses_written"` BytesWritten uint64 `json:"bytes_written"` } // Connections represents connection related stats. type Connections struct { Accepted uint64 Dropped uint64 Active uint64 Idle uint64 } // Slabs is map of slab stats by zone name. type Slabs map[string]Slab // Slab represents slab related stats. type Slab struct { Pages Pages Slots Slots } // Pages represents the slab memory usage stats. type Pages struct { Used uint64 Free uint64 } // Slots is a map of slots by slot size type Slots map[string]Slot // Slot represents slot related stats. type Slot struct { Used uint64 Free uint64 Reqs uint64 Fails uint64 } // HTTPRequests represents HTTP request related stats. type HTTPRequests struct { Total uint64 Current uint64 } // SSL represents SSL related stats. type SSL struct { Handshakes uint64 HandshakesFailed uint64 `json:"handshakes_failed"` SessionReuses uint64 `json:"session_reuses"` } // ServerZones is map of server zone stats by zone name type ServerZones map[string]ServerZone // ServerZone represents server zone related stats. type ServerZone struct { Processing uint64 Requests uint64 Responses Responses Discarded uint64 Received uint64 Sent uint64 } // StreamServerZones is map of stream server zone stats by zone name. type StreamServerZones map[string]StreamServerZone // StreamServerZone represents stream server zone related stats. type StreamServerZone struct { Processing uint64 Connections uint64 Sessions Sessions Discarded uint64 Received uint64 Sent uint64 } // StreamZoneSync represents the sync information per each shared memory zone and the sync information per node in a cluster type StreamZoneSync struct { Zones map[string]SyncZone Status StreamZoneSyncStatus } // SyncZone represents the synchronization status of a shared memory zone type SyncZone struct { RecordsPending uint64 `json:"records_pending"` RecordsTotal uint64 `json:"records_total"` } // StreamZoneSyncStatus represents the status of a shared memory zone type StreamZoneSyncStatus struct { BytesIn uint64 `json:"bytes_in"` MsgsIn uint64 `json:"msgs_in"` MsgsOut uint64 `json:"msgs_out"` BytesOut uint64 `json:"bytes_out"` NodesOnline uint64 `json:"nodes_online"` } // Responses represents HTTP response related stats. type Responses struct { Responses1xx uint64 `json:"1xx"` Responses2xx uint64 `json:"2xx"` Responses3xx uint64 `json:"3xx"` Responses4xx uint64 `json:"4xx"` Responses5xx uint64 `json:"5xx"` Total uint64 } // Sessions represents stream session related stats. type Sessions struct { Sessions2xx uint64 `json:"2xx"` Sessions4xx uint64 `json:"4xx"` Sessions5xx uint64 `json:"5xx"` Total uint64 } // Upstreams is a map of upstream stats by upstream name. type Upstreams map[string]Upstream // Upstream represents upstream related stats. type Upstream struct { Peers []Peer Keepalives int Zombies int Zone string Queue Queue } // StreamUpstreams is a map of stream upstream stats by upstream name. type StreamUpstreams map[string]StreamUpstream // StreamUpstream represents stream upstream related stats. type StreamUpstream struct { Peers []StreamPeer Zombies int Zone string } // Queue represents queue related stats for an upstream. type Queue struct { Size int MaxSize int `json:"max_size"` Overflows uint64 } // Peer represents peer (upstream server) related stats. type Peer struct { ID int Server string Service string Name string Backup bool Weight int State string Active uint64 MaxConns int `json:"max_conns"` Requests uint64 Responses Responses Sent uint64 Received uint64 Fails uint64 Unavail uint64 HealthChecks HealthChecks `json:"health_checks"` Downtime uint64 Downstart string Selected string HeaderTime uint64 `json:"header_time"` ResponseTime uint64 `json:"response_time"` } // StreamPeer represents peer (stream upstream server) related stats. type StreamPeer struct { ID int Server string Service string Name string Backup bool Weight int State string Active uint64 MaxConns int `json:"max_conns"` Connections uint64 ConnectTime int `json:"connect_time"` FirstByteTime int `json:"first_byte_time"` ResponseTime uint64 `json:"response_time"` Sent uint64 Received uint64 Fails uint64 Unavail uint64 HealthChecks HealthChecks `json:"health_checks"` Downtime uint64 Downstart string Selected string } // HealthChecks represents health check related stats for a peer. type HealthChecks struct { Checks uint64 Fails uint64 Unhealthy uint64 LastPassed bool `json:"last_passed"` } // LocationZones represents location_zones related stats type LocationZones map[string]LocationZone // Resolvers represents resolvers related stats type Resolvers map[string]Resolver // LocationZone represents location_zones related stats type LocationZone struct { Requests int64 Responses Responses Discarded int64 Received int64 Sent int64 } // Resolver represents resolvers related stats type Resolver struct { Requests ResolverRequests `json:"requests"` Responses ResolverResponses `json:"responses"` } // ResolverRequests represents resolver requests type ResolverRequests struct { Name int64 Srv int64 Addr int64 } // ResolverResponses represents resolver responses type ResolverResponses struct { Noerror int64 Formerr int64 Servfail int64 Nxdomain int64 Notimp int64 Refused int64 Timedout int64 Unknown int64 } // Processes represents processes related stats type Processes struct { Respawned int64 } // NewNginxClient creates an NginxClient with the latest supported version. func NewNginxClient(httpClient *http.Client, apiEndpoint string) (*NginxClient, error) { return NewNginxClientWithVersion(httpClient, apiEndpoint, APIVersion) } // NewNginxClientWithVersion creates an NginxClient with the given version of NGINX Plus API. func NewNginxClientWithVersion(httpClient *http.Client, apiEndpoint string, version int) (*NginxClient, error) { if !versionSupported(version) { return nil, fmt.Errorf("API version %v is not supported by the client", version) } versions, err := getAPIVersions(httpClient, apiEndpoint) if err != nil { return nil, fmt.Errorf("error accessing the API: %w", err) } found := false for _, v := range *versions { if v == version { found = true break } } if !found { return nil, ErrUnsupportedVer } return &NginxClient{ apiEndpoint: apiEndpoint, httpClient: httpClient, version: version, }, nil } func versionSupported(n int) bool { for _, version := range supportedAPIVersions { if n == version { return true } } return false } func getAPIVersions(httpClient *http.Client, endpoint string) (*versions, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create a get request: %w", err) } resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("%v is not accessible: %w", endpoint, err) } 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) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error while reading body of the response: %w", err) } var vers versions err = json.Unmarshal(body, &vers) if err != nil { return nil, fmt.Errorf("error unmarshalling versions, got %q response: %w", string(body), err) } return &vers, nil } func createResponseMismatchError(respBody io.ReadCloser) *internalError { apiErrResp, err := readAPIErrorResponse(respBody) if err != nil { return &internalError{ err: fmt.Sprintf("failed to read the response body: %v", err), } } return &internalError{ err: apiErrResp.toString(), apiError: apiErrResp.Error, } } func readAPIErrorResponse(respBody io.ReadCloser) (*apiErrorResponse, error) { body, err := ioutil.ReadAll(respBody) if err != nil { return nil, fmt.Errorf("failed to read the response body: %w", err) } var apiErr apiErrorResponse err = json.Unmarshal(body, &apiErr) if err != nil { return nil, fmt.Errorf("error unmarshalling apiErrorResponse: got %q response: %w", string(body), err) } return &apiErr, nil } // 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) return err } // GetHTTPServers returns the servers of the upstream from NGINX. func (client *NginxClient) GetHTTPServers(upstream string) ([]UpstreamServer, error) { path := fmt.Sprintf("http/upstreams/%v/servers", upstream) var servers []UpstreamServer err := client.get(path, &servers) if err != nil { return nil, fmt.Errorf("failed to get the HTTP servers of upstream %v: %w", upstream, err) } return servers, nil } // AddHTTPServer adds the server to the upstream. func (client *NginxClient) AddHTTPServer(upstream string, server UpstreamServer) error { id, err := client.getIDOfHTTPServer(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) } path := fmt.Sprintf("http/upstreams/%v/servers/", upstream) err = client.post(path, &server) if err != nil { return fmt.Errorf("failed to add %v server to %v upstream: %w", server.Server, upstream, err) } return nil } // DeleteHTTPServer the server from the upstream. func (client *NginxClient) DeleteHTTPServer(upstream string, server string) error { id, err := client.getIDOfHTTPServer(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) } path := fmt.Sprintf("http/upstreams/%v/servers/%v", upstream, id) err = client.delete(path, http.StatusOK) if err != nil { return fmt.Errorf("failed to remove %v server from %v upstream: %w", server, upstream, err) } return nil } // UpdateHTTPServers updates the servers of the upstream. // 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) if err != nil { return nil, nil, nil, fmt.Errorf("failed to update servers of %v upstream: %w", upstream, err) } // We assume port 80 if no port is set for servers. var formattedServers []UpstreamServer for _, server := range servers { server.Server = addPortToServer(server.Server) formattedServers = append(formattedServers, server) } 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) } } 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) } } 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) } } return toAdd, toDelete, toUpdate, nil } // 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 if serverNGX.MaxConns != nil && newServer.MaxConns == nil { newServer.MaxConns = &defaultMaxConns } if serverNGX.MaxFails != nil && newServer.MaxFails == nil { newServer.MaxFails = &defaultMaxFails } if serverNGX.FailTimeout != "" && newServer.FailTimeout == "" { newServer.FailTimeout = defaultFailTimeout } if serverNGX.SlowStart != "" && newServer.SlowStart == "" { newServer.SlowStart = defaultSlowStart } if serverNGX.Backup != nil && newServer.Backup == nil { newServer.Backup = &defaultBackup } if serverNGX.Down != nil && newServer.Down == nil { newServer.Down = &defaultDown } if serverNGX.Weight != nil && newServer.Weight == nil { newServer.Weight = &defaultWeight } return reflect.DeepEqual(newServer, serverNGX) } 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) { server.ID = serverNGX.ID updateFound = true break } } if updateFound { toUpdate = append(toUpdate, server) } } for _, server := range updatedServers { found := false for _, serverNGX := range nginxServers { if server.Server == serverNGX.Server { found = true break } } if !found { toAdd = append(toAdd, server) } } for _, serverNGX := range nginxServers { found := false for _, server := range updatedServers { if serverNGX.Server == server.Server { found = true break } } if !found { toRemove = append(toRemove, serverNGX) } } return } func (client *NginxClient) getIDOfHTTPServer(upstream string, name string) (int, error) { servers, err := client.GetHTTPServers(upstream) if err != nil { return -1, fmt.Errorf("error getting id of server %v of upstream %v: %w", name, upstream, err) } for _, s := range servers { if s.Server == name { return s.ID, nil } } return -1, nil } func (client *NginxClient) get(path string, data interface{}) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, client.version, path) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("failed to create a get request: %w", err) } resp, err := client.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to get %v: %w", path, err) } if resp.StatusCode != http.StatusOK { return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( "expected %v response, got %v", http.StatusOK, resp.StatusCode)) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read the response body: %w", err) } err = json.Unmarshal(body, data) if err != nil { return fmt.Errorf("error unmarshaling response %q: %w", string(body), err) } return nil } func (client *NginxClient) post(path string, input interface{}) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, client.version, path) jsonInput, err := json.Marshal(input) if err != nil { return fmt.Errorf("failed to marshall input: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonInput)) if err != nil { return fmt.Errorf("failed to create a post request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := client.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to post %v: %w", path, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( "expected %v response, got %v", http.StatusCreated, resp.StatusCode)) } return nil } func (client *NginxClient) delete(path string, expectedStatusCode int) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() path = fmt.Sprintf("%v/%v/%v/", client.apiEndpoint, client.version, path) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil) if err != nil { return fmt.Errorf("failed to create a delete request: %w", err) } resp, err := client.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to create delete request: %w", err) } defer resp.Body.Close() if resp.StatusCode != expectedStatusCode { return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( "failed to complete delete request: expected %v response, got %v", expectedStatusCode, resp.StatusCode)) } return nil } func (client *NginxClient) patch(path string, input interface{}, expectedStatusCode int) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() path = fmt.Sprintf("%v/%v/%v/", client.apiEndpoint, client.version, path) jsonInput, err := json.Marshal(input) if err != nil { return fmt.Errorf("failed to marshall input: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPatch, path, bytes.NewBuffer(jsonInput)) if err != nil { return fmt.Errorf("failed to create a patch request: %w", err) } resp, err := client.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to create patch request: %w", err) } defer resp.Body.Close() if resp.StatusCode != expectedStatusCode { return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( "failed to complete patch request: expected %v response, got %v", expectedStatusCode, resp.StatusCode)) } return nil } // 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) return err } // GetStreamServers returns the stream servers of the upstream from NGINX. func (client *NginxClient) GetStreamServers(upstream string) ([]StreamUpstreamServer, error) { path := fmt.Sprintf("stream/upstreams/%v/servers", upstream) var servers []StreamUpstreamServer err := client.get(path, &servers) if err != nil { return nil, fmt.Errorf("failed to get stream servers of upstream server %v: %w", upstream, err) } return servers, nil } // AddStreamServer adds the stream server to the upstream. func (client *NginxClient) AddStreamServer(upstream string, server StreamUpstreamServer) error { id, err := client.getIDOfStreamServer(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) } path := fmt.Sprintf("stream/upstreams/%v/servers/", upstream) err = client.post(path, &server) if err != nil { return fmt.Errorf("failed to add %v stream server to %v upstream: %w", server.Server, upstream, err) } return nil } // DeleteStreamServer the server from the upstream. func (client *NginxClient) DeleteStreamServer(upstream string, server string) error { id, err := client.getIDOfStreamServer(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) } path := fmt.Sprintf("stream/upstreams/%v/servers/%v", upstream, id) err = client.delete(path, http.StatusOK) if err != nil { return fmt.Errorf("failed to remove %v stream server from %v upstream: %w", server, upstream, err) } return nil } // UpdateStreamServers updates the servers of the upstream. // 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) if err != nil { return nil, nil, nil, fmt.Errorf("failed to update stream servers of %v upstream: %w", upstream, err) } var formattedServers []StreamUpstreamServer for _, server := range servers { server.Server = addPortToServer(server.Server) formattedServers = append(formattedServers, server) } 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) } } 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) } } 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) } } return toAdd, toDelete, toUpdate, nil } func (client *NginxClient) getIDOfStreamServer(upstream string, name string) (int, error) { servers, err := client.GetStreamServers(upstream) if err != nil { return -1, fmt.Errorf("error getting id of stream server %v of upstream %v: %w", name, upstream, err) } for _, s := range servers { if s.Server == name { return s.ID, nil } } 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 } if serverNGX.MaxFails != nil && newServer.MaxFails == nil { newServer.MaxFails = &defaultMaxFails } if serverNGX.FailTimeout != "" && newServer.FailTimeout == "" { newServer.FailTimeout = defaultFailTimeout } if serverNGX.SlowStart != "" && newServer.SlowStart == "" { newServer.SlowStart = defaultSlowStart } if serverNGX.Backup != nil && newServer.Backup == nil { newServer.Backup = &defaultBackup } if serverNGX.Down != nil && newServer.Down == nil { newServer.Down = &defaultDown } if serverNGX.Weight != nil && newServer.Weight == nil { newServer.Weight = &defaultWeight } return reflect.DeepEqual(newServer, serverNGX) } 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) { server.ID = serverNGX.ID updateFound = true break } } if updateFound { toUpdate = append(toUpdate, server) } } for _, server := range updatedServers { found := false for _, serverNGX := range nginxServers { if server.Server == serverNGX.Server { found = true break } } if !found { toAdd = append(toAdd, server) } } for _, serverNGX := range nginxServers { found := false for _, server := range updatedServers { if serverNGX.Server == server.Server { found = true break } } if !found { toRemove = append(toRemove, serverNGX) } } return } // 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) { info, err := client.GetNginxInfo() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } caches, err := client.GetCaches() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } processes, err := client.GetProcesses() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } slabs, err := client.GetSlabs() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } cons, err := client.GetConnections() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } requests, err := client.GetHTTPRequests() if err != nil { return nil, fmt.Errorf("Failed to get stats: %w", err) } ssl, err := client.GetSSL() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } zones, err := client.GetServerZones() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } upstreams, err := client.GetUpstreams() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } streamZones, err := client.GetStreamServerZones() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } streamUpstreams, err := client.GetStreamUpstreams() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } streamZoneSync, err := client.GetStreamZoneSync() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } locationZones, err := client.GetLocationZones() if err != nil { return nil, fmt.Errorf("failed to get stats: %w", err) } resolvers, err := client.GetResolvers() if err != nil { return nil, fmt.Errorf("failed to get 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, }, nil } // GetNginxInfo returns Nginx stats. func (client *NginxClient) GetNginxInfo() (*NginxInfo, error) { var info NginxInfo err := client.get("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) { var caches Caches err := client.get("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) { var slabs Slabs err := client.get("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) { var cons Connections err := client.get("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) { var requests HTTPRequests err := client.get("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) { var ssl SSL err := client.get("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) { var zones ServerZones err := client.get("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) { var zones StreamServerZones err := client.get("stream/server_zones", &zones) if err != nil { var ie *internalError if errors.As(err, &ie) { if ie.Code == pathNotFoundCode { return &zones, nil } } return nil, fmt.Errorf("failed to get stream server zones: %w", err) } return &zones, err } // GetUpstreams returns http/upstreams stats. func (client *NginxClient) GetUpstreams() (*Upstreams, error) { var upstreams Upstreams err := client.get("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) { var upstreams StreamUpstreams err := client.get("stream/upstreams", &upstreams) if err != nil { var ie *internalError if errors.As(err, &ie) { if ie.Code == pathNotFoundCode { return &upstreams, nil } } return nil, fmt.Errorf("failed to get stream upstreams: %w", err) } return &upstreams, nil } // GetStreamZoneSync returns stream/zone_sync stats. func (client *NginxClient) GetStreamZoneSync() (*StreamZoneSync, error) { var streamZoneSync StreamZoneSync err := client.get("stream/zone_sync", &streamZoneSync) if err != nil { var ie *internalError if errors.As(err, &ie) { if ie.Code == pathNotFoundCode { return nil, nil } } return nil, fmt.Errorf("failed to get stream zone sync: %w", err) } return &streamZoneSync, err } // GetLocationZones returns http/location_zones stats. func (client *NginxClient) GetLocationZones() (*LocationZones, error) { var locationZones LocationZones if client.version < 5 { return &locationZones, nil } err := client.get("http/location_zones", &locationZones) if err != nil { return nil, fmt.Errorf("failed to get location zones: %w", err) } return &locationZones, err } // GetResolvers returns Resolvers stats. func (client *NginxClient) GetResolvers() (*Resolvers, error) { var resolvers Resolvers if client.version < 5 { return &resolvers, nil } err := client.get("resolvers", &resolvers) if err != nil { return nil, fmt.Errorf("failed to get resolvers: %w", err) } return &resolvers, err } // GetProcesses returns Processes stats. func (client *NginxClient) GetProcesses() (*Processes, error) { var processes Processes err := client.get("processes", &processes) if err != nil { return nil, fmt.Errorf("failed to get processes: %w", err) } return &processes, err } // KeyValPairs are the key-value pairs stored in a zone. type KeyValPairs map[string]string // KeyValPairsByZone are the KeyValPairs for all zones, by zone name. 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) } // 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) getKeyValPairs(zone string, stream bool) (KeyValPairs, error) { base := "http" if stream { base = "stream" } if zone == "" { return nil, fmt.Errorf("zone required") } path := fmt.Sprintf("%v/keyvals/%v", base, zone) var keyValPairs KeyValPairs err := client.get(path, &keyValPairs) if err != nil { return nil, fmt.Errorf("failed to get keyvals for %v/%v zone: %w", base, zone, err) } return keyValPairs, nil } // GetAllKeyValPairs fetches all key/value pairs for all HTTP zones. func (client *NginxClient) GetAllKeyValPairs() (KeyValPairsByZone, error) { return client.getAllKeyValPairs(httpContext) } // GetAllStreamKeyValPairs fetches all key/value pairs for all Stream zones. func (client *NginxClient) GetAllStreamKeyValPairs() (KeyValPairsByZone, error) { return client.getAllKeyValPairs(streamContext) } func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, error) { base := "http" if stream { base = "stream" } path := fmt.Sprintf("%v/keyvals", base) var keyValPairsByZone KeyValPairsByZone err := client.get(path, &keyValPairsByZone) if err != nil { return nil, fmt.Errorf("failed to get keyvals for all %v zones: %w", base, err) } return keyValPairsByZone, nil } // 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) } // 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) addKeyValPair(zone string, key string, val string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { return fmt.Errorf("zone required") } path := fmt.Sprintf("%v/keyvals/%v", base, zone) input := KeyValPairs{key: val} err := client.post(path, &input) if err != nil { return fmt.Errorf("failed to add key value pair for %v/%v zone: %w", base, zone, err) } return nil } // 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) } // 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) modifyKeyValPair(zone string, key string, val string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { return fmt.Errorf("zone required") } path := fmt.Sprintf("%v/keyvals/%v", base, zone) input := KeyValPairs{key: val} err := client.patch(path, &input, http.StatusNoContent) if err != nil { return fmt.Errorf("failed to update key value pair for %v/%v zone: %w", base, zone, err) } return nil } // 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) } // 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) } // 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 { base := "http" if stream { base = "stream" } if zone == "" { return fmt.Errorf("zone required") } // map[string]string can't have a nil value so we use a different type here. keyval := make(map[string]interface{}) keyval[key] = nil path := fmt.Sprintf("%v/keyvals/%v", base, zone) err := client.patch(path, &keyval, http.StatusNoContent) if err != nil { return fmt.Errorf("failed to remove key values pair for %v/%v zone: %w", base, zone, err) } return nil } // DeleteKeyValPairs deletes all the key-value pairs in a given HTTP zone. func (client *NginxClient) DeleteKeyValPairs(zone string) error { return client.deleteKeyValPairs(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) deleteKeyValPairs(zone string, stream bool) error { base := "http" if stream { base = "stream" } if zone == "" { return fmt.Errorf("zone required") } path := fmt.Sprintf("%v/keyvals/%v", base, zone) err := client.delete(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 { path := fmt.Sprintf("http/upstreams/%v/servers/%v", upstream, server.ID) server.ID = 0 err := client.patch(path, &server, http.StatusOK) if err != nil { return fmt.Errorf("failed to update %v server to %v upstream: %w", server.Server, upstream, err) } return nil } // UpdateStreamServer updates the stream server of the upstream. func (client *NginxClient) UpdateStreamServer(upstream string, server StreamUpstreamServer) error { path := fmt.Sprintf("stream/upstreams/%v/servers/%v", upstream, server.ID) server.ID = 0 err := client.patch(path, &server, http.StatusOK) if err != nil { return fmt.Errorf("failed to update %v stream server to %v upstream: %w", server.Server, upstream, err) } return nil } // Version returns client's current N+ API version. func (client *NginxClient) Version() int { return client.version } func addPortToServer(server string) string { if len(strings.Split(server, ":")) == 2 { return server } if len(strings.Split(server, "]:")) == 2 { return server } if strings.HasPrefix(server, "unix:") { return server } return fmt.Sprintf("%v:%v", server, defaultServerPort) } nginx-plus-go-client-0.9.0/client/nginx_test.go000066400000000000000000000243771412471403100215040ustar00rootroot00000000000000package client import ( "reflect" "testing" ) func TestDetermineUpdates(t *testing.T) { maxConns := 1 tests := []struct { updated []UpstreamServer nginx []UpstreamServer expectedToAdd []UpstreamServer expectedToDelete []UpstreamServer expectedToUpdate []UpstreamServer }{ { updated: []UpstreamServer{ { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, nginx: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, expectedToAdd: []UpstreamServer{ { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, expectedToDelete: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, }, { updated: []UpstreamServer{ { Server: "10.0.0.2:80", }, { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, nginx: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, { ID: 3, Server: "10.0.0.3:80", }, }, expectedToAdd: []UpstreamServer{ { Server: "10.0.0.4:80", }, }, expectedToDelete: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, }, }, { updated: []UpstreamServer{ { Server: "10.0.0.1:80", }, { Server: "10.0.0.2:80", }, { Server: "10.0.0.3:80", }, }, nginx: []UpstreamServer{ { Server: "10.0.0.1:80", }, { Server: "10.0.0.2:80", }, { Server: "10.0.0.3:80", }, }, }, { // empty values }, { updated: []UpstreamServer{ { Server: "10.0.0.1:80", MaxConns: &maxConns, }, }, nginx: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, expectedToDelete: []UpstreamServer{ { ID: 2, Server: "10.0.0.2:80", }, }, expectedToUpdate: []UpstreamServer{ { ID: 1, Server: "10.0.0.1:80", MaxConns: &maxConns, }, }, }, } 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) } } } func TestStreamDetermineUpdates(t *testing.T) { maxConns := 1 tests := []struct { updated []StreamUpstreamServer nginx []StreamUpstreamServer expectedToAdd []StreamUpstreamServer expectedToDelete []StreamUpstreamServer expectedToUpdate []StreamUpstreamServer }{ { updated: []StreamUpstreamServer{ { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, nginx: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, expectedToAdd: []StreamUpstreamServer{ { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, expectedToDelete: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, }, { updated: []StreamUpstreamServer{ { Server: "10.0.0.2:80", }, { Server: "10.0.0.3:80", }, { Server: "10.0.0.4:80", }, }, nginx: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, { ID: 3, Server: "10.0.0.3:80", }, }, expectedToAdd: []StreamUpstreamServer{ { Server: "10.0.0.4:80", }, }, expectedToDelete: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, }, }, { updated: []StreamUpstreamServer{ { Server: "10.0.0.1:80", }, { Server: "10.0.0.2:80", }, { Server: "10.0.0.3:80", }, }, nginx: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, { ID: 3, Server: "10.0.0.3:80", }, }, }, { // empty values }, { updated: []StreamUpstreamServer{ { Server: "10.0.0.1:80", MaxConns: &maxConns, }, }, nginx: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", }, { ID: 2, Server: "10.0.0.2:80", }, }, expectedToDelete: []StreamUpstreamServer{ { ID: 2, Server: "10.0.0.2:80", }, }, expectedToUpdate: []StreamUpstreamServer{ { ID: 1, Server: "10.0.0.1:80", MaxConns: &maxConns, }, }, }, } 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) } } } func TestAddPortToServer(t *testing.T) { // More info about addresses http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server tests := []struct { address string expected string msg string }{ { address: "example.com:8080", expected: "example.com:8080", msg: "host and port", }, { address: "127.0.0.1:8080", expected: "127.0.0.1:8080", msg: "ipv4 and port", }, { address: "[::]:8080", expected: "[::]:8080", msg: "ipv6 and port", }, { address: "unix:/path/to/socket", expected: "unix:/path/to/socket", msg: "unix socket", }, { address: "example.com", expected: "example.com:80", msg: "host without port", }, { address: "127.0.0.1", expected: "127.0.0.1:80", msg: "ipv4 without port", }, { address: "[::]", expected: "[::]:80", msg: "ipv6 without port", }, } 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) } } } func TestHaveSameParameters(t *testing.T) { tests := []struct { server UpstreamServer serverNGX UpstreamServer expected bool }{ { server: UpstreamServer{}, serverNGX: UpstreamServer{}, expected: true, }, { server: UpstreamServer{ID: 2}, serverNGX: UpstreamServer{ID: 3}, expected: true, }, { server: UpstreamServer{}, serverNGX: UpstreamServer{ MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, expected: true, }, { server: UpstreamServer{ ID: 1, Server: "127.0.0.1", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, serverNGX: UpstreamServer{ ID: 1, Server: "127.0.0.1", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, expected: true, }, { server: UpstreamServer{SlowStart: "10s"}, serverNGX: UpstreamServer{}, expected: false, }, { server: UpstreamServer{}, serverNGX: UpstreamServer{SlowStart: "10s"}, expected: false, }, { server: UpstreamServer{SlowStart: "20s"}, serverNGX: UpstreamServer{SlowStart: "10s"}, expected: false, }, } 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) } } } func TestHaveSameParametersForStream(t *testing.T) { tests := []struct { server StreamUpstreamServer serverNGX StreamUpstreamServer expected bool }{ { server: StreamUpstreamServer{}, serverNGX: StreamUpstreamServer{}, expected: true, }, { server: StreamUpstreamServer{ID: 2}, serverNGX: StreamUpstreamServer{ID: 3}, expected: true, }, { server: StreamUpstreamServer{}, serverNGX: StreamUpstreamServer{ MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, expected: true, }, { server: StreamUpstreamServer{ ID: 1, Server: "127.0.0.1", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, serverNGX: StreamUpstreamServer{ ID: 1, Server: "127.0.0.1", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Weight: &defaultWeight, Down: &defaultDown, }, expected: true, }, { server: StreamUpstreamServer{}, serverNGX: StreamUpstreamServer{SlowStart: "10s"}, expected: false, }, { server: StreamUpstreamServer{SlowStart: "20s"}, serverNGX: StreamUpstreamServer{SlowStart: "10s"}, expected: false, }, } 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) } } } nginx-plus-go-client-0.9.0/docker/000077500000000000000000000000001412471403100167475ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/docker/Dockerfile000066400000000000000000000030321412471403100207370ustar00rootroot00000000000000# syntax=docker/dockerfile:1.3 FROM debian:buster-slim LABEL maintainer="NGINX Docker Maintainers " ARG NGINX_PLUS_VERSION # Install NGINX Plus # Download certificate and key from the customer portal (https://my.f5.com) # and copy to the build context SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 \ --mount=type=secret,id=nginx-repo.key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 \ apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl apt-transport-https \ && curl -sSL https://cs.nginx.com/static/keys/nginx_signing.key | gpg --dearmor > /etc/apt/trusted.gpg.d/nginx_signing.gpg \ && curl -sSL -o /etc/apt/apt.conf.d/90pkgs-nginx https://cs.nginx.com/static/files/90pkgs-nginx \ && printf "%s\n" "deb https://pkgs.nginx.com/plus/debian buster nginx-plus" > /etc/apt/sources.list.d/nginx-plus.list \ && apt-get update && apt-get install -y nginx-plus-${NGINX_PLUS_VERSION} \ && apt-get remove --purge --auto-remove -y gnupg \ && rm -rf /var/lib/apt/lists/* \ && rm /etc/apt/apt.conf.d/90pkgs-nginx /etc/apt/sources.list.d/nginx-plus.list # Forward request logs to Docker log collector RUN ln -sf /dev/stdout /var/log/nginx/access.log \ && ln -sf /dev/stderr /var/log/nginx/error.log EXPOSE 80 STOPSIGNAL SIGQUIT RUN rm -rf /etc/nginx/conf.d/* COPY test.conf /etc/nginx/conf.d/ COPY nginx.conf /etc/nginx/ CMD ["nginx", "-g", "daemon off;"] nginx-plus-go-client-0.9.0/docker/nginx.conf000066400000000000000000000024041412471403100207410ustar00rootroot00000000000000 user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; keyval_zone zone=zone_one:32k; keyval $arg_text $text zone=zone_one; include /etc/nginx/conf.d/*.conf; } stream { keyval_zone zone=zone_one_stream:32k; keyval $hostname $text zone=zone_one_stream; keyval_zone zone=zone_test_sync:32k timeout=5s sync; upstream stream_test { zone stream_test 64k; } server { listen 8081; proxy_pass stream_test; status_zone stream_test; health_check interval=10 fails=3 passes=1; } resolver 127.0.0.11 valid=5s status_zone=resolver_test; server { listen 7777; zone_sync; zone_sync_server nginx-plus-test:7777 resolve; } } nginx-plus-go-client-0.9.0/docker/nginx_no_stream.conf000066400000000000000000000012101412471403100230020ustar00rootroot00000000000000 user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } nginx-plus-go-client-0.9.0/docker/test.conf000066400000000000000000000010471412471403100205770ustar00rootroot00000000000000upstream test { zone test 64k; } proxy_cache_path /var/cache/nginx keys_zone=http_cache:10m max_size=100m; server { listen 8080; location = /dashboard.html { root /usr/share/nginx/html; } location /api { status_zone location_test; api write=on; } location /test { proxy_pass http://test; proxy_cache http_cache; health_check interval=10 fails=3 passes=1; } status_zone test; } upstream test-drain { zone test-drain 64k; server 127.0.0.1:9001 drain; } nginx-plus-go-client-0.9.0/go.mod000066400000000000000000000000711412471403100166040ustar00rootroot00000000000000module github.com/nginxinc/nginx-plus-go-client go 1.17 nginx-plus-go-client-0.9.0/tests/000077500000000000000000000000001412471403100166425ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/tests/client_no_stream_test.go000066400000000000000000000022361412471403100235600ustar00rootroot00000000000000package tests import ( "net/http" "testing" "github.com/nginxinc/nginx-plus-go-client/client" "github.com/nginxinc/nginx-plus-go-client/tests/helpers" ) // TestStatsNoStream tests the peculiar behavior of getting Stream-related // stats from the API when there are no stream blocks in the config. // 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) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } stats, err := c.GetStats() if err != nil { t.Errorf("Error getting stats: %w", err) } if stats.Connections.Accepted < 1 { t.Errorf("Stats should report some connections: %v", stats.Connections) } if len(stats.StreamServerZones) != 0 { t.Error("No stream block should result in no StreamServerZones") } if len(stats.StreamUpstreams) != 0 { t.Error("No stream block should result in no StreamUpstreams") } if stats.StreamZoneSync != nil { t.Error("No stream block should result in StreamZoneSync = `nil`") } } nginx-plus-go-client-0.9.0/tests/client_test.go000066400000000000000000001000441412471403100215050ustar00rootroot00000000000000package tests import ( "net" "net/http" "reflect" "testing" "time" "github.com/nginxinc/nginx-plus-go-client/client" "github.com/nginxinc/nginx-plus-go-client/tests/helpers" ) const ( cacheZone = "http_cache" upstream = "test" streamUpstream = "stream_test" streamZoneSync = "zone_test_sync" locationZone = "location_test" resolverMetric = "resolver_test" ) var ( defaultMaxConns = 0 defaultMaxFails = 1 defaultFailTimeout = "10s" defaultSlowStart = "0s" defaultBackup = false defaultDown = false defaultWeight = 1 ) func TestStreamClient(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error when creating a client: %v", err) } streamServer := client.StreamUpstreamServer{ Server: "127.0.0.1:8001", } // test adding a stream server err = c.AddStreamServer(streamUpstream, streamServer) if err != nil { t.Fatalf("Error when adding a server: %v", err) } err = c.AddStreamServer(streamUpstream, streamServer) if err == nil { t.Errorf("Adding a duplicated server succeeded") } // test deleting a stream server err = c.DeleteStreamServer(streamUpstream, streamServer.Server) if err != nil { t.Fatalf("Error when deleting a server: %v", err) } err = c.DeleteStreamServer(streamUpstream, streamServer.Server) if err == nil { t.Errorf("Deleting a nonexisting server succeeded") } streamServers, err := c.GetStreamServers(streamUpstream) if err != nil { t.Errorf("Error getting stream servers: %w", err) } if len(streamServers) != 0 { t.Errorf("Expected 0 servers, got %v", streamServers) } // test updating stream servers streamServers1 := []client.StreamUpstreamServer{ { Server: "127.0.0.1:8001", }, { Server: "127.0.0.2:8002", }, { Server: "127.0.0.3:8003", }, } streamAdded, streamDeleted, streamUpdated, err := c.UpdateStreamServers(streamUpstream, streamServers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(streamAdded) != len(streamServers1) { t.Errorf("The number of added servers %v != %v", len(streamAdded), len(streamServers1)) } if len(streamDeleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(streamDeleted)) } if len(streamUpdated) != 0 { t.Errorf("The number of updated servers %v != 0", len(streamUpdated)) } // test getting servers streamServers, err = c.GetStreamServers(streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } if !compareStreamUpstreamServers(streamServers1, streamServers) { t.Errorf("Return servers %v != added servers %v", streamServers, streamServers1) } // updating with the same servers added, deleted, updated, err := c.UpdateStreamServers(streamUpstream, streamServers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // updating one server with different parameters newMaxConns := 5 newMaxFails := 6 newFailTimeout := "15s" newSlowStart := "10s" streamServers[0].MaxConns = &newMaxConns streamServers[0].MaxFails = &newMaxFails streamServers[0].FailTimeout = newFailTimeout streamServers[0].SlowStart = newSlowStart // updating one server with only one different parameter streamServers[1].SlowStart = newSlowStart added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, streamServers) if err != nil { t.Fatalf("Error when updating server with different parameters: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(deleted)) } if len(updated) != 2 { t.Errorf("The number of updated servers %v != 2", len(updated)) } streamServers, err = c.GetStreamServers(streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } for _, srv := range streamServers { if srv.Server == streamServers[0].Server { if *srv.MaxConns != newMaxConns { t.Errorf("The parameter MaxConns of the updated server %v is != %v", *srv.MaxConns, newMaxConns) } if *srv.MaxFails != newMaxFails { t.Errorf("The parameter MaxFails of the updated server %v is != %v", *srv.MaxFails, newMaxFails) } if srv.FailTimeout != newFailTimeout { t.Errorf("The parameter FailTimeout of the updated server %v is != %v", srv.FailTimeout, newFailTimeout) } if srv.SlowStart != newSlowStart { t.Errorf("The parameter SlowStart of the updated server %v is != %v", srv.SlowStart, newSlowStart) } } if srv.Server == streamServers[1].Server { if *srv.MaxConns != defaultMaxConns { t.Errorf("The parameter MaxConns of the updated server %v is != %v", *srv.MaxConns, defaultMaxConns) } if *srv.MaxFails != defaultMaxFails { t.Errorf("The parameter MaxFails of the updated server %v is != %v", *srv.MaxFails, defaultMaxFails) } if srv.FailTimeout != defaultFailTimeout { t.Errorf("The parameter FailTimeout of the updated server %v is != %v", srv.FailTimeout, defaultFailTimeout) } if srv.SlowStart != newSlowStart { t.Errorf("The parameter SlowStart of the updated server %v is != %v", srv.SlowStart, newSlowStart) } } } streamServers2 := []client.StreamUpstreamServer{ { Server: "127.0.0.2:8003", }, { Server: "127.0.0.2:8004", }, { Server: "127.0.0.2:8005", }, } // updating with 2 new servers, 1 existing added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, streamServers2) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 3 { t.Errorf("The number of added servers %v != 3", len(added)) } if len(deleted) != 3 { t.Errorf("The number of deleted servers %v != 3", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // updating with zero servers - removing added, deleted, updated, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 3 { t.Errorf("The number of deleted servers %v != 3", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // test getting servers again servers, err := c.GetStreamServers(streamUpstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } if len(servers) != 0 { t.Errorf("The number of servers %v != 0", len(servers)) } } func TestStreamUpstreamServer(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } maxFails := 64 weight := 10 maxConns := 321 backup := true down := true streamServer := client.StreamUpstreamServer{ Server: "127.0.0.1:2000", MaxConns: &maxConns, MaxFails: &maxFails, FailTimeout: "21s", SlowStart: "12s", Weight: &weight, Backup: &backup, Down: &down, } err = c.AddStreamServer(streamUpstream, streamServer) if err != nil { t.Errorf("Error adding upstream server: %w", err) } servers, err := c.GetStreamServers(streamUpstream) if err != nil { t.Fatalf("Error getting stream servers: %v", err) } if len(servers) != 1 { t.Errorf("Too many servers") } // don't compare IDs servers[0].ID = 0 if !reflect.DeepEqual(streamServer, servers[0]) { t.Errorf("Expected: %v Got: %v", streamServer, servers[0]) } // remove stream upstream servers _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %w", err) } } func TestClient(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error when creating a client: %v", err) } // test checking an upstream for existence err = c.CheckIfUpstreamExists(upstream) if err != nil { t.Fatalf("Error when checking an upstream for existence: %v", err) } err = c.CheckIfUpstreamExists("random") if err == nil { t.Errorf("Nonexisting upstream exists") } server := client.UpstreamServer{ Server: "127.0.0.1:8001", } // test adding a http server err = c.AddHTTPServer(upstream, server) if err != nil { t.Fatalf("Error when adding a server: %v", err) } err = c.AddHTTPServer(upstream, server) if err == nil { t.Errorf("Adding a duplicated server succeeded") } // test deleting a http server err = c.DeleteHTTPServer(upstream, server.Server) if err != nil { t.Fatalf("Error when deleting a server: %v", err) } err = c.DeleteHTTPServer(upstream, server.Server) if err == nil { t.Errorf("Deleting a nonexisting server succeeded") } // test updating servers servers1 := []client.UpstreamServer{ { Server: "127.0.0.2:8001", }, { Server: "127.0.0.2:8002", }, { Server: "127.0.0.2:8003", }, } added, deleted, updated, err := c.UpdateHTTPServers(upstream, servers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != len(servers1) { t.Errorf("The number of added servers %v != %v", len(added), len(servers1)) } if len(deleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // test getting servers servers, err := c.GetHTTPServers(upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } if !compareUpstreamServers(servers1, servers) { t.Errorf("Return servers %v != added servers %v", servers, servers1) } // continue test updating servers // updating with the same servers added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers1) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // updating one server with different parameters newMaxConns := 5 newMaxFails := 6 newFailTimeout := "15s" newSlowStart := "10s" servers[0].MaxConns = &newMaxConns servers[0].MaxFails = &newMaxFails servers[0].FailTimeout = newFailTimeout servers[0].SlowStart = newSlowStart // updating one server with only one different parameter servers[1].SlowStart = newSlowStart added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers) if err != nil { t.Fatalf("Error when updating server with different parameters: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 0 { t.Errorf("The number of deleted servers %v != 0", len(deleted)) } if len(updated) != 2 { t.Errorf("The number of updated servers %v != 2", len(updated)) } servers, err = c.GetHTTPServers(upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } for _, srv := range servers { if srv.Server == servers[0].Server { if *srv.MaxConns != newMaxConns { t.Errorf("The parameter MaxConns of the updated server %v is != %v", *srv.MaxConns, newMaxConns) } if *srv.MaxFails != newMaxFails { t.Errorf("The parameter MaxFails of the updated server %v is != %v", *srv.MaxFails, newMaxFails) } if srv.FailTimeout != newFailTimeout { t.Errorf("The parameter FailTimeout of the updated server %v is != %v", srv.FailTimeout, newFailTimeout) } if srv.SlowStart != newSlowStart { t.Errorf("The parameter SlowStart of the updated server %v is != %v", srv.SlowStart, newSlowStart) } } if srv.Server == servers[1].Server { if *srv.MaxConns != defaultMaxConns { t.Errorf("The parameter MaxConns of the updated server %v is != %v", *srv.MaxConns, defaultMaxConns) } if *srv.MaxFails != defaultMaxFails { t.Errorf("The parameter MaxFails of the updated server %v is != %v", *srv.MaxFails, defaultMaxFails) } if srv.FailTimeout != defaultFailTimeout { t.Errorf("The parameter FailTimeout of the updated server %v is != %v", srv.FailTimeout, defaultFailTimeout) } if srv.SlowStart != newSlowStart { t.Errorf("The parameter SlowStart of the updated server %v is != %v", srv.SlowStart, newSlowStart) } } } servers2 := []client.UpstreamServer{ { Server: "127.0.0.2:8003", }, { Server: "127.0.0.2:8004", }, { Server: "127.0.0.2:8005", }, } // updating with 2 new servers, 1 existing added, deleted, updated, err = c.UpdateHTTPServers(upstream, servers2) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 2 { t.Errorf("The number of added servers %v != 2", len(added)) } if len(deleted) != 2 { t.Errorf("The number of deleted servers %v != 2", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // updating with zero servers - removing added, deleted, updated, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) if err != nil { t.Fatalf("Error when updating servers: %v", err) } if len(added) != 0 { t.Errorf("The number of added servers %v != 0", len(added)) } if len(deleted) != 3 { t.Errorf("The number of deleted servers %v != 3", len(deleted)) } if len(updated) != 0 { t.Errorf("The number of updated servers %v != 0", len(updated)) } // test getting servers again servers, err = c.GetHTTPServers(upstream) if err != nil { t.Fatalf("Error when getting servers: %v", err) } if len(servers) != 0 { t.Errorf("The number of servers %v != 0", len(servers)) } } func TestUpstreamServer(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } maxFails := 64 weight := 10 maxConns := 321 backup := true down := true server := client.UpstreamServer{ Server: "127.0.0.1:2000", MaxConns: &maxConns, MaxFails: &maxFails, FailTimeout: "21s", SlowStart: "12s", Weight: &weight, Route: "test", Backup: &backup, Down: &down, } err = c.AddHTTPServer(upstream, server) if err != nil { t.Errorf("Error adding upstream server: %w", err) } servers, err := c.GetHTTPServers(upstream) if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) } if len(servers) != 1 { t.Errorf("Too many servers") } // don't compare IDs servers[0].ID = 0 if !reflect.DeepEqual(server, servers[0]) { t.Errorf("Expected: %v Got: %v", server, servers[0]) } // remove upstream servers _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %w", err) } } func TestStats(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } server := client.UpstreamServer{ Server: "127.0.0.1:8080", } err = c.AddHTTPServer(upstream, server) if err != nil { t.Errorf("Error adding upstream server: %w", err) } stats, err := c.GetStats() if err != nil { t.Errorf("Error getting stats: %w", err) } // NginxInfo if stats.NginxInfo.Version == "" { t.Error("Missing version string") } if stats.NginxInfo.Build == "" { t.Error("Missing build string") } if stats.NginxInfo.Address == "" { t.Errorf("Missing server address") } if stats.NginxInfo.Generation < 1 { t.Errorf("Bad config generation: %v", stats.NginxInfo.Generation) } if stats.NginxInfo.LoadTimestamp == "" { t.Error("Missing load timestamp") } if stats.NginxInfo.Timestamp == "" { t.Error("Missing timestamp") } if stats.NginxInfo.ProcessID < 1 { t.Errorf("Bad process id: %v", stats.NginxInfo.ProcessID) } if stats.NginxInfo.ParentProcessID < 1 { t.Errorf("Bad parent process id: %v", stats.NginxInfo.ParentProcessID) } if stats.Connections.Accepted < 1 { t.Errorf("Bad connections: %v", stats.Connections) } if val, ok := stats.Caches[cacheZone]; ok { if val.MaxSize != 104857600 { // 100MiB t.Errorf("Cache max size stats missing: %v", val.Size) } } else { t.Errorf("Cache stats for cache zone '%v' not found", cacheZone) } if val, ok := stats.Slabs[upstream]; ok { if val.Pages.Used < 1 { t.Errorf("Slabs pages stats missing: %v", val.Pages) } if len(val.Slots) < 1 { t.Errorf("Slab slots not visible in stats: %v", val.Slots) } } else { t.Errorf("Slab stats for upstream '%v' not found", upstream) } if stats.HTTPRequests.Total < 1 { t.Errorf("Bad HTTPRequests: %v", stats.HTTPRequests) } // SSL metrics blank in this example if len(stats.ServerZones) < 1 { t.Errorf("No ServerZone metrics: %v", stats.ServerZones) } if val, ok := stats.ServerZones["test"]; ok { if val.Requests < 1 { t.Errorf("ServerZone stats missing: %v", val) } } else { t.Errorf("ServerZone 'test' not found") } if ups, ok := stats.Upstreams["test"]; ok { if len(ups.Peers) < 1 { t.Errorf("upstream server not visible in stats") } else { if ups.Peers[0].State != "up" { t.Errorf("upstream server state should be 'up'") } if ups.Peers[0].HealthChecks.LastPassed { t.Errorf("upstream server health check should report last failed") } } } else { t.Errorf("Upstream 'test' not found") } if locZones, ok := stats.LocationZones[locationZone]; ok { if locZones.Requests < 1 { t.Errorf("LocationZone stats missing: %v", locZones.Requests) } } else { t.Errorf("LocationZone %v not found", locationZone) } if resolver, ok := stats.Resolvers[resolverMetric]; ok { if resolver.Requests.Name < 1 { t.Errorf("Resolvers stats missing: %v", resolver.Requests) } } else { t.Errorf("Resolver %v not found", resolverMetric) } // cleanup upstream servers _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %w", err) } } func TestUpstreamServerDefaultParameters(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } server := client.UpstreamServer{ Server: "127.0.0.1:2000", } expected := client.UpstreamServer{ ID: 0, Server: "127.0.0.1:2000", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Route: "", Backup: &defaultBackup, Down: &defaultDown, Drain: false, Weight: &defaultWeight, Service: "", } err = c.AddHTTPServer(upstream, server) if err != nil { t.Errorf("Error adding upstream server: %w", err) } servers, err := c.GetHTTPServers(upstream) if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) } if len(servers) != 1 { t.Errorf("Too many servers") } // don't compare IDs servers[0].ID = 0 if !reflect.DeepEqual(expected, servers[0]) { t.Errorf("Expected: %v Got: %v", expected, servers[0]) } // remove upstream servers _, _, _, err = c.UpdateHTTPServers(upstream, []client.UpstreamServer{}) if err != nil { t.Errorf("Couldn't remove servers: %w", err) } } func TestStreamStats(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } server := client.StreamUpstreamServer{ Server: "127.0.0.1:8080", } err = c.AddStreamServer(streamUpstream, server) if err != nil { t.Errorf("Error adding stream upstream server: %w", err) } // make connection so we have stream server zone stats - ignore response _, err = net.Dial("tcp", helpers.GetStreamAddress()) if err != nil { t.Errorf("Error making tcp connection: %w", err) } // wait for health checks time.Sleep(50 * time.Millisecond) stats, err := c.GetStats() if err != nil { t.Errorf("Error getting stats: %w", err) } if stats.Connections.Active == 0 { t.Errorf("Bad connections: %v", stats.Connections) } if len(stats.StreamServerZones) < 1 { t.Errorf("No StreamServerZone metrics: %v", stats.StreamServerZones) } if streamServerZone, ok := stats.StreamServerZones[streamUpstream]; ok { if streamServerZone.Connections < 1 { t.Errorf("StreamServerZone stats missing: %v", streamServerZone) } } else { t.Errorf("StreamServerZone 'stream_test' not found") } if upstream, ok := stats.StreamUpstreams[streamUpstream]; ok { if len(upstream.Peers) < 1 { t.Errorf("stream upstream server not visible in stats") } else { if upstream.Peers[0].State != "up" { t.Errorf("stream upstream server state should be 'up'") } if upstream.Peers[0].Connections < 1 { t.Errorf("stream upstream should have connects value") } if !upstream.Peers[0].HealthChecks.LastPassed { t.Errorf("stream upstream server health check should report last passed") } } } else { t.Errorf("Stream upstream 'stream_test' not found") } // cleanup stream upstream servers _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove stream servers: %w", err) } } func TestStreamUpstreamServerDefaultParameters(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } streamServer := client.StreamUpstreamServer{ Server: "127.0.0.1:2000", } expected := client.StreamUpstreamServer{ ID: 0, Server: "127.0.0.1:2000", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Backup: &defaultBackup, Down: &defaultDown, Weight: &defaultWeight, Service: "", } err = c.AddStreamServer(streamUpstream, streamServer) if err != nil { t.Errorf("Error adding upstream server: %w", err) } streamServers, err := c.GetStreamServers(streamUpstream) if err != nil { t.Fatalf("Error getting stream servers: %v", err) } if len(streamServers) != 1 { t.Errorf("Too many servers") } // don't compare IDs streamServers[0].ID = 0 if !reflect.DeepEqual(expected, streamServers[0]) { t.Errorf("Expected: %v Got: %v", expected, streamServers[0]) } // cleanup stream upstream servers _, _, _, err = c.UpdateStreamServers(streamUpstream, []client.StreamUpstreamServer{}) if err != nil { t.Errorf("Couldn't remove stream servers: %w", err) } } func TestKeyValue(t *testing.T) { zoneName := "zone_one" httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } err = c.AddKeyValPair(zoneName, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %w", err) } var keyValPairs client.KeyValPairs keyValPairs, err = c.GetKeyValPairs(zoneName) if err != nil { t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) } expectedKeyValPairs := client.KeyValPairs{ "key1": "val1", } if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } keyValuPairsByZone, err := c.GetAllKeyValPairs() if err != nil { t.Errorf("Couldn't get keyvals, %w", err) } expectedKeyValPairsByZone := client.KeyValPairsByZone{ zoneName: expectedKeyValPairs, } if !reflect.DeepEqual(expectedKeyValPairsByZone, keyValuPairsByZone) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairsByZone, keyValuPairsByZone) } // modify keyval expectedKeyValPairs["key1"] = "valModified1" err = c.ModifyKeyValPair(zoneName, "key1", "valModified1") if err != nil { t.Errorf("couldn't set keyval: %w", err) } keyValPairs, err = c.GetKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } // error expected err = c.AddKeyValPair(zoneName, "key1", "valModified1") if err == nil { t.Errorf("adding same key/val should result in error") } err = c.AddKeyValPair(zoneName, "key2", "val2") if err != nil { t.Errorf("error adding another key/val pair: %w", err) } err = c.DeleteKeyValuePair(zoneName, "key1") if err != nil { t.Errorf("error deleting key") } expectedKeyValPairs2 := client.KeyValPairs{ "key2": "val2", } keyValPairs, err = c.GetKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } if !reflect.DeepEqual(keyValPairs, expectedKeyValPairs2) { t.Errorf("didn't delete key1 %+v", keyValPairs) } err = c.DeleteKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't delete all: %w", err) } keyValPairs, err = c.GetKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } if len(keyValPairs) > 0 { t.Errorf("zone should be empty after bulk delete") } // error expected err = c.ModifyKeyValPair(zoneName, "key1", "val1") if err == nil { t.Errorf("modifying nonexistent key/val should result in error") } } func TestKeyValueStream(t *testing.T) { zoneName := "zone_one_stream" httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } err = c.AddStreamKeyValPair(zoneName, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %w", err) } keyValPairs, err := c.GetStreamKeyValPairs(zoneName) if err != nil { t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) } expectedKeyValPairs := client.KeyValPairs{ "key1": "val1", } if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } keyValPairsByZone, err := c.GetAllStreamKeyValPairs() if err != nil { t.Errorf("Couldn't get keyvals, %w", err) } expectedKeyValuePairsByZone := client.KeyValPairsByZone{ zoneName: expectedKeyValPairs, streamZoneSync: client.KeyValPairs{}, } if !reflect.DeepEqual(expectedKeyValuePairsByZone, keyValPairsByZone) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValuePairsByZone, keyValPairsByZone) } // modify keyval expectedKeyValPairs["key1"] = "valModified1" err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified1") if err != nil { t.Errorf("couldn't set keyval: %w", err) } keyValPairs, err = c.GetStreamKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) } // error expected err = c.AddStreamKeyValPair(zoneName, "key1", "valModified1") if err == nil { t.Errorf("adding same key/val should result in error") } err = c.AddStreamKeyValPair(zoneName, "key2", "val2") if err != nil { t.Errorf("error adding another key/val pair: %w", err) } err = c.DeleteStreamKeyValuePair(zoneName, "key1") if err != nil { t.Errorf("error deleting key") } keyValPairs, err = c.GetStreamKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } expectedKeyValPairs2 := client.KeyValPairs{ "key2": "val2", } if !reflect.DeepEqual(keyValPairs, expectedKeyValPairs2) { t.Errorf("didn't delete key1 %+v", keyValPairs) } err = c.DeleteStreamKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't delete all: %w", err) } keyValPairs, err = c.GetStreamKeyValPairs(zoneName) if err != nil { t.Errorf("couldn't get keyval: %w", err) } if len(keyValPairs) > 0 { t.Errorf("zone should be empty after bulk delete") } // error expected err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified") if err == nil { t.Errorf("modifying nonexistent key/val should result in error") } } func TestStreamZoneSync(t *testing.T) { c1, err := client.NewNginxClient(&http.Client{}, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } c2, err := client.NewNginxClient(&http.Client{}, helpers.GetAPIEndpointOfHelper()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } err = c1.AddStreamKeyValPair(streamZoneSync, "key1", "val1") if err != nil { t.Errorf("Couldn't set keyvals: %w", err) } // wait for nodes to sync information of synced zones time.Sleep(5 * time.Second) statsC1, err := c1.GetStats() if err != nil { t.Errorf("Error getting stats: %w", err) } if statsC1.StreamZoneSync == nil { t.Errorf("Stream zone sync can't be nil if configured") } if statsC1.StreamZoneSync.Status.NodesOnline == 0 { t.Errorf("At least 1 node must be online") } if statsC1.StreamZoneSync.Status.MsgsOut == 0 { t.Errorf("Msgs out cannot be 0") } if statsC1.StreamZoneSync.Status.MsgsIn == 0 { t.Errorf("Msgs in cannot be 0") } if statsC1.StreamZoneSync.Status.BytesIn == 0 { t.Errorf("Bytes in cannot be 0") } if statsC1.StreamZoneSync.Status.BytesOut == 0 { t.Errorf("Bytes Out cannot be 0") } if zone, ok := statsC1.StreamZoneSync.Zones[streamZoneSync]; ok { if zone.RecordsTotal == 0 { t.Errorf("Total records cannot be 0 after adding keyvals") } if zone.RecordsPending != 0 { t.Errorf("Pending records must be 0 after adding keyvals") } } else { t.Errorf("Sync zone %v missing in stats", streamZoneSync) } statsC2, err := c2.GetStats() if err != nil { t.Errorf("Error getting stats: %w", err) } if statsC2.StreamZoneSync == nil { t.Errorf("Stream zone sync can't be nil if configured") } if statsC2.StreamZoneSync.Status.NodesOnline == 0 { t.Errorf("At least 1 node must be online") } if statsC2.StreamZoneSync.Status.MsgsOut != 0 { t.Errorf("Msgs out must be 0") } if statsC2.StreamZoneSync.Status.MsgsIn == 0 { t.Errorf("Msgs in cannot be 0") } if statsC2.StreamZoneSync.Status.BytesIn == 0 { t.Errorf("Bytes in cannot be 0") } if statsC2.StreamZoneSync.Status.BytesOut != 0 { t.Errorf("Bytes out must be 0") } if zone, ok := statsC2.StreamZoneSync.Zones[streamZoneSync]; ok { if zone.RecordsTotal == 0 { t.Errorf("Total records cannot be 0 after adding keyvals") } if zone.RecordsPending != 0 { t.Errorf("Pending records must be 0 after adding keyvals") } } else { t.Errorf("Sync zone %v missing in stats", streamZoneSync) } } func compareUpstreamServers(x []client.UpstreamServer, y []client.UpstreamServer) bool { var xServers []string for _, us := range x { xServers = append(xServers, us.Server) } var yServers []string for _, us := range y { yServers = append(yServers, us.Server) } return reflect.DeepEqual(xServers, yServers) } func compareStreamUpstreamServers(x []client.StreamUpstreamServer, y []client.StreamUpstreamServer) bool { var xServers []string for _, us := range x { xServers = append(xServers, us.Server) } var yServers []string for _, us := range y { yServers = append(yServers, us.Server) } return reflect.DeepEqual(xServers, yServers) } func TestUpstreamServerWithDrain(t *testing.T) { httpClient := &http.Client{} c, err := client.NewNginxClient(httpClient, helpers.GetAPIEndpoint()) if err != nil { t.Fatalf("Error connecting to nginx: %v", err) } server := client.UpstreamServer{ ID: 0, Server: "127.0.0.1:9001", MaxConns: &defaultMaxConns, MaxFails: &defaultMaxFails, FailTimeout: defaultFailTimeout, SlowStart: defaultSlowStart, Route: "", Backup: &defaultBackup, Down: &defaultDown, Drain: true, Weight: &defaultWeight, Service: "", } // Get existing upstream servers servers, err := c.GetHTTPServers("test-drain") if err != nil { t.Fatalf("Error getting HTTPServers: %v", err) } if len(servers) != 1 { t.Errorf("Too many servers") } servers[0].ID = 0 if !reflect.DeepEqual(server, servers[0]) { t.Errorf("Expected: %v Got: %v", server, servers[0]) } } nginx-plus-go-client-0.9.0/tests/helpers/000077500000000000000000000000001412471403100203045ustar00rootroot00000000000000nginx-plus-go-client-0.9.0/tests/helpers/env_variables.go000066400000000000000000000017061412471403100234570ustar00rootroot00000000000000package helpers import "os" // GetAPIEndpoint returns the api endpoint. // For testing purposes only. The endpoint is set in the Makefile. func GetAPIEndpoint() string { ep := os.Getenv("TEST_API_ENDPOINT") if ep == "" { panic("TEST_API_ENDPOINT env variable is not set or empty") } return ep } // GetAPIEndpointOfHelper returns the api endpoint of the helper. // For testing purposes only. The endpoint is set in the Makefile. func GetAPIEndpointOfHelper() string { ep := os.Getenv("TEST_API_ENDPOINT_OF_HELPER") if ep == "" { panic("TEST_API_ENDPOINT_OF_HELPER env variable is not set or empty") } return ep } // GetStreamAddress returns the address of the unavailable stream server. // For testing purposes only. The address is set in the Makefile. func GetStreamAddress() string { addr := os.Getenv("TEST_UNAVAILABLE_STREAM_ADDRESS") if addr == "" { panic("TEST_UNAVAILABLE_STREAM_ADDRESS env variable is not set or empty") } return addr }