├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── defect.yml │ └── proposal.yml ├── dependabot.yml └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .goreleaser.yml ├── ABTaskFile ├── AUTH.md ├── CODE-OF-CONDUCT.md ├── GOVERNANCE.md ├── LICENSE ├── LOCAL_DEVELOPMENT.md ├── MAINTAINERS.md ├── README.md ├── cli ├── account_command.go ├── account_tls_command.go ├── audit_analyze_command.go ├── audit_checks_command.go ├── audit_command.go ├── audit_gather_command.go ├── auth_account_command.go ├── auth_account_exports.go ├── auth_account_imports.go ├── auth_command.go ├── auth_nkey_command.go ├── auth_operator_command.go ├── auth_user_command.go ├── auth_xkey_test.go ├── bench_command.go ├── cheats │ ├── account.md │ ├── auth.md │ ├── bench.md │ ├── consumer.md │ ├── contexts.md │ ├── errors.md │ ├── events.md │ ├── governor.md │ ├── kv.md │ ├── latency.md │ ├── obj.md │ ├── pub.md │ ├── reply.md │ ├── schemas.md │ ├── server.md │ ├── stream.md │ └── sub.md ├── cli.go ├── columns.go ├── consumer_command.go ├── context_command.go ├── errors_command.go ├── events_command.go ├── jsonschema.go ├── jsonschema_test.go ├── kv_command.go ├── latency_command.go ├── object_command.go ├── plugins_command.go ├── pub_command.go ├── reply_command.go ├── rtt_command.go ├── schema_command.go ├── schema_info_command.go ├── schema_req_command.go ├── schema_search_command.go ├── schema_validate_command.go ├── server_account_command.go ├── server_check_command.go ├── server_check_exporter_command.go ├── server_cluster_command.go ├── server_command.go ├── server_config_command.go ├── server_consumer_check.go ├── server_generate.go ├── server_graph_command.go ├── server_info_command.go ├── server_list_command.go ├── server_mapping_command.go ├── server_mkpasswd_command.go ├── server_ping_command.go ├── server_report_command.go ├── server_request_command.go ├── server_run_command.go ├── server_stream_check.go ├── server_watch_acct_command.go ├── server_watch_command.go ├── server_watch_js_command.go ├── server_watch_srv_command.go ├── service_command.go ├── stream_command.go ├── sub_command.go ├── top_command.go ├── trace_command.go ├── traffic_command.go ├── util.go ├── util_test.go └── yaml.go ├── columns └── columns.go ├── dependencies.md ├── go.mod ├── go.sum ├── install.ps1 ├── internal ├── asciigraph │ ├── LICENSE │ ├── asciigraph.go │ ├── asciigraph_test.go │ ├── color.go │ ├── legend.go │ ├── options.go │ └── utils.go ├── auth │ └── auth.go ├── exporter │ └── exporter.go ├── scaffold │ ├── funcs.go │ ├── logger.go │ ├── scaffold.go │ └── store │ │ ├── natsbuilder │ │ ├── bundle.yaml │ │ ├── form.yaml │ │ ├── scaffold.json │ │ └── scaffold │ │ │ ├── _partials │ │ │ ├── system_context.got │ │ │ └── user_context.got │ │ │ ├── cli │ │ │ ├── context.txt │ │ │ └── context │ │ │ │ └── generate.got │ │ │ ├── cluster.conf │ │ │ └── docker-compose.yaml │ │ ├── ngsleafnodeconfig │ │ ├── bundle.yaml │ │ ├── form.yaml │ │ ├── scaffold.json │ │ └── scaffold │ │ │ └── leafnode.conf │ │ ├── operator │ │ ├── bundle.yaml │ │ ├── form.yaml │ │ ├── scaffold.json │ │ └── scaffold │ │ │ └── server.conf │ │ └── operatork8s │ │ ├── bundle.yaml │ │ ├── form.yaml │ │ ├── scaffold.json │ │ └── scaffold │ │ └── values.yaml ├── sysclient │ ├── healthstatus.go │ └── sysclient.go └── util │ ├── backoff.go │ ├── config.go │ ├── header_test.go │ ├── headers.go │ ├── jetstream.go │ ├── progress.go │ ├── random.go │ ├── random_test.go │ ├── tables.go │ ├── util.go │ └── util_test.go ├── nats ├── main.go └── tests │ ├── auth_command_test.go │ ├── consumer_command_test.go │ ├── kv_test.go │ ├── nats_nix_test.go │ ├── nats_test.go │ ├── nats_windows_test.go │ ├── pub_command_test.go │ ├── server_command_test.go │ ├── service_command_test.go │ ├── stream_command_test.go │ ├── sub_command_test.go │ ├── test_helpers.go │ └── testdata │ ├── ORDERS_config.json │ ├── mem1_config.json │ └── mem1_pull1_consumer.json ├── options └── options.go ├── plugins └── plugins.go └── top ├── LICENSE ├── top.go ├── toputils.go └── toputils_test.go /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discussion 4 | url: https://github.com/nats-io/natscli/discussions 5 | about: Ideal for ideas, feedback, or longer form questions. 6 | - name: Chat 7 | url: https://slack.nats.io 8 | about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/defect.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Defect 3 | description: Report a defect, such as a bug or regression. 4 | labels: 5 | - defect 6 | body: 7 | - type: textarea 8 | id: observed 9 | attributes: 10 | label: Observed behavior 11 | description: Describe the unexpected behavior or performance regression you are observing. 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: expected 16 | attributes: 17 | label: Expected behavior 18 | description: Describe the expected behavior or performance characteristics. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: versions 23 | attributes: 24 | label: Server and client version 25 | description: |- 26 | Provide the versions you were using when the detect was observed. 27 | For the server, use `nats-server --version`, check the startup log output, or the image tag pulled from Docker. 28 | For the CLI client, use `nats --version`. 29 | For language-specific clients, check the version downloaded by the language dependency manager. 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: environment 34 | attributes: 35 | label: Host environment 36 | description: |- 37 | Specify any relevant details about the host environment the server and/or client was running in, 38 | such as operating system, CPU architecture, container runtime, etc. 39 | validations: 40 | required: false 41 | - type: textarea 42 | id: steps 43 | attributes: 44 | label: Steps to reproduce 45 | description: Provide as many concrete steps to reproduce the defect. 46 | validations: 47 | required: false 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposal 3 | description: Propose an enhancement or new feature. 4 | labels: 5 | - proposal 6 | body: 7 | - type: textarea 8 | id: change 9 | attributes: 10 | label: Proposed change 11 | description: This could be a behavior change, enhanced API, or a new feature. 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: usecase 16 | attributes: 17 | label: Use case 18 | description: What is the use case or general motivation for this proposal? 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: contribute 23 | attributes: 24 | label: Contribution 25 | description: |- 26 | Are you intending or interested in contributing code for this proposal if accepted? 27 | validations: 28 | required: false 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+\.[0-9]+\.[0-9]+ 6 | - v[0-9]+\.[0-9]+\.[0-9]+-preview\.[0-9]+ 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | 24 | - name: Install deps 25 | shell: bash --noprofile --norc -x -eo pipefail {0} 26 | run: | 27 | go install honnef.co/go/tools/cmd/staticcheck@latest 28 | go install github.com/client9/misspell/cmd/misspell@latest 29 | 30 | - name: Lint 31 | shell: bash --noprofile --norc -x -eo pipefail {0} 32 | run: | 33 | PATH=$PATH:$GOPATH/bin 34 | GO_LIST=$(go list ./... | grep -F -e asciigraph -v) 35 | $(exit $(go fmt $GO_LIST | wc -l)) 36 | go vet -composites=false $GO_LIST 37 | find . -type f -name "*.go" | grep -F -e asciigraph -v | xargs misspell -error -locale US 38 | staticcheck -f stylish $GO_LIST 39 | 40 | - name: Test 41 | shell: bash --noprofile --norc -x -eo pipefail {0} 42 | run: | 43 | set -e 44 | cd nats 45 | go build 46 | cd .. 47 | go list ./... | grep -F -e asciigraph -v | xargs go test -v --failfast -p=1 48 | 49 | 50 | - name: Create GitHub App Token 51 | id: token 52 | uses: actions/create-github-app-token@v1 53 | with: 54 | app-id: ${{ secrets.NATSIO_ARTIFACT_CROSS_REPO_PUSHER_APP_ID }} 55 | private-key: ${{ secrets.NATSIO_ARTIFACT_CROSS_REPO_PUSHER_PRIVATE_KEY }} 56 | owner: "nats-io" 57 | repositories: "homebrew-nats-tools" 58 | 59 | - name: Run GoReleaser 60 | uses: goreleaser/goreleaser-action@v6 61 | with: 62 | version: "~> v2" 63 | args: release --clean 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | GITHUB_APP_TOKEN: ${{ steps.token.outputs.token }} 67 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: NATS CLI Testing 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | lint: 11 | name: Running linting (ubuntu-latest/1.24) 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.24" 21 | 22 | - name: Install linting tools 23 | shell: bash --noprofile --norc -x -eo pipefail {0} 24 | run: | 25 | go install honnef.co/go/tools/cmd/staticcheck@latest 26 | go install github.com/client9/misspell/cmd/misspell@latest 27 | 28 | - name: Lint 29 | shell: bash --noprofile --norc -x -eo pipefail {0} 30 | run: | 31 | PATH=$PATH:$GOPATH/bin 32 | GO_LIST=$(go list ./... | grep -F -e asciigraph -v) 33 | $(exit $(go fmt $GO_LIST | wc -l)) 34 | go vet -composites=false $GO_LIST 35 | find . -type f -name "*.go" | grep -F -e asciigraph -v | xargs misspell -error -locale US 36 | staticcheck -f stylish $GO_LIST 37 | 38 | test: 39 | name: Running tests (${{ matrix.os }}/${{matrix.go}}) 40 | needs: lint 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | go: [ "1.23", "1.24" ] 45 | os: 46 | - ubuntu-latest 47 | - windows-latest 48 | 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v4 53 | 54 | - name: Setup Go 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version: ${{matrix.go}} 58 | 59 | - name: Build 60 | run: | 61 | cd nats 62 | go build 63 | 64 | - name: Run Linux tests 65 | if: startsWith(runner.os, 'Linux') 66 | shell: bash --noprofile --norc -x -eo pipefail {0} 67 | run: | 68 | set -e 69 | go list ./... | grep -F -e asciigraph -v | xargs go test -v --failfast -p=1 70 | set +e 71 | 72 | - name: Run Windows tests 73 | if: startsWith(runner.os, 'Windows') 74 | shell: pwsh 75 | run: | 76 | $ErrorActionPreference = "Stop" 77 | function Invoke-LoggedCommand { 78 | param([string]$command) 79 | Invoke-Expression $command 80 | if ($LASTEXITCODE -ne 0) { 81 | Write-Error "x Command failed: $command" 82 | exit $LASTEXITCODE 83 | } 84 | } 85 | 86 | go list ./... | ForEach-Object{ 87 | if ($_ -notmatch 'asciigraph') { 88 | Invoke-LoggedCommand "go test -v --failfast -p=1 $_" 89 | } 90 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | 26 | # Eclipse 27 | .project 28 | 29 | # IntelliJ 30 | .idea/ 31 | 32 | # Emacs 33 | *~ 34 | \#*\# 35 | .\#* 36 | 37 | # vscode 38 | .vscode 39 | 40 | # Mac 41 | .DS_Store 42 | 43 | # coverage 44 | coverage.out 45 | 46 | # Cross compiled binaries 47 | pkg 48 | 49 | # bin 50 | dist 51 | natscli 52 | nats/nats 53 | nats/main 54 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: natscli 2 | version: 2 3 | 4 | release: 5 | github: 6 | owner: nats-io 7 | name: natscli 8 | name_template: "Release {{.Version}}" 9 | draft: true 10 | prerelease: auto 11 | 12 | changelog: 13 | disable: true 14 | 15 | builds: 16 | - main: ./nats 17 | id: nats 18 | binary: nats 19 | env: 20 | - GO111MODULE=on 21 | - CGO_ENABLED=0 22 | goos: 23 | - darwin 24 | - linux 25 | - windows 26 | - freebsd 27 | goarch: 28 | - amd64 29 | - arm 30 | - arm64 31 | - 386 32 | - s390x 33 | goarm: 34 | - 6 35 | - 7 36 | ignore: 37 | - goos: freebsd 38 | goarch: arm 39 | - goos: freebsd 40 | goarch: arm64 41 | - goos: freebsd 42 | goarch: 386 43 | 44 | archives: 45 | - name_template: "nats-{{.Version}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}" 46 | wrap_in_directory: true 47 | formats: 48 | - zip 49 | files: 50 | - README.md 51 | - LICENSE 52 | 53 | checksum: 54 | name_template: "SHA256SUMS" 55 | algorithm: sha256 56 | 57 | brews: 58 | - name: nats 59 | directory: Formula 60 | repository: 61 | owner: nats-io 62 | name: homebrew-nats-tools 63 | token: "{{ .Env.GITHUB_APP_TOKEN }}" 64 | url_template: "https://github.com/nats-io/natscli/releases/download/{{ .Tag }}/nats-{{.Version}}-{{ .Os }}-{{ .Arch }}{{if .Arm}}{{.Arm}}{{end}}.zip" 65 | homepage: "https://github.com/nats-io/natscli" 66 | description: "NATS utility" 67 | skip_upload: auto 68 | test: | 69 | system "#{bin}/nats --version" 70 | install: | 71 | bin.install "nats" 72 | generate_completions_from_executable(bin/"nats", shells: [:bash, :zsh], shell_parameter_format: "--completion-script-") 73 | 74 | nfpms: 75 | - file_name_template: 'nats-{{.Version}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}' 76 | homepage: https://nats.io 77 | description: NATS Utility 78 | maintainer: R.I. Pienaar 79 | license: Apache 2.0 80 | vendor: Synadia Inc. 81 | bindir: /usr/local/bin 82 | formats: 83 | - deb 84 | - rpm 85 | -------------------------------------------------------------------------------- /ABTaskFile: -------------------------------------------------------------------------------- 1 | # Install https://choria-io.github.io/appbuilder/ and run `abt` to use this file 2 | 3 | name: dev 4 | description: Development tools 5 | 6 | commands: 7 | - name: test 8 | type: parent 9 | aliases: [t] 10 | description: Perform various tests 11 | commands: 12 | - name: unit 13 | type: exec 14 | dir: "{{ AppDir }}" 15 | description: Run unit tests 16 | aliases: [u] 17 | script: | 18 | set -e 19 | 20 | go list ./... | grep -F -e asciigraph -v |xargs go test 21 | 22 | - name: lint 23 | type: exec 24 | dir: "{{ AppDir }}" 25 | flags: 26 | - name: vet 27 | description: Perform go vet 28 | bool: true 29 | default: true 30 | - name: staticcheck 31 | description: Perform staticcheck 32 | bool: true 33 | default: true 34 | - name: spell 35 | description: Perform spell check 36 | bool: true 37 | default: true 38 | - name: update 39 | description: Updates lint dependencies 40 | bool: true 41 | script: | 42 | set -e 43 | 44 | . "{{ BashHelperPath }}" 45 | 46 | {{ if .Flags.update }} 47 | ab_say Updating linting tools 48 | go install github.com/client9/misspell/cmd/misspell@latest 49 | go install honnef.co/go/tools/cmd/staticcheck@latest 50 | {{ else }} 51 | echo ">>> Run with --update to install required commands" 52 | echo 53 | {{ end }} 54 | 55 | ab_say Formatting source files 56 | go fmt ./... 57 | 58 | ab_say Tidying go mod 59 | go mod tidy 60 | 61 | {{ if .Flags.spell }} 62 | ab_say Checking spelling 63 | find . -type f -name "*.go" | grep -F -e asciigraph -v | xargs misspell -error -locale US -i flavour 64 | {{ end }} 65 | 66 | {{ if .Flags.vet }} 67 | ab_say Performing go vet 68 | go list ./... | grep -F -e asciigraph -v |xargs go vet 69 | {{ end }} 70 | 71 | {{ if .Flags.staticcheck }} 72 | ab_say Running staticcheck 73 | go list ./... | grep -F -e asciigraph -v |xargs staticcheck 74 | {{ end }} 75 | 76 | - name: dependencies 77 | type: parent 78 | description: Manage dependencies 79 | aliases: [ d ] 80 | commands: 81 | - name: update 82 | description: Update dependencies 83 | type: exec 84 | aliases: [ up ] 85 | dir: "{{ AppDir }}" 86 | flags: 87 | - name: verbose 88 | description: Log verbosely 89 | short: v 90 | bool: true 91 | - name: proxy 92 | description: Enable using go proxy 93 | bool: true 94 | default: "true" 95 | script: | 96 | . "{{ BashHelperPath }}" 97 | 98 | ab_announce Updating all dependencies 99 | echo 100 | 101 | {{ if eq .Flags.proxy false }} 102 | export GOPROXY=direct 103 | ab_say Disabling go mod proxy 104 | {{ end }} 105 | 106 | go get -u -n -a -t {{- if .Flags.verbose }} -d -x {{ end }} ./... 107 | 108 | ab_say Running go mod tidy 109 | 110 | go mod tidy 111 | 112 | - name: report 113 | type: parent 114 | description: Report on downloads 115 | commands: 116 | - name: releases 117 | type: exec 118 | description: Report releases and their release_id for us in downloads report 119 | flags: 120 | - name: org 121 | description: The organization to fetch for 122 | default: nats-io 123 | - name: repo 124 | description: The repository to fetch for 125 | default: natscli 126 | command: curl -s https://api.github.com/repos/{{.Flags.org}}/{{.Flags.repo}}/releases 127 | transform: 128 | report: 129 | name: Releases Report 130 | header: |+1 131 | Release List 132 | ------------------------------------------------------------------------------------ 133 | body: | 134 | Name: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Tag: @<<<<<<<<<< ID: @########## 135 | row.name, row.tag_name, row.id 136 | 137 | - name: downloads 138 | type: exec 139 | flags: 140 | - name: release 141 | description: The release id to report for 142 | default: latest 143 | - name: org 144 | description: The organization to fetch for 145 | default: nats-io 146 | - name: repo 147 | description: The repository to fetch for 148 | default: natscli 149 | description: Reports on downloads for a certain release id 150 | command: curl -s https://api.github.com/repos/{{.Flags.org}}/{{.Flags.repo}}/releases/{{.Flags.release}} 151 | transform: 152 | pipeline: 153 | - report: 154 | name: Asset Report 155 | initial_query: assets 156 | header: |+ 157 | @||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| 158 | data.name 159 | ---------------------------------------------------------------------------------- 160 | 161 | body: | 162 | Name: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Size: @B###### Downloads: @#### 163 | row.name, row.size, row.download_count 164 | footer: |+2 165 | 166 | ======================= 167 | Total Downloads: @##### 168 | report.summary.download_count 169 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Community Code of Conduct 2 | 3 | NATS follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # NATS CLI Governance 2 | 3 | NATS CLI (nats) is part of the NATS project and is subject to the [NATS Governance](https://github.com/nats-io/nats-general/blob/master/GOVERNANCE.md). 4 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | Maintainership is on a per project basis. 4 | 5 | ### Maintainers 6 | - Derek Collison [@derekcollison](https://github.com/derekcollison) 7 | - Ivan Kozlovic [@kozlovic](https://github.com/kozlovic) 8 | - R.I. Pienaar [@ripienaar](https://github.com/ripienaar) 9 | 10 | -------------------------------------------------------------------------------- /cli/account_tls_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "crypto/tls" 18 | "crypto/x509" 19 | "encoding/pem" 20 | "fmt" 21 | "os" 22 | "time" 23 | 24 | "github.com/choria-io/fisk" 25 | 26 | "golang.org/x/crypto/ocsp" 27 | ) 28 | 29 | type ActTLSCmd struct { 30 | expireWarnDuration time.Duration 31 | wantOCSP bool 32 | wantPEM bool 33 | 34 | // values after here derived inside showTLS 35 | now time.Time 36 | warnIfBefore time.Time 37 | } 38 | 39 | func configureAccountTLSCommand(srv *fisk.CmdClause) { 40 | c := &ActTLSCmd{} 41 | 42 | tls := srv.Command("tls", "Report TLS chain for connected server").Action(c.showTLS) 43 | tls.Flag("expire-warn", "Warn about certs expiring this soon (1w; 0 to disable)").Default("1w").PlaceHolder("DURATION").DurationVar(&c.expireWarnDuration) 44 | tls.Flag("ocsp", "Report OCSP information, if any").UnNegatableBoolVar(&c.wantOCSP) 45 | tls.Flag("pem", "Show PEM Certificate blocks (true)").Default("true").BoolVar(&c.wantPEM) 46 | 47 | // TODO: consider NAGIOS-compatible option (output format, exit statuses) 48 | } 49 | 50 | func (c *ActTLSCmd) showTLS(_ *fisk.ParseContext) error { 51 | c.now = time.Now() 52 | if c.expireWarnDuration > 0 { 53 | c.warnIfBefore = c.now.Add(c.expireWarnDuration) 54 | } 55 | 56 | nc, _, err := prepareHelper("", natsOpts()...) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | t, err := nc.TLSConnectionState() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var showingOCSP bool 67 | if c.wantOCSP { 68 | if len(t.OCSPResponse) > 0 { 69 | showingOCSP = true 70 | } else { 71 | fmt.Printf("# No OCSP Response found in TLS connection\n\n") 72 | } 73 | } 74 | 75 | fmt.Printf("# TLS Verified Chains count: %d\n", len(t.VerifiedChains)) 76 | if len(t.VerifiedChains) < 1 { 77 | return fmt.Errorf("no verified chains found in TLS") 78 | } 79 | 80 | err = nil 81 | for i := range t.VerifiedChains { 82 | fmt.Printf("\n# chain: %d\n", i+1) 83 | if chainErr := c.showOneTLSChain(t.VerifiedChains[i], i+1); chainErr != nil && err == nil { 84 | err = chainErr 85 | } 86 | if showingOCSP { 87 | if ocspErr := c.showOneOCSP(t.VerifiedChains[i], i+1, t); ocspErr != nil && err == nil { 88 | err = ocspErr 89 | } 90 | } 91 | } 92 | return err 93 | } 94 | 95 | func (c *ActTLSCmd) showOneTLSChain(chain []*x509.Certificate, chain_number int) error { 96 | var err error 97 | for ci, cert := range chain { 98 | fmt.Printf("# chain=%d cert=%d isCA=%v Subject=%q\n", chain_number, ci+1, cert.IsCA, cert.Subject.String()) 99 | if cert.NotAfter.Before(c.now) { 100 | // I don't think this should happen because we're for verified chains, but protect against being called on an unverified chain. 101 | fmt.Printf("# EXPIRED after %v\n", cert.NotAfter) 102 | if err == nil { 103 | err = fmt.Errorf("expired cert chain=%d cert=%d expiration=%q subject=%q", chain_number, ci+1, cert.NotAfter, cert.Subject.String()) 104 | } 105 | } else if cert.NotAfter.Before(c.warnIfBefore) { 106 | fmt.Printf("# EXPIRING SOON: within %v of %v\n", c.expireWarnDuration, cert.NotAfter) 107 | if err == nil { 108 | err = fmt.Errorf("cert expiring soon chain=%d cert=%d expiration=%q subject=%q", chain_number, ci+1, cert.NotAfter, cert.Subject.String()) 109 | } 110 | } 111 | // Always show expiration in this form, even if already shown, to have a stable grep pattern 112 | fmt.Printf("# Expiration: %s\n", cert.NotAfter) 113 | if len(cert.DNSNames) > 0 { 114 | fmt.Printf("# SAN: DNS Names: %v\n", cert.DNSNames) 115 | } 116 | if len(cert.IPAddresses) > 0 { 117 | fmt.Printf("# SAN: IP Addresses: %v\n", cert.IPAddresses) 118 | } 119 | if len(cert.URIs) > 0 { 120 | fmt.Printf("# SAN: URIs: %v\n", cert.URIs) 121 | } 122 | if len(cert.EmailAddresses) > 0 { 123 | fmt.Printf("# SAN: Email Addresses: %v\n", cert.EmailAddresses) 124 | } 125 | fmt.Printf("# Serial: %v\n# Signed-with: %v\n", cert.SerialNumber, cert.SignatureAlgorithm) 126 | if c.wantPEM { 127 | pem.Encode(os.Stdout, &pem.Block{ 128 | Type: "CERTIFICATE", 129 | Bytes: cert.Raw, 130 | }) 131 | } 132 | } 133 | return err 134 | } 135 | 136 | func (c *ActTLSCmd) showOneOCSP(chain []*x509.Certificate, chain_number int, cs tls.ConnectionState) error { 137 | if len(chain) < 2 { 138 | fmt.Printf("\n# Skipping OCSP verification for solo end-entity cert chain\n") 139 | return nil 140 | } 141 | 142 | liveStaple, err := ocsp.ParseResponseForCert(cs.OCSPResponse, chain[0], chain[1]) 143 | if err != nil { 144 | errContext := fmt.Sprintf("OCSP response invalid for chain %d's %q from %q", chain_number, chain[0].Subject, chain[1].Subject) 145 | fmt.Printf("\n# %s: %v\n", errContext, err) 146 | return fmt.Errorf("%s: %w", errContext, err) 147 | } 148 | 149 | switch liveStaple.Status { 150 | case ocsp.Good: 151 | fmt.Printf("\n# OCSP: GOOD status=%v sn=%v producedAt=(%s) thisUpdate=(%s) nextUpdate=(%s)\n", 152 | liveStaple.Status, liveStaple.SerialNumber, 153 | liveStaple.ProducedAt, liveStaple.ThisUpdate, liveStaple.NextUpdate) 154 | case ocsp.Revoked: 155 | fmt.Printf("\n# OCSP: REVOKED status=%v RevokedAt=(%s)\n", liveStaple.Status, liveStaple.RevokedAt) 156 | default: 157 | fmt.Printf("\n# OCSP: BAD status=%v sn=%v\n", liveStaple.Status, liveStaple.SerialNumber) 158 | } 159 | 160 | // should we return an error for OCSP bad/revoked status? 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /cli/audit_checks_command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/choria-io/fisk" 9 | "github.com/nats-io/jsm.go/audit" 10 | iu "github.com/nats-io/natscli/internal/util" 11 | ) 12 | 13 | type auditChecksCommand struct { 14 | json bool 15 | } 16 | 17 | func configureAuditChecksCommand(app *fisk.CmdClause) { 18 | c := &auditChecksCommand{} 19 | 20 | checks := app.Command("checks", "List configured audit checks").Alias("ls").Action(c.checksAction) 21 | checks.Flag("json", "Produce JSON output").UnNegatableBoolVar(&c.json) 22 | } 23 | 24 | func (c *auditChecksCommand) checksAction(_ *fisk.ParseContext) error { 25 | collection, err := audit.NewDefaultCheckCollection() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | var checks []*audit.Check 31 | collection.EachCheck(func(c *audit.Check) { 32 | checks = append(checks, c) 33 | }) 34 | 35 | if c.json { 36 | return iu.PrintJSON(checks) 37 | } 38 | 39 | tbl := iu.NewTableWriter(opts(), "Audit Checks") 40 | tbl.AddHeaders("Suite", "Code", "Description", "Configuration") 41 | 42 | for _, check := range checks { 43 | var cfgKeys []string 44 | for _, cfg := range check.Configuration { 45 | switch cfg.Unit { 46 | case audit.PercentageUnit: 47 | cfgKeys = append(cfgKeys, fmt.Sprintf("%s (%s%%)", cfg.Key, f(int(cfg.Default)))) 48 | case audit.IntUnit, audit.UIntUnit: 49 | cfgKeys = append(cfgKeys, fmt.Sprintf("%s (%s)", cfg.Key, f(cfg.Default))) 50 | default: 51 | cfgKeys = append(cfgKeys, fmt.Sprintf("%s (%s)", cfg.Key, f(cfg.Default))) 52 | } 53 | } 54 | sort.Strings(cfgKeys) 55 | 56 | tbl.AddRow(check.Suite, check.Code, check.Description, strings.Join(cfgKeys, ", ")) 57 | } 58 | 59 | fmt.Println(tbl.Render()) 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cli/audit_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | func configureAuditCommand(app commandHost) { 17 | audit := app.Command("audit", "Audit a NATS deployment") 18 | 19 | configureAuditAnalyzeCommand(audit) 20 | configureAuditChecksCommand(audit) 21 | configureAuditGatherCommand(audit) 22 | } 23 | 24 | func init() { 25 | registerCommand("audit", 19, configureAuditCommand) 26 | } 27 | -------------------------------------------------------------------------------- /cli/audit_gather_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "github.com/nats-io/jsm.go/api" 18 | gatherer "github.com/nats-io/jsm.go/audit/gather" 19 | 20 | "github.com/choria-io/fisk" 21 | ) 22 | 23 | type auditGatherCmd struct { 24 | progress bool 25 | config *gatherer.Configuration 26 | } 27 | 28 | func configureAuditGatherCommand(app *fisk.CmdClause) { 29 | c := &auditGatherCmd{ 30 | config: gatherer.NewCaptureConfiguration(), 31 | } 32 | 33 | gather := app.Command("gather", "capture a variety of data from a deployment into an archive file").Alias("capture").Alias("cap").Action(c.gather) 34 | gather.Flag("output", "output file path of generated archive").Short('o').StringVar(&c.config.TargetPath) 35 | gather.Flag("progress", "Display progress messages during gathering").Default("true").BoolVar(&c.progress) 36 | gather.Flag("server-endpoints", "Capture monitoring endpoints for each server").Default("true").BoolVar(&c.config.Include.ServerEndpoints) 37 | gather.Flag("server-profiles", "Capture profiles for each server").Default("true").BoolVar(&c.config.Include.ServerProfiles) 38 | gather.Flag("account-endpoints", "Capture monitoring endpoints for each account").Default("true").BoolVar(&c.config.Include.AccountEndpoints) 39 | gather.Flag("streams", "Capture state of each stream").Default("true").BoolVar(&c.config.Include.Streams) 40 | gather.Flag("consumers", "Capture state of each stream consumers").Default("true").BoolVar(&c.config.Include.Consumers) 41 | gather.Flag("details", "Capture detailed server information from the audit").Default("true").BoolVar(&c.config.Detailed) 42 | } 43 | 44 | func (c *auditGatherCmd) gather(_ *fisk.ParseContext) error { 45 | nc, err := newNatsConn("", natsOpts()...) 46 | if err != nil { 47 | return err 48 | } 49 | defer nc.Close() 50 | 51 | switch { 52 | case opts().Trace: 53 | c.config.LogLevel = api.TraceLevel 54 | case c.progress: 55 | c.config.LogLevel = api.InfoLevel 56 | default: 57 | c.config.LogLevel = api.ErrorLevel 58 | } 59 | 60 | c.config.Timeout = opts().Timeout 61 | 62 | return gatherer.Gather(nc, c.config) 63 | } 64 | -------------------------------------------------------------------------------- /cli/auth_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | func configureAuthCommand(app commandHost) { 17 | auth := app.Command("auth", "NATS Decentralized Authentication") 18 | addCheat("auth", auth) 19 | 20 | // todo: 21 | // - Improve maintaining pub/sub permissions for a user, perhaps allow interactive edits of yaml? 22 | 23 | auth.HelpLong("WARNING: This is experimental and subject to change, do not use yet for production deployment. ") 24 | 25 | configureAuthOperatorCommand(auth) 26 | configureAuthAccountCommand(auth) 27 | configureAuthUserCommand(auth) 28 | configureAuthNkeyCommand(auth) 29 | } 30 | 31 | func init() { 32 | registerCommand("auth", 0, configureAuthCommand) 33 | } 34 | -------------------------------------------------------------------------------- /cli/auth_xkey_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "crypto/rand" 18 | "io" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/nats-io/nkeys" 24 | ) 25 | 26 | func TestSealUnseal(t *testing.T) { 27 | tDir := t.TempDir() 28 | // Create two pairs of xkeys 29 | ef := rand.Reader 30 | p1, err := nkeys.CreateCurveKeysWithRand(ef) 31 | if err != nil { 32 | t.Error("Failed to create key") 33 | t.FailNow() 34 | } 35 | p2, err := nkeys.CreateCurveKeysWithRand(ef) 36 | if err != nil { 37 | t.Error("Failed to create key") 38 | t.FailNow() 39 | } 40 | p1_seed, _ := p1.Seed() 41 | p2_seed, _ := p2.Seed() 42 | 43 | // Setup all the test files 44 | p1_key, err := os.Create(filepath.Join(tDir, "p1_seed")) 45 | if err != nil { 46 | t.Error("Failed to create test key file") 47 | t.FailNow() 48 | } 49 | defer p1_key.Close() 50 | defer os.RemoveAll(filepath.Join(tDir, "p1_seed")) 51 | err = os.WriteFile(filepath.Join(tDir, "p1_seed"), p1_seed, 0644) 52 | if err != nil { 53 | t.Error("Failed to write test key to file") 54 | t.FailNow() 55 | } 56 | 57 | p2_key, err := os.Create(filepath.Join(tDir, "p2_seed")) 58 | if err != nil { 59 | t.Error("Failed to create test key file") 60 | t.FailNow() 61 | } 62 | defer p2_key.Close() 63 | defer os.RemoveAll(filepath.Join(tDir, "p2_seed")) 64 | err = os.WriteFile(filepath.Join(tDir, "p2_seed"), p2_seed, 0644) 65 | if err != nil { 66 | t.Error("Failed to write test key to file") 67 | t.FailNow() 68 | } 69 | 70 | message, err := os.Create(filepath.Join(tDir, "message.txt")) 71 | if err != nil { 72 | t.Error("Failed to create test key file") 73 | t.FailNow() 74 | } 75 | defer message.Close() 76 | defer os.RemoveAll(filepath.Join(tDir, "message.txt")) 77 | err = os.WriteFile(filepath.Join(tDir, "message.txt"), []byte("test"), 0644) 78 | if err != nil { 79 | t.Error("Failed to write test key to file") 80 | t.FailNow() 81 | } 82 | 83 | // Get both public keys 84 | p1_pub, _ := p1.PublicKey() 85 | p2_pub, _ := p2.PublicKey() 86 | 87 | // Setup fisk for Seal Test 88 | c := &authNKCommand{} 89 | c.useB64 = true 90 | c.counterpartKey = p2_pub 91 | c.dataFile = filepath.Join(tDir, "message.txt") 92 | c.outFile = filepath.Join(tDir, "message.enc") 93 | c.keyFile = filepath.Join(tDir, "p1_seed") 94 | 95 | err = c.sealAction(nil) 96 | if err != nil { 97 | t.Error("Failed to seal message: " + err.Error()) 98 | t.FailNow() 99 | } 100 | 101 | // Setup fisk for Open Test 102 | c = &authNKCommand{} 103 | c.counterpartKey = p1_pub 104 | c.useB64 = true 105 | c.dataFile = filepath.Join(tDir, "message.enc") 106 | c.keyFile = filepath.Join(tDir, "p2_seed") 107 | 108 | // Redirect stdout to capture decrypted output 109 | stdout := os.Stdout 110 | r, w, _ := os.Pipe() 111 | os.Stdout = w 112 | 113 | err = c.unsealAction(nil) 114 | if err != nil { 115 | t.Error("Failed to unseal message: " + err.Error()) 116 | t.FailNow() 117 | } 118 | 119 | w.Close() 120 | out, _ := io.ReadAll(r) 121 | os.Stdout = stdout 122 | 123 | // Test if decrypted output is correct 124 | if string(out) != "test\n" { 125 | t.Fail() 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cli/cheats/account.md: -------------------------------------------------------------------------------- 1 | # To view account information and connection 2 | nats account info 3 | 4 | # To report connections for your command 5 | nats account report connections 6 | 7 | # To backup all JetStream streams 8 | nats account backup /path/to/backup --check 9 | -------------------------------------------------------------------------------- /cli/cheats/auth.md: -------------------------------------------------------------------------------- 1 | # Basic steps for setting up decentralized authentication 2 | All configuration changes are stored locally until `nats auth account push`ed to a nats cluster. 3 | 4 | # Create a new operator and set as working context 5 | nats auth operator add sysopp 6 | 7 | # Generate a template server configuration file from an operator 8 | nats server generate server.conf 9 | 10 | # Create a new account 11 | nats auth account add MyAccount 12 | 13 | # Create a new user in an account 14 | nats auth user add MyUser 15 | 16 | # Create an admin user in system account 17 | nats auth user add admin SYSTEM 18 | 19 | # Export credentials for a user 20 | nats auth user credential sys_admin.cred admin SYSTEM 21 | 22 | # Push an account or its changes from a specific operator to a specific server, using system account credentials. 23 | nats auth account push MyAccount --server nats://localhost:4222 --operator sysopp --creds sys_admin.cred 24 | 25 | # Use `nats context` and `nats auth operator select` to set defaults 26 | nats context add sysadmin --description "System Account" --server nats://localhost:4222 --creds sys_admin.cred 27 | 28 | nats auth operator select sysopp 29 | 30 | # Push account with default settings 31 | nats auth account push MyAccount 32 | 33 | 34 | -------------------------------------------------------------------------------- /cli/cheats/bench.md: -------------------------------------------------------------------------------- 1 | # benchmark core nats publish with 10 publishers on subject foo 2 | nats bench pub foo --clients 10 --msgs 10000 --size 512 3 | 4 | # benchmark core nats subscribe for 4 clients on subject foo 5 | nats bench sub foo --clients 5 --msgs 10000 6 | 7 | # benchmark core nats request-reply with queuing 8 | ## run 4 clients servicing requests 9 | nats bench service serve --clients 4 testservice 10 | 11 | ## run 4 clients making synchronous requests on the service at subject testservice 12 | nats bench service request --clients 4 testservice --msgs 20000 13 | 14 | # benchmark JetStream asynchronously acknowledged publishing of batches of 1000 on subject foo creating the stream first 15 | nats bench js pub foo --create --batch 1000 16 | 17 | # benchmark JetStream synchronous publishing on subject foo using 10 clients and purging the stream first 18 | nats bench js pub foo --purge --batch=1 --clients=10 19 | 20 | # benchmark JetStream delivery of messages from a stream using an ephemeral ordered consumer, disabling the progress bar 21 | nats bench js ordered --no-progress 22 | 23 | # benchmark JetStream delivery of messages from a stream through a durable consumer shared by 4 clients using the Consume() (callback) method. 24 | nats bench js consume --clients 4 25 | 26 | # benchmark JetStream delivery of messages from a stream through a durable consumer with no acks shared by 4 clients using the fetch() method with batches of 1000. 27 | nats bench js fetch --clients 4 --acks=none --batch=1000 28 | 29 | # simulate a message processing time of 50 microseconds 30 | nats bench service serve testservice --sleep 50us 31 | 32 | # generate load by publishing messages at an interval of 100 nanoseconds rather than back to back 33 | nats bench pub foo --sleep=100ns 34 | 35 | # remember when benchmarking JetStream 36 | Once you are finished benchmarking, remember to free up the resources (i.e. memory and files) consumed by the stream using 'nats stream rm'. 37 | 38 | You can get more accurate results by disabling the progress bar using the `--no-progress` flag. -------------------------------------------------------------------------------- /cli/cheats/consumer.md: -------------------------------------------------------------------------------- 1 | # Adding, Removing, Viewing a Consumer 2 | nats consumer add 3 | nats consumer info ORDERS NEW 4 | nats consumer rm ORDERS NEW 5 | 6 | # Editing a consumer 7 | nats consumer edit ORDERS NEW --description "new description" 8 | 9 | # Get messages from a consumer 10 | nats consumer next ORDERS NEW --ack 11 | nats consumer next ORDERS NEW --no-ack 12 | nats consumer sub ORDERS NEW --ack 13 | 14 | # Force leader election on a consumer 15 | nats consumer cluster down ORDERS NEW 16 | -------------------------------------------------------------------------------- /cli/cheats/contexts.md: -------------------------------------------------------------------------------- 1 | # Create or update 2 | nats context add development --server nats.dev.example.net:4222 [other standard connection properties] 3 | nats context add ngs --description "NGS Connection in Orders Account" --nsc nsc://acme/orders/new 4 | nats context edit development [standard connection properties] 5 | 6 | # View contexts 7 | nats context ls 8 | nats context info development --json 9 | 10 | # Validate all connections are valid and that connections can be established 11 | nats context validate --connect 12 | 13 | # Select a new default context 14 | nats context select 15 | 16 | # Connecting using a context 17 | nats pub --context development subject body 18 | -------------------------------------------------------------------------------- /cli/cheats/errors.md: -------------------------------------------------------------------------------- 1 | # To look up information for error code 1000 2 | nats errors lookup 1000 3 | 4 | # To list all errors mentioning stream using regular expression matches 5 | nats errors ls stream 6 | 7 | # As a NATS Server developer edit an existing code in errors.json 8 | nats errors edit errors.json 10013 9 | 10 | # As a NATS Server developer add a new code to the errors.json, auto picking a code 11 | nats errors add errors.json 12 | -------------------------------------------------------------------------------- /cli/cheats/events.md: -------------------------------------------------------------------------------- 1 | # To view common system events 2 | nats events 3 | nats events --short --all 4 | nats events --no-srv-advisory --js-metric --js-advisory 5 | nats events --no-srv-advisory --subjects service.latency.weather 6 | -------------------------------------------------------------------------------- /cli/cheats/governor.md: -------------------------------------------------------------------------------- 1 | # to create governor with 10 slots and 1 minute timeout 2 | nats governor add cron 10 1m 3 | 4 | # to view the configuration and state 5 | nats governor view cron 6 | 7 | # to reset the governor, clearing all slots 8 | nats governor reset cron 9 | 10 | # to run long-job.sh when a slot is available, giving up after 20 minutes without a slot 11 | nats governor run cron $(hostname -f) --max-wait 20m long-job.sh' 12 | -------------------------------------------------------------------------------- /cli/cheats/kv.md: -------------------------------------------------------------------------------- 1 | # to create a replicated KV bucket 2 | nats kv add CONFIG --replicas 3 3 | 4 | # to store a value in the bucket 5 | nats kv put CONFIG username bob 6 | 7 | # to read just the value with no additional details 8 | nats kv get CONFIG username --raw 9 | 10 | # view an audit trail for a key if history is kept 11 | nats kv history CONFIG username 12 | 13 | # to see the bucket status 14 | nats kv status CONFIG 15 | 16 | # observe real time changes for an entire bucket 17 | nats kv watch CONFIG 18 | # observe real time changes for all keys below users 19 | nats kv watch CONFIG 'users.>'' 20 | 21 | # create a bucket backup for CONFIG into backups/CONFIG 22 | nats kv status CONFIG 23 | nats stream backup backups/CONFIG 24 | 25 | # restore a bucket from a backup 26 | nats stream restore backups/CONFIG 27 | 28 | # list known buckets 29 | nats kv ls 30 | -------------------------------------------------------------------------------- /cli/cheats/latency.md: -------------------------------------------------------------------------------- 1 | # To test latency between 2 servers 2 | nats latency --server srv1.example.net:4222 --server-b srv2.example.net:4222 --duration 10s 3 | -------------------------------------------------------------------------------- /cli/cheats/obj.md: -------------------------------------------------------------------------------- 1 | # to create a replicated bucket 2 | nats obj add FILES --replicas 3 3 | 4 | # store a file in the bucket 5 | nats obj put FILES image.jpg 6 | 7 | # store contents of STDIN in the bucket 8 | cat x.jpg|nats obj put FILES --name image.jpg 9 | 10 | # retrieve a file from a bucket 11 | nats obj get FILES image.jpg -O out.jpg 12 | 13 | # delete a file 14 | nats obj del FILES image.jpg 15 | 16 | # delete a bucket 17 | nats obj del FILES 18 | 19 | # view bucket info 20 | nats obj info FILES 21 | 22 | # view file info 23 | nats obj info FILES image.jpg 24 | 25 | # list known buckets 26 | nats obj ls 27 | 28 | # view all files in a bucket 29 | nats obj ls FILES 30 | 31 | # prevent further modifications to the bucket 32 | nats obj seal FILES 33 | 34 | # create a bucket backup for FILES into backups/FILES 35 | nats obj status FILES 36 | nats stream backup backups/FILES 37 | 38 | # restore a bucket from a backup 39 | nats stream restore backups/FILES 40 | -------------------------------------------------------------------------------- /cli/cheats/pub.md: -------------------------------------------------------------------------------- 1 | # To publish 100 messages with a random body between 100 and 1000 characters 2 | nats pub destination.subject "{{ Random 100 1000 }}" -H Count:{{ Count }} --count 100 3 | 4 | # To publish messages from STDIN 5 | echo "hello world" | nats pub destination.subject 6 | 7 | # To publish messages from STDIN in a headless (non-tty) context 8 | echo "hello world" | nats pub --force-stdin destination.subject 9 | 10 | # To request a response from a server and show just the raw result 11 | nats request destination.subject "hello world" -H "Content-type:text/plain" --raw 12 | 13 | # To listen on STDIN and publish one message per newline 14 | nats pub destination.subject --send-on=newline -------------------------------------------------------------------------------- /cli/cheats/reply.md: -------------------------------------------------------------------------------- 1 | # To set up a responder that runs an external command with the 3rd subject token as argument 2 | nats reply "service.requests.>" --command "service.sh {{2}}" 3 | 4 | # To set up basic responder 5 | nats reply service.requests "Message {{Count}} @ {{Time}}" 6 | nats reply service.requests --echo --sleep 10 7 | -------------------------------------------------------------------------------- /cli/cheats/schemas.md: -------------------------------------------------------------------------------- 1 | # To see all available schemas using regular expressions 2 | nats schema search 'response|request' 3 | 4 | # To view a specific schema 5 | nats schema info io.nats.jetstream.api.v1.stream_msg_get_request --yaml 6 | 7 | # To validate a JSON input against a specific schema 8 | nats schema validate io.nats.jetstream.api.v1.stream_msg_get_request request.json 9 | -------------------------------------------------------------------------------- /cli/cheats/server.md: -------------------------------------------------------------------------------- 1 | # To see all servers, including their server ID and show a response graph 2 | nats server ping --id --graph --user system 3 | 4 | # To see information about a specific server 5 | nats server info nats1.example.net --user system 6 | nats server info NCAXNST2VH7QGBVYBEDQGX73GMBXTWXACUTMQPTNKWLOYG2ES67NMX6M --user system 7 | 8 | # To list all servers and show basic summaries, expecting responses from 10 servers 9 | nats server list 10 --user system 10 | 11 | # To report on current connections 12 | nats server report connections 13 | nats server report connz --account WEATHER 14 | nats server report connz --sort in-msgs 15 | nats server report connz --top 10 --sort in-msgs 16 | 17 | # To limit connections report to surveyor connections and all from a specific IP using https://expr.medv.io/docs/Language-Definition 18 | nats server report connz --filter 'lower(conns.name) matches "surveyor" || conns.ip == "46.101.44.80"' 19 | 20 | # To report on accounts 21 | nats server report accounts 22 | nats server report accounts --account WEATHER --sort in-msgs --top 10 23 | 24 | # To report on JetStream usage by account WEATHER 25 | nats server report jetstream --account WEATHER --sort cluster 26 | 27 | # To generate a NATS Server bcrypt command 28 | nats server password 29 | nats server pass -p 'W#OZwVN-UjMb8nszwvT2LQ' 30 | nats server pass -g 31 | PASSWORD='W#OZwVN-UjMb8nszwvT2LQ' nats server pass 32 | 33 | # To request raw monitoring data from servers 34 | nats server request subscriptions --detail --filter-account WEATHER --cluster EAST 35 | nats server req variables --name nats1.example.net 36 | nats server req connections --filter-state open 37 | nats server req connz --subscriptions --name nats1.example.net 38 | nats server req gateways --filter-name EAST 39 | nats server req leafnodes --subscriptions 40 | nats server req accounts --account WEATHER 41 | nats server req jsz --leader 42 | 43 | # To manage JetStream cluster RAFT membership 44 | nats server raft step-down 45 | -------------------------------------------------------------------------------- /cli/cheats/stream.md: -------------------------------------------------------------------------------- 1 | # Adding, Removing, Viewing a Stream 2 | nats stream add 3 | nats stream info STREAMNAME 4 | nats stream rm STREAMNAME 5 | 6 | # Editing a single property of a stream 7 | nats stream edit STREAMNAME --description "new description" 8 | # Editing a stream configuration in your editor 9 | EDITOR=vi nats stream edit -i STREAMNAME 10 | 11 | # Show a list of streams, including basic info or compatible with pipes 12 | nats stream list 13 | nats stream list -n 14 | 15 | # Find all empty streams or streams with messages 16 | nats stream find --empty 17 | nats stream find --empty --invert 18 | 19 | # Creates a new Stream based on the config of another, does not copy data 20 | nats stream copy ORDERS ARCHIVE --description "Orders Archive" --subjects ARCHIVE 21 | 22 | # Get message 12344, delete a message, delete all messages 23 | nats stream get ORDERS 12345 24 | nats stream rmm ORDERS 12345 25 | 26 | # Purge messages from streams 27 | nats stream purge ORDERS 28 | # deletes up to, but not including, 1000 29 | nats stream purge ORDERS --seq 1000 30 | nats stream purge ORDERS --keep 100 31 | nats stream purge ORDERS --subject one.subject 32 | 33 | # Page through a stream 34 | nats stream view ORDERS 35 | nats stream view --id 1000 36 | nats stream view --since 1h 37 | nats stream view --subject one.subject 38 | 39 | # Backup and restore 40 | nats stream backup ORDERS backups/orders/$(date +%Y-%m-%d) 41 | nats stream restore ORDERS backups/orders/$(date +%Y-%m-%d) 42 | 43 | # Marks a stream as read only 44 | nats stream seal ORDERS 45 | 46 | # Force a cluster leader election 47 | nats stream cluster ORDERS down 48 | 49 | # Evict the stream from a node 50 | stream cluster peer-remove ORDERS nats1.example.net 51 | -------------------------------------------------------------------------------- /cli/cheats/sub.md: -------------------------------------------------------------------------------- 1 | # To subscribe to messages, in a queue group and acknowledge any JetStream ones 2 | nats sub source.subject --queue work --ack 3 | 4 | # To subscribe to a randomly generated inbox 5 | nats sub --inbox 6 | 7 | # To dump all messages to files, 1 file per message 8 | nats sub --inbox --dump /tmp/archive 9 | 10 | # To process all messages using xargs 1 message at a time through a shell command 11 | nats sub subject --dump=- | xargs -0 -n 1 -I "{}" sh -c "echo '{}' | wc -c" 12 | 13 | # To receive new messages received in a stream with the subject ORDERS.new 14 | nats sub ORDERS.new --next 15 | 16 | # To report the number of subjects with message and byte count. The default `--report-top` is 10 17 | nats sub ">" --report-subjects --report-top=20 18 | 19 | # To base64 decode message bodies before rendering them 20 | nats sub 'encoded.sub' --translate "base64 -d" 21 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "context" 18 | "embed" 19 | "github.com/nats-io/natscli/options" 20 | glog "log" 21 | "sort" 22 | "sync" 23 | "time" 24 | 25 | "github.com/choria-io/fisk" 26 | ) 27 | 28 | type command struct { 29 | Name string 30 | Order int 31 | Command func(app commandHost) 32 | } 33 | 34 | type commandHost interface { 35 | Command(name string, help string) *fisk.CmdClause 36 | } 37 | 38 | // Logger provides a pluggable logger implementation 39 | type Logger interface { 40 | Printf(format string, a ...any) 41 | Fatalf(format string, a ...any) 42 | Print(a ...any) 43 | Fatal(a ...any) 44 | Println(a ...any) 45 | } 46 | 47 | var ( 48 | commands = []*command{} 49 | mu sync.Mutex 50 | Version = "development" 51 | log Logger 52 | ctx context.Context 53 | 54 | //go:embed cheats 55 | fs embed.FS 56 | 57 | // These are persisted by contexts, as properties thereof. 58 | // So don't include NATS_CONTEXT in this list. 59 | overrideEnvVars = []string{"NATS_URL", "NATS_USER", "NATS_PASSWORD", "NATS_CREDS", "NATS_NKEY", "NATS_CERT", "NATS_KEY", "NATS_CA", "NATS_TIMEOUT", "NATS_SOCKS_PROXY", "NATS_COLOR"} 60 | ) 61 | 62 | func registerCommand(name string, order int, c func(app commandHost)) { 63 | mu.Lock() 64 | commands = append(commands, &command{name, order, c}) 65 | mu.Unlock() 66 | } 67 | 68 | // SkipContexts used during tests 69 | var SkipContexts bool 70 | 71 | func SetVersion(v string) { 72 | mu.Lock() 73 | defer mu.Unlock() 74 | 75 | Version = v 76 | } 77 | 78 | // SetLogger sets a custom logger to use 79 | func SetLogger(l Logger) { 80 | mu.Lock() 81 | defer mu.Unlock() 82 | 83 | log = l 84 | } 85 | 86 | // SetContext sets the context to use 87 | func SetContext(c context.Context) { 88 | mu.Lock() 89 | defer mu.Unlock() 90 | 91 | ctx = c 92 | } 93 | 94 | func commonConfigure(cmd commandHost, cliOpts *options.Options, disable ...string) error { 95 | if cliOpts != nil { 96 | options.DefaultOptions = cliOpts 97 | } else { 98 | options.DefaultOptions = &options.Options{ 99 | Timeout: 5 * time.Second, 100 | } 101 | } 102 | 103 | if options.DefaultOptions.PrometheusNamespace == "" { 104 | options.DefaultOptions.PrometheusNamespace = "nats_server_check" 105 | } 106 | 107 | ctx = context.Background() 108 | log = goLogger{} 109 | 110 | sort.Slice(commands, func(i int, j int) bool { 111 | return commands[i].Name < commands[j].Name 112 | }) 113 | 114 | shouldEnable := func(name string) bool { 115 | for _, d := range disable { 116 | if d == name { 117 | return false 118 | } 119 | } 120 | 121 | return true 122 | } 123 | 124 | for _, c := range commands { 125 | if shouldEnable(c.Name) { 126 | c.Command(cmd) 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // ConfigureInCommand attaches the cli commands to cmd, prepare will load the context on demand and should be true unless override nats, 134 | // manager and js context is given in a custom PreAction in the caller. Disable is a list of command names to skip. 135 | func ConfigureInCommand(cmd *fisk.CmdClause, cliOpts *options.Options, prepare bool, disable ...string) (*options.Options, error) { 136 | err := commonConfigure(cmd, cliOpts, disable...) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | if prepare { 142 | cmd.PreAction(preAction) 143 | } 144 | 145 | return options.DefaultOptions, nil 146 | } 147 | 148 | // ConfigureInApp attaches the cli commands to app, prepare will load the context on demand and should be true unless override nats, 149 | // manager and js context is given in a custom PreAction in the caller. Disable is a list of command names to skip. 150 | func ConfigureInApp(app *fisk.Application, cliOpts *options.Options, prepare bool, disable ...string) (*options.Options, error) { 151 | err := commonConfigure(app, cliOpts, disable...) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | if prepare { 157 | app.PreAction(preAction) 158 | } 159 | 160 | return options.DefaultOptions, nil 161 | } 162 | 163 | func preAction(_ *fisk.ParseContext) (err error) { 164 | loadContext(true) 165 | return nil 166 | } 167 | 168 | type goLogger struct{} 169 | 170 | func (goLogger) Fatalf(format string, a ...any) { glog.Fatalf(format, a...) } 171 | func (goLogger) Printf(format string, a ...any) { glog.Printf(format, a...) } 172 | func (goLogger) Print(a ...any) { glog.Print(a...) } 173 | func (goLogger) Println(a ...any) { glog.Println(a...) } 174 | func (goLogger) Fatal(a ...any) { glog.Fatal(a...) } 175 | 176 | func opts() *options.Options { 177 | return options.DefaultOptions 178 | } 179 | -------------------------------------------------------------------------------- /cli/columns.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "github.com/dustin/go-humanize" 18 | "github.com/nats-io/natscli/columns" 19 | ) 20 | 21 | func newColumns(heading string, a ...any) *columns.Writer { 22 | w := columns.New(heading, a...) 23 | w.SetColorScheme(opts().Config.ColorScheme()) 24 | w.SetHeading(heading, a...) 25 | 26 | return w 27 | } 28 | 29 | func fiBytes(v uint64) string { 30 | return humanize.IBytes(v) 31 | } 32 | 33 | func f(v any) string { 34 | return columns.F(v) 35 | } 36 | 37 | func fFloat2Int(v any) string { 38 | return columns.F(uint64(v.(float64))) 39 | } 40 | 41 | func fiBytesFloat2Int(v any) string { 42 | return fiBytes(uint64(v.(float64))) 43 | } 44 | -------------------------------------------------------------------------------- /cli/jsonschema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2022 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | 20 | "github.com/nats-io/jsm.go/api" 21 | "github.com/santhosh-tekuri/jsonschema/v5" 22 | ) 23 | 24 | type SchemaValidator struct{} 25 | 26 | func (v SchemaValidator) ValidateStruct(data any, schemaType string) (ok bool, errs []string) { 27 | s, err := api.Schema(schemaType) 28 | if err != nil { 29 | return false, []string{fmt.Sprintf("unknown schema type %s", schemaType)} 30 | } 31 | sch, err := jsonschema.CompileString("schema.json", string(s)) 32 | if err != nil { 33 | return false, []string{fmt.Sprintf("could not load schema %s: %s", s, err)} 34 | } 35 | 36 | // it only accepts basic primitives so we have to specifically convert to any 37 | var d any 38 | dj, err := json.Marshal(data) 39 | if err != nil { 40 | return false, []string{fmt.Sprintf("could not serialize data: %s", err)} 41 | } 42 | err = json.Unmarshal(dj, &d) 43 | if err != nil { 44 | return false, []string{fmt.Sprintf("could not de-serialize data: %s", err)} 45 | } 46 | 47 | err = sch.Validate(d) 48 | if err != nil { 49 | if verr, ok := err.(*jsonschema.ValidationError); ok { 50 | for _, e := range verr.BasicOutput().Errors { 51 | if e.KeywordLocation == "" || e.Error == "oneOf failed" || e.Error == "allOf failed" { 52 | continue 53 | } 54 | 55 | if e.InstanceLocation == "" { 56 | errs = append(errs, e.Error) 57 | } else { 58 | errs = append(errs, fmt.Sprintf("%s: %s", e.InstanceLocation, e.Error)) 59 | } 60 | } 61 | return false, errs 62 | } else { 63 | return false, []string{fmt.Sprintf("could not validate: %s", err)} 64 | } 65 | } 66 | 67 | return true, nil 68 | } 69 | -------------------------------------------------------------------------------- /cli/jsonschema_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "testing" 18 | "time" 19 | 20 | "github.com/nats-io/jsm.go/api" 21 | ) 22 | 23 | type mockValidator interface { 24 | Validate(...api.StructValidator) (bool, []string) 25 | } 26 | 27 | func validateExpectSuccess(t *testing.T, cfg mockValidator) { 28 | t.Helper() 29 | 30 | ok, errs := cfg.Validate(new(SchemaValidator)) 31 | if !ok { 32 | t.Fatalf("expected success but got: %v", errs) 33 | } 34 | } 35 | 36 | func validateExpectFailure(t *testing.T, cfg mockValidator) { 37 | t.Helper() 38 | 39 | ok, errs := cfg.Validate(new(SchemaValidator)) 40 | if ok { 41 | t.Fatalf("expected success but got: %v", errs) 42 | } 43 | } 44 | 45 | func TestStreamConfiguration(t *testing.T) { 46 | reset := func() api.StreamConfig { 47 | return api.StreamConfig{ 48 | Name: "BASIC", 49 | Subjects: []string{"BASIC"}, 50 | Retention: api.LimitsPolicy, 51 | MaxConsumers: -1, 52 | MaxAge: 0, 53 | MaxBytes: -1, 54 | MaxMsgs: -1, 55 | Storage: api.FileStorage, 56 | Replicas: 1, 57 | } 58 | } 59 | 60 | cfg := reset() 61 | validateExpectSuccess(t, cfg) 62 | 63 | // invalid names 64 | cfg = reset() 65 | cfg.Name = "X.X" 66 | validateExpectFailure(t, cfg) 67 | 68 | // valid subject 69 | cfg = reset() 70 | cfg.Subjects = []string{"bob"} 71 | validateExpectSuccess(t, cfg) 72 | 73 | // invalid retention 74 | cfg.Retention = 10 75 | validateExpectFailure(t, cfg) 76 | 77 | // max consumers >= -1 78 | cfg = reset() 79 | cfg.MaxConsumers = -2 80 | validateExpectFailure(t, cfg) 81 | cfg.MaxConsumers = 10 82 | validateExpectSuccess(t, cfg) 83 | 84 | // max messages >= -1 85 | cfg = reset() 86 | cfg.MaxMsgs = -2 87 | validateExpectFailure(t, cfg) 88 | cfg.MaxMsgs = 10 89 | validateExpectSuccess(t, cfg) 90 | 91 | // max bytes >= -1 92 | cfg = reset() 93 | cfg.MaxBytes = -2 94 | validateExpectFailure(t, cfg) 95 | cfg.MaxBytes = 10 96 | validateExpectSuccess(t, cfg) 97 | 98 | // max age >= 0 99 | cfg = reset() 100 | cfg.MaxAge = -1 101 | validateExpectFailure(t, cfg) 102 | cfg.MaxAge = time.Second 103 | validateExpectSuccess(t, cfg) 104 | 105 | // max msg size >= -1 106 | cfg = reset() 107 | cfg.MaxMsgSize = -2 108 | validateExpectFailure(t, cfg) 109 | cfg.MaxMsgSize = 10 110 | validateExpectSuccess(t, cfg) 111 | 112 | // storage is valid 113 | cfg = reset() 114 | cfg.Storage = 10 115 | validateExpectFailure(t, cfg) 116 | 117 | // num replicas > 0 118 | cfg = reset() 119 | cfg.Replicas = -1 120 | validateExpectFailure(t, cfg) 121 | cfg.Replicas = 0 122 | validateExpectFailure(t, cfg) 123 | } 124 | 125 | func TestStreamTemplateConfiguration(t *testing.T) { 126 | reset := func() api.StreamTemplateConfig { 127 | return api.StreamTemplateConfig{ 128 | Name: "BASIC_T", 129 | MaxStreams: 10, 130 | Config: &api.StreamConfig{ 131 | Name: "BASIC", 132 | Subjects: []string{"BASIC"}, 133 | Retention: api.LimitsPolicy, 134 | MaxConsumers: -1, 135 | MaxAge: 0, 136 | MaxBytes: -1, 137 | MaxMsgs: -1, 138 | Storage: api.FileStorage, 139 | Replicas: 1, 140 | }, 141 | } 142 | } 143 | 144 | cfg := reset() 145 | validateExpectSuccess(t, cfg) 146 | 147 | cfg.Name = "" 148 | validateExpectFailure(t, cfg) 149 | 150 | // should also validate config 151 | cfg = reset() 152 | cfg.Config.Storage = 10 153 | validateExpectFailure(t, cfg) 154 | 155 | // unlimited managed streams 156 | cfg = reset() 157 | cfg.MaxStreams = 0 158 | validateExpectSuccess(t, cfg) 159 | } 160 | 161 | func TestConsumerConfiguration(t *testing.T) { 162 | reset := func() api.ConsumerConfig { 163 | return api.ConsumerConfig{ 164 | DeliverPolicy: api.DeliverAll, 165 | AckPolicy: api.AckExplicit, 166 | ReplayPolicy: api.ReplayInstant, 167 | } 168 | } 169 | 170 | cfg := reset() 171 | validateExpectSuccess(t, cfg) 172 | 173 | // durable name 174 | cfg = reset() 175 | cfg.Durable = "bob.bob" 176 | validateExpectFailure(t, cfg) 177 | 178 | // last policy 179 | cfg = reset() 180 | cfg.DeliverPolicy = api.DeliverLast 181 | validateExpectSuccess(t, cfg) 182 | 183 | // new policy 184 | cfg = reset() 185 | cfg.DeliverPolicy = api.DeliverNew 186 | validateExpectSuccess(t, cfg) 187 | 188 | // start sequence policy 189 | cfg = reset() 190 | cfg.DeliverPolicy = api.DeliverByStartSequence 191 | cfg.OptStartSeq = 10 192 | validateExpectSuccess(t, cfg) 193 | cfg.OptStartSeq = 0 194 | validateExpectFailure(t, cfg) 195 | 196 | // start time policy 197 | cfg = reset() 198 | ts := time.Now() 199 | cfg.DeliverPolicy = api.DeliverByStartTime 200 | cfg.OptStartTime = &ts 201 | validateExpectSuccess(t, cfg) 202 | cfg.OptStartTime = nil 203 | validateExpectFailure(t, cfg) 204 | 205 | // ack policy 206 | cfg = reset() 207 | cfg.AckPolicy = 10 208 | validateExpectFailure(t, cfg) 209 | cfg.AckPolicy = api.AckExplicit 210 | validateExpectSuccess(t, cfg) 211 | cfg.AckPolicy = api.AckAll 212 | validateExpectSuccess(t, cfg) 213 | cfg.AckPolicy = api.AckNone 214 | validateExpectSuccess(t, cfg) 215 | 216 | // replay policy 217 | cfg = reset() 218 | cfg.ReplayPolicy = 10 219 | validateExpectFailure(t, cfg) 220 | cfg.ReplayPolicy = api.ReplayInstant 221 | validateExpectSuccess(t, cfg) 222 | cfg.ReplayPolicy = api.ReplayOriginal 223 | validateExpectSuccess(t, cfg) 224 | } 225 | -------------------------------------------------------------------------------- /cli/plugins_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "github.com/choria-io/fisk" 19 | "github.com/nats-io/natscli/plugins" 20 | ) 21 | 22 | type pluginsCmd struct { 23 | name string 24 | command string 25 | force bool 26 | } 27 | 28 | func configurePluginCommand(app commandHost) { 29 | c := &pluginsCmd{} 30 | 31 | cmd := app.Command("plugins", "Manage plugins").Hidden() 32 | 33 | register := cmd.Commandf("register", "Registers a new plugin").Action(c.registerAction) 34 | register.Arg("name", "The top level name to register the command as").Required().StringVar(&c.name) 35 | register.Arg("command", "The command the provides the plugins").Required().ExistingFileVar(&c.command) 36 | register.Flag("force", "Overwrite existing plugins").UnNegatableBoolVar(&c.force) 37 | } 38 | 39 | func init() { 40 | registerCommand("plugins", 18, configurePluginCommand) 41 | } 42 | 43 | func (c *pluginsCmd) registerAction(_ *fisk.ParseContext) error { 44 | fmt.Println("WARNING: Plugins support is experimental and not officially supported") 45 | fmt.Println() 46 | 47 | err := plugins.Register(c.name, c.command, c.force) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | fmt.Printf("Plugin %s using command %s was added or updated\n", c.name, c.command) 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /cli/rtt_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "crypto/tls" 18 | "fmt" 19 | iu "github.com/nats-io/natscli/internal/util" 20 | "net" 21 | "net/url" 22 | "strings" 23 | "time" 24 | 25 | "github.com/choria-io/fisk" 26 | "github.com/nats-io/nats.go" 27 | ) 28 | 29 | type rttCmd struct { 30 | iterations int 31 | json bool 32 | } 33 | 34 | type rttResult struct { 35 | Time time.Time `json:"time"` 36 | Address string `json:"address"` 37 | RTT time.Duration `json:"rtt"` 38 | URL string `json:"url"` 39 | } 40 | 41 | type rttTarget struct { 42 | URL string `json:"url"` 43 | Results []*rttResult `json:"results"` 44 | tlsName string 45 | } 46 | 47 | func configureRTTCommand(app commandHost) { 48 | c := &rttCmd{} 49 | 50 | rtt := app.Command("rtt", "Compute round-trip time to NATS server").Action(c.rtt) 51 | rtt.Arg("iterations", "How many round trips to do when testing").Default("5").IntVar(&c.iterations) 52 | rtt.Flag("json", "Produce JSON output").Short('j').UnNegatableBoolVar(&c.json) 53 | } 54 | 55 | func init() { 56 | registerCommand("rtt", 13, configureRTTCommand) 57 | } 58 | 59 | func (c *rttCmd) rtt(_ *fisk.ParseContext) error { 60 | targets, err := c.targets() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = c.performTest(targets) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if c.json { 71 | iu.PrintJSON(targets) 72 | 73 | return nil 74 | } 75 | 76 | f := fmt.Sprintf("%%%ds: %%v\n", c.calcIndent(targets, 3)) 77 | 78 | for _, t := range targets { 79 | fmt.Printf("%s:\n\n", t.URL) 80 | 81 | for _, r := range t.Results { 82 | fmt.Printf(f, r.Address, r.RTT) 83 | } 84 | 85 | fmt.Println() 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (c *rttCmd) calcIndent(targets []*rttTarget, prefix int) int { 92 | i := prefix 93 | 94 | for _, t := range targets { 95 | for _, r := range t.Results { 96 | p := len(r.Address) + prefix 97 | 98 | if p > i { 99 | i = p 100 | } 101 | } 102 | } 103 | 104 | return i 105 | } 106 | 107 | func (c *rttCmd) performTest(targets []*rttTarget) (err error) { 108 | for _, target := range targets { 109 | opts := natsOpts() 110 | if target.tlsName != "" { 111 | opts = append(opts, func(o *nats.Options) error { 112 | if o.TLSConfig == nil { 113 | o.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12, ServerName: target.tlsName} 114 | } else { 115 | o.TLSConfig.ServerName = target.tlsName 116 | } 117 | 118 | return nil 119 | }) 120 | } 121 | 122 | for _, r := range target.Results { 123 | r.Time = time.Now() 124 | r.URL, r.RTT, err = c.calcRTT(r.Address, opts) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (c *rttCmd) calcRTT(server string, copts []nats.Option) (string, time.Duration, error) { 135 | opts().Conn = nil 136 | 137 | if opts().Trace { 138 | log.Printf(">>> Connecting to %s\n", server) 139 | } 140 | 141 | nc, err := newNatsConn(server, copts...) 142 | if err != nil { 143 | return "", 0, err 144 | } 145 | defer nc.Close() 146 | 147 | time.Sleep(25 * time.Millisecond) 148 | 149 | var totalTime time.Duration 150 | 151 | if opts().Trace { 152 | fmt.Printf("RTT iterations for server: %s\n", server) 153 | } 154 | for i := 1; i <= c.iterations; i++ { 155 | rtt, err := nc.RTT() 156 | if err != nil { 157 | return "", 0, fmt.Errorf("rtt failed: %v", err) 158 | } 159 | 160 | totalTime += rtt 161 | if opts().Trace { 162 | fmt.Printf("#%d:\trtt=%s\n", i, rtt) 163 | if i == c.iterations { 164 | fmt.Println() 165 | } 166 | } 167 | } 168 | 169 | return nc.ConnectedUrl(), totalTime / time.Duration(c.iterations), nil 170 | } 171 | 172 | func (c *rttCmd) targets() (targets []*rttTarget, err error) { 173 | servers := "" 174 | if opts().Conn != nil { 175 | servers = strings.Join(opts().Conn.DiscoveredServers(), ",") 176 | } else if opts().Config != nil { 177 | servers = opts().Config.ServerURL() 178 | } else { 179 | return nil, fmt.Errorf("cannot find a server list to test") 180 | } 181 | 182 | for _, s := range strings.Split(servers, ",") { 183 | if !strings.Contains(s, "://") { 184 | s = fmt.Sprintf("nats://%s", s) 185 | } 186 | 187 | u, err := url.Parse(s) 188 | if err != nil { 189 | return targets, err 190 | } 191 | 192 | port := u.Port() 193 | if port == "" { 194 | port = "4222" 195 | } 196 | 197 | targets = append(targets, &rttTarget{URL: u.String()}) 198 | target := targets[len(targets)-1] 199 | 200 | // its a ip just add it 201 | if net.ParseIP(u.Hostname()) != nil { 202 | target.Results = append(target.Results, &rttResult{Address: fmt.Sprintf("%s://%s", u.Scheme, net.JoinHostPort(u.Hostname(), port))}) 203 | continue 204 | } 205 | 206 | // else look it up and add all its addresses 207 | addrs, _ := net.LookupHost(u.Hostname()) 208 | if len(addrs) == 0 { 209 | target.Results = append(target.Results, &rttResult{Address: fmt.Sprintf("%s://%s", u.Scheme, net.JoinHostPort(u.Hostname(), port))}) 210 | continue 211 | } 212 | 213 | // if we have many addresses we'll connect to each IP but we have to use the 214 | // name of the original server address to do validate TLS, connect here, check it 215 | // requires TLS and store the name to use when connecting to each IP 216 | target.tlsName = u.Hostname() 217 | 218 | for _, a := range addrs { 219 | target.Results = append(target.Results, &rttResult{Address: fmt.Sprintf("%s://%s", u.Scheme, net.JoinHostPort(a, port))}) 220 | } 221 | } 222 | 223 | return targets, nil 224 | } 225 | -------------------------------------------------------------------------------- /cli/schema_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | func configureSchemaCommand(app commandHost) { 17 | schema := app.Command("schema", "Schema tools") 18 | addCheat("schemas", schema) 19 | 20 | configureSchemaSearchCommand(schema) 21 | configureSchemaInfoCommand(schema) 22 | configureSchemaValidateCommand(schema) 23 | configureSchemaReqCommand(schema) 24 | } 25 | 26 | func init() { 27 | registerCommand("schema", 14, configureSchemaCommand) 28 | } 29 | -------------------------------------------------------------------------------- /cli/schema_info_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | 19 | "github.com/choria-io/fisk" 20 | "github.com/ghodss/yaml" 21 | "github.com/nats-io/jsm.go/api" 22 | ) 23 | 24 | type schemaInfoCmd struct { 25 | schema string 26 | yaml bool 27 | } 28 | 29 | func configureSchemaInfoCommand(schema *fisk.CmdClause) { 30 | c := &schemaInfoCmd{} 31 | info := schema.Command("info", "Display schema contents").Alias("show").Alias("view").Action(c.info) 32 | info.Arg("schema", "Schema ID to show").Required().StringVar(&c.schema) 33 | info.Flag("yaml", "Produce YAML format output").UnNegatableBoolVar(&c.yaml) 34 | } 35 | 36 | func (c *schemaInfoCmd) info(_ *fisk.ParseContext) error { 37 | schema, err := api.Schema(c.schema) 38 | if err != nil { 39 | return fmt.Errorf("could not load schema %q: %s", c.schema, err) 40 | } 41 | 42 | if c.yaml { 43 | schema, err = yaml.JSONToYAML(schema) 44 | if err != nil { 45 | return fmt.Errorf("could not reformat schema as YAML: %s", err) 46 | } 47 | } 48 | 49 | fmt.Println(string(schema)) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cli/schema_req_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "github.com/choria-io/fisk" 20 | "github.com/nats-io/jsm.go/api" 21 | ) 22 | 23 | type schemaReqCmd struct { 24 | subject string 25 | body string 26 | schema string 27 | dump bool 28 | } 29 | 30 | func configureSchemaReqCommand(schema *fisk.CmdClause) { 31 | c := &schemaReqCmd{} 32 | 33 | req := schema.Command("request", "Request and validate data from a NATS service").Alias("req").Action(c.requestAction) 34 | req.Arg("subject", "The subject to send a request to").Required().StringVar(&c.subject) 35 | req.Arg("body", "The body to send").Default(`{}`).StringVar(&c.body) 36 | req.Flag("schema", "The schema identifier to validate against").StringVar(&c.schema) 37 | req.Flag("show", "Show the received data").UnNegatableBoolVar(&c.dump) 38 | } 39 | 40 | func (c *schemaReqCmd) requestAction(_ *fisk.ParseContext) error { 41 | nc, err := newNatsConn("", natsOpts()...) 42 | if err != nil { 43 | return err 44 | } 45 | defer nc.Close() 46 | 47 | res, err := nc.Request(c.subject, []byte(c.body), opts().Timeout) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if c.dump { 53 | var d any 54 | err := json.Unmarshal(res.Data, &d) 55 | if err != nil { 56 | return err 57 | } 58 | j, err := json.MarshalIndent(d, "", " ") 59 | if err != nil { 60 | return err 61 | } 62 | fmt.Println(string(j)) 63 | fmt.Println() 64 | } 65 | 66 | schemaType, msg, err := api.ParseMessage(res.Data) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if schemaType == "io.nats.unknown_message" { 72 | return fmt.Errorf("could not determine the message type") 73 | } 74 | 75 | if c.schema != "" && schemaType != c.schema { 76 | return fmt.Errorf("invalid message type %s", schemaType) 77 | } 78 | 79 | ok, errs := validator().ValidateStruct(msg, schemaType) 80 | if !ok { 81 | fmt.Printf("Message did not pass validation against %s\n\n", schemaType) 82 | for _, err := range errs { 83 | fmt.Printf(" %s\n", err) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | fmt.Printf("Response is a valid %s message\n", schemaType) 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /cli/schema_search_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | iu "github.com/nats-io/natscli/internal/util" 19 | "strings" 20 | 21 | "github.com/choria-io/fisk" 22 | "github.com/nats-io/jsm.go/api" 23 | ) 24 | 25 | type schemaSearchCmd struct { 26 | filter string 27 | json bool 28 | } 29 | 30 | func configureSchemaSearchCommand(schema *fisk.CmdClause) { 31 | c := &schemaSearchCmd{} 32 | search := schema.Command("search", "Search schemas using a pattern").Alias("find").Alias("list").Alias("ls").Action(c.search) 33 | search.Arg("pattern", "Regular expression to search for").Default(".").StringVar(&c.filter) 34 | search.Flag("json", "Produce JSON format output").UnNegatableBoolVar(&c.json) 35 | } 36 | 37 | func (c *schemaSearchCmd) search(_ *fisk.ParseContext) error { 38 | found, err := api.SchemaSearch(c.filter) 39 | if err != nil { 40 | return fmt.Errorf("search failed: %s", err) 41 | } 42 | 43 | if c.json { 44 | iu.PrintJSON(found) 45 | return nil 46 | } 47 | 48 | if len(found) == 0 { 49 | fmt.Printf("No schemas matched %q\n", c.filter) 50 | return nil 51 | } 52 | 53 | fmt.Printf("Matched Schemas:\n\n %s\n", strings.Join(found, "\n ")) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /cli/schema_validate_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2022 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | iu "github.com/nats-io/natscli/internal/util" 20 | "io" 21 | "os" 22 | "strings" 23 | 24 | "github.com/choria-io/fisk" 25 | ) 26 | 27 | type schemaValidateCmd struct { 28 | schema string 29 | file string 30 | json bool 31 | } 32 | 33 | func configureSchemaValidateCommand(schema *fisk.CmdClause) { 34 | c := &schemaValidateCmd{} 35 | 36 | validate := schema.Command("validate", "Validates a JSON file against a schema").Alias("check").Action(c.validate) 37 | validate.Arg("schema", "Schema ID to validate against").Required().StringVar(&c.schema) 38 | validate.Arg("file", "JSON data to validate (- for stdin)").Required().StringVar(&c.file) 39 | validate.Flag("json", "Produce JSON format output").UnNegatableBoolVar(&c.json) 40 | } 41 | 42 | func (c *schemaValidateCmd) validate(_ *fisk.ParseContext) error { 43 | var f io.ReadCloser 44 | var err error 45 | 46 | if c.file == "-" { 47 | f = os.Stdin 48 | } else { 49 | f, err = os.Open(c.file) 50 | if err != nil { 51 | return err 52 | } 53 | defer f.Close() 54 | } 55 | 56 | file, err := io.ReadAll(f) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | var data any 62 | err = json.Unmarshal(file, &data) 63 | if err != nil { 64 | return fmt.Errorf("could not parse JSON data in %q: %s", c.file, err) 65 | } 66 | 67 | ok, errs := new(SchemaValidator).ValidateStruct(data, c.schema) 68 | if c.json { 69 | if errs == nil { 70 | errs = []string{} 71 | } 72 | iu.PrintJSON(errs) 73 | return nil 74 | } 75 | 76 | if ok { 77 | fmt.Printf("%s validates against %s\n", c.file, c.schema) 78 | return nil 79 | } 80 | 81 | fmt.Printf("Validation errors in %s:\n\n", c.file) 82 | fmt.Printf(" %s\n", strings.Join(errs, "\n ")) 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /cli/server_check_exporter_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | 20 | "github.com/choria-io/fisk" 21 | "github.com/nats-io/natscli/internal/exporter" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | ) 25 | 26 | func (c *SrvCheckCmd) exporterAction(_ *fisk.ParseContext) error { 27 | exp, err := exporter.NewExporter(opts().PrometheusNamespace, c.exporterConfigFile) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | prometheus.MustRegister(exp) 33 | http.Handle("/metrics", promhttp.Handler()) 34 | 35 | if c.exporterCertificate != "" && c.exporterKey != "" { 36 | log.Printf("NATS CLI Prometheus Exporter listening on https://0.0.0.0:%da/metrics", c.exporterPort) 37 | return http.ListenAndServeTLS(fmt.Sprintf(":%d", c.exporterPort), c.exporterCertificate, c.exporterKey, nil) 38 | } else { 39 | log.Printf("NATS CLI Prometheus Exporter listening on http://0.0.0.0:%d/metrics", c.exporterPort) 40 | return http.ListenAndServe(fmt.Sprintf(":%d", c.exporterPort), nil) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cli/server_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | func configureServerCommand(app commandHost) { 17 | srv := app.Command("server", "Server information").Alias("srv").Alias("sys").Alias("system") 18 | addCheat("server", srv) 19 | 20 | configureServerAccountCommand(srv) 21 | configureServerCheckCommand(srv) 22 | configureServerClusterCommand(srv) 23 | configureServerConfigCommand(srv) 24 | configureServerGenerateCommand(srv) 25 | configureServerGraphCommand(srv) 26 | configureServerInfoCommand(srv) 27 | configureServerListCommand(srv) 28 | configureServerMappingCommand(srv) 29 | configureServerPasswdCommand(srv) 30 | configureServerPingCommand(srv) 31 | configureServerReportCommand(srv) 32 | configureServerRequestCommand(srv) 33 | configureServerRunCommand(srv) 34 | configureServerWatchCommand(srv) 35 | configureStreamCheckCommand(srv) 36 | configureConsumerCheckCommand(srv) 37 | } 38 | 39 | func init() { 40 | registerCommand("server", 15, configureServerCommand) 41 | } 42 | -------------------------------------------------------------------------------- /cli/server_config_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | 20 | "github.com/choria-io/fisk" 21 | "github.com/nats-io/nats-server/v2/server" 22 | ) 23 | 24 | type SrvConfigCmd struct { 25 | serverID string 26 | force bool 27 | } 28 | 29 | func configureServerConfigCommand(srv *fisk.CmdClause) { 30 | c := SrvConfigCmd{} 31 | 32 | cfg := srv.Command("config", "Interact with server configuration") 33 | 34 | reload := cfg.Command("reload", "Reloads the runtime configuration").Action(c.reloadAction) 35 | reload.Arg("id", "The server ID to trigger a reload for").Required().StringVar(&c.serverID) 36 | reload.Flag("force", "Force reload without prompting").Short('f').BoolVar(&c.force) 37 | } 38 | 39 | func (c *SrvConfigCmd) reloadAction(pc *fisk.ParseContext) error { 40 | nc, err := newNatsConn("", natsOpts()...) 41 | if err != nil { 42 | return err 43 | } 44 | defer nc.Close() 45 | 46 | if !c.force { 47 | resps, err := doReq(nil, fmt.Sprintf("$SYS.REQ.SERVER.%s.VARZ", c.serverID), 1, nc) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if len(resps) != 1 { 53 | return fmt.Errorf("invalid response from %d servers", len(resps)) 54 | } 55 | vz := server.ServerAPIResponse{} 56 | err = json.Unmarshal(resps[0], &vz) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | ok, err := askConfirmation(fmt.Sprintf("Really reload configuration for %s (%s) on %s", vz.Server.Name, vz.Server.ID, vz.Server.Host), false) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if !ok { 67 | return nil 68 | } 69 | } 70 | 71 | resps, err := doReq(nil, fmt.Sprintf("$SYS.REQ.SERVER.%s.RELOAD", c.serverID), 1, nc) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if len(resps) != 1 { 77 | return fmt.Errorf("invalid response from %d servers", len(resps)) 78 | } 79 | 80 | nfo := &SrvInfoCmd{id: c.serverID} 81 | return nfo.info(pc) 82 | } 83 | -------------------------------------------------------------------------------- /cli/server_generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "net/url" 19 | "sort" 20 | "strings" 21 | 22 | "github.com/AlecAivazis/survey/v2" 23 | "github.com/choria-io/fisk" 24 | au "github.com/nats-io/natscli/internal/auth" 25 | "github.com/nats-io/natscli/internal/scaffold" 26 | iu "github.com/nats-io/natscli/internal/util" 27 | ) 28 | 29 | type serverGenerateCmd struct { 30 | source string 31 | target string 32 | } 33 | 34 | func configureServerGenerateCommand(srv *fisk.CmdClause) { 35 | c := &serverGenerateCmd{} 36 | 37 | gen := srv.Command("generate", `Generate server configurations`).Alias("gen").Action(c.generateAction) 38 | gen.Arg("target", "Write the output to a specific location").Required().StringVar(&c.target) 39 | gen.Flag("source", "Fetch the configuration bundle from a file or URL").StringVar(&c.source) 40 | } 41 | 42 | func (c *serverGenerateCmd) generateAction(_ *fisk.ParseContext) error { 43 | var b *scaffold.Bundle 44 | var err error 45 | 46 | if iu.FileExists(c.target) { 47 | return fmt.Errorf("target directory %s already exist", c.target) 48 | } 49 | 50 | fmt.Println("This tool generates NATS Server configurations based on a question and answer") 51 | fmt.Println("form-based approach and then renders the result into a directory.") 52 | fmt.Println() 53 | fmt.Println("It supports rendering local bundles compiled into the 'nats' command but can also") 54 | fmt.Println("fetch and render remote ones using a URL.") 55 | fmt.Println() 56 | 57 | switch { 58 | case c.source == "": 59 | err = c.pickEmbedded() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fallthrough 65 | 66 | case strings.Contains(c.source, "://"): 67 | var uri *url.URL 68 | uri, err = url.Parse(c.source) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if uri.Scheme == "" { 74 | return fmt.Errorf("invalid URL %q", c.source) 75 | } 76 | 77 | b, err = scaffold.FromUrl(uri) 78 | 79 | case iu.IsDirectory(c.source): 80 | b, err = scaffold.FromDir(c.source) 81 | 82 | default: 83 | b, err = scaffold.FromFile(c.source) 84 | } 85 | if err != nil { 86 | return err 87 | } 88 | 89 | if b.Requires.Operator { 90 | auth, err := au.GetAuthBuilder() 91 | if err != nil { 92 | return err 93 | } 94 | if len(auth.Operators().List()) == 0 { 95 | return fmt.Errorf("no operator found") 96 | } 97 | } 98 | 99 | env := map[string]any{ 100 | "_target": c.target, 101 | "_source": c.source, 102 | } 103 | 104 | err = b.Run(c.target, env, opts().Trace) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | return b.Close() 110 | } 111 | 112 | func (c *serverGenerateCmd) pickEmbedded() error { 113 | list := map[string]string{ 114 | "Development Super Cluster using Docker Compose": "fs:///natsbuilder", 115 | "'nats auth' managed NATS Server configuration": "fs:///operator", 116 | "'nats auth' managed NATS Cluster in Kubernetes": "fs:///operatork8s", 117 | "Synadia Cloud Leafnode Configuration": "fs:///ngsleafnodeconfig", 118 | } 119 | 120 | names := []string{} 121 | for k := range list { 122 | names = append(names, k) 123 | } 124 | sort.Strings(names) 125 | 126 | choice := "" 127 | err := iu.AskOne(&survey.Select{ 128 | Message: "Select a template", 129 | Options: names, 130 | PageSize: iu.SelectPageSize(len(names)), 131 | }, &choice) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | c.source = list[choice] 137 | 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /cli/server_mapping_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "github.com/nats-io/natscli/internal/util" 19 | 20 | "github.com/AlecAivazis/survey/v2" 21 | "github.com/choria-io/fisk" 22 | "github.com/nats-io/nats-server/v2/server" 23 | ) 24 | 25 | type SrvMappingCmd struct { 26 | src string 27 | dest string 28 | subj string 29 | } 30 | 31 | func configureServerMappingCommand(srv *fisk.CmdClause) { 32 | c := &SrvMappingCmd{} 33 | 34 | m := srv.Command("mappings", "Test subject mapping patterns").Alias("mapping").Action(c.mappingAction) 35 | m.Arg("source", "Source subject pattern").StringVar(&c.src) 36 | m.Arg("dest", "Destination subject pattern").StringVar(&c.dest) 37 | m.Arg("subject", "Subject to transform").StringVar(&c.subj) 38 | } 39 | 40 | func (c *SrvMappingCmd) mappingAction(_ *fisk.ParseContext) error { 41 | if c.src == "" { 42 | err := util.AskOne(&survey.Input{ 43 | Message: "Source subject pattern", 44 | Help: "The pattern matching source subjects", 45 | }, &c.src, survey.WithValidator(survey.Required)) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | 51 | if c.dest == "" { 52 | err := util.AskOne(&survey.Input{ 53 | Message: "Destination subject pattern", 54 | Help: "The pattern matching describing the mapping to test", 55 | }, &c.dest, survey.WithValidator(survey.Required)) 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | 61 | trans, err := server.NewSubjectTransform(c.src, c.dest) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | transAndShow := func(trans server.SubjectTransformer, subj string) { 67 | s, err := trans.Match(subj) 68 | if err != nil { 69 | fmt.Printf("Error: %v\n", err) 70 | } 71 | 72 | fmt.Println(s) 73 | fmt.Println() 74 | } 75 | 76 | if c.subj != "" { 77 | transAndShow(trans, c.subj) 78 | return nil 79 | } 80 | 81 | fmt.Println("Enter subjects to test, empty subject terminates.") 82 | fmt.Println() 83 | fmt.Println("NOTE: This only tests mappings, it does not add them to the server") 84 | fmt.Println() 85 | 86 | for { 87 | c.subj = "" 88 | err = util.AskOne(&survey.Input{ 89 | Message: "Subject", 90 | Help: "Enter a subject that matching source and the mapping will be shown", 91 | }, &c.subj) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if c.subj == "" { 97 | break 98 | } 99 | 100 | transAndShow(trans, c.subj) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /cli/server_mkpasswd_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "github.com/AlecAivazis/survey/v2" 19 | "github.com/choria-io/fisk" 20 | iu "github.com/nats-io/natscli/internal/util" 21 | "golang.org/x/crypto/bcrypt" 22 | ) 23 | 24 | type SrvPasswdCmd struct { 25 | pass string 26 | cost uint 27 | generate bool 28 | } 29 | 30 | func configureServerPasswdCommand(srv *fisk.CmdClause) { 31 | c := &SrvPasswdCmd{} 32 | 33 | passwd := srv.Command("passwd", "Creates encrypted passwords for use in NATS Server").Alias("mkpasswd").Alias("pass").Alias("password").Action(c.mkpasswd) 34 | passwd.Flag("pass", "The password to encrypt (PASSWORD)").Short('p').Envar("PASSWORD").StringVar(&c.pass) 35 | passwd.Flag("cost", "The cost to use in the bcrypt argument").Short('c').Default("11").UintVar(&c.cost) 36 | passwd.Flag("generate", "Generates a secure passphrase and encrypt it").Short('g').UnNegatableBoolVar(&c.generate) 37 | } 38 | 39 | func (c *SrvPasswdCmd) mkpasswd(_ *fisk.ParseContext) error { 40 | if int(c.cost) < bcrypt.MinCost || int(c.cost) > bcrypt.MaxCost { 41 | return fmt.Errorf("bcrypt cost should be between %d and %d", bcrypt.MinCost, bcrypt.MaxCost) 42 | } 43 | 44 | var err error 45 | 46 | if c.pass == "" && c.generate { 47 | c.pass = iu.RandomPassword(22) 48 | fmt.Printf("Generated password: %s\n", c.pass) 49 | } else if c.pass == "" && !c.generate { 50 | c.pass, err = c.askPassword() 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | 56 | if len(c.pass) < 22 { 57 | return fmt.Errorf("password should be at least 22 characters long") 58 | } 59 | 60 | cb, err := bcrypt.GenerateFromPassword([]byte(c.pass), int(c.cost)) 61 | if err != nil { 62 | return fmt.Errorf("error producing bcrypt hash: %w", err) 63 | } 64 | 65 | if c.generate { 66 | fmt.Printf(" bcrypt hash: %s\n", string(cb)) 67 | } else { 68 | fmt.Println(string(cb)) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (c *SrvPasswdCmd) askPassword() (string, error) { 75 | bp1 := "" 76 | bp2 := "" 77 | 78 | err := iu.AskOne(&survey.Password{Message: "Enter password", Help: "Enter a password string that's minimum 22 characters long"}, &bp1) 79 | if err != nil { 80 | return "", fmt.Errorf("could not read password: %w", err) 81 | } 82 | fmt.Println() 83 | err = iu.AskOne(&survey.Password{Message: "Re-enter password", Help: "Enter the same password again"}, &bp2) 84 | if err != nil { 85 | return "", fmt.Errorf("could not read password: %w", err) 86 | } 87 | 88 | fmt.Println() 89 | 90 | if bp1 != bp2 { 91 | return "", fmt.Errorf("entered and re-entered passwords do not match") 92 | } 93 | 94 | return bp1, nil 95 | } 96 | -------------------------------------------------------------------------------- /cli/server_ping_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "os/signal" 22 | "sort" 23 | "sync" 24 | "sync/atomic" 25 | "time" 26 | 27 | "github.com/choria-io/fisk" 28 | "github.com/nats-io/nats-server/v2/server" 29 | "github.com/nats-io/nats.go" 30 | "github.com/nats-io/natscli/internal/asciigraph" 31 | ) 32 | 33 | type SrvPingCmd struct { 34 | expect uint32 35 | graph bool 36 | showId bool 37 | } 38 | 39 | func configureServerPingCommand(srv *fisk.CmdClause) { 40 | c := &SrvPingCmd{} 41 | 42 | ls := srv.Command("ping", "Ping all servers").Action(c.ping) 43 | ls.Arg("expect", "How many servers to expect").Uint32Var(&c.expect) 44 | ls.Flag("graph", "Produce a response distribution graph").UnNegatableBoolVar(&c.graph) 45 | ls.Flag("id", "Include the Server ID in the output").UnNegatableBoolVar(&c.showId) 46 | } 47 | 48 | func (c *SrvPingCmd) ping(_ *fisk.ParseContext) error { 49 | nc, err := newNatsConn("", natsOpts()...) 50 | if err != nil { 51 | return err 52 | } 53 | defer nc.Close() 54 | 55 | ctx, cancel := context.WithTimeout(ctx, opts().Timeout) 56 | 57 | seen := uint32(0) 58 | mu := &sync.Mutex{} 59 | start := time.Now() 60 | times := []float64{} 61 | 62 | sub, err := nc.Subscribe(nc.NewRespInbox(), func(msg *nats.Msg) { 63 | if msg.Header != nil && msg.Header.Get("Status") != "" { 64 | fmt.Printf("%s status from $SYS.REQ.SERVER.PING, ensure a system account is used with appropriate permissions\n", msg.Header.Get("Status")) 65 | os.Exit(1) 66 | } 67 | 68 | ssm := &server.ServerStatsMsg{} 69 | err = json.Unmarshal(msg.Data, ssm) 70 | if err != nil { 71 | log.Printf("Could not decode response: %s", err) 72 | os.Exit(1) 73 | } 74 | 75 | mu.Lock() 76 | defer mu.Unlock() 77 | 78 | last := atomic.AddUint32(&seen, 1) 79 | 80 | if c.expect == 0 && ssm.Stats.ActiveServers > 0 && last == 1 { 81 | c.expect = uint32(ssm.Stats.ActiveServers) 82 | } 83 | 84 | since := time.Since(start) 85 | rtt := since.Milliseconds() 86 | times = append(times, float64(rtt)) 87 | 88 | if c.showId { 89 | fmt.Printf("%s %-60s rtt=%s\n", ssm.Server.ID, ssm.Server.Name, since) 90 | } else { 91 | fmt.Printf("%-60s rtt=%s\n", ssm.Server.Name, since) 92 | } 93 | 94 | if last == c.expect { 95 | cancel() 96 | } 97 | }) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | err = nc.PublishRequest("$SYS.REQ.SERVER.PING", sub.Subject, nil) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | ic := make(chan os.Signal, 1) 108 | signal.Notify(ic, os.Interrupt) 109 | 110 | select { 111 | case <-ic: 112 | cancel() 113 | case <-ctx.Done(): 114 | } 115 | 116 | sub.Drain() 117 | 118 | c.summarize(times) 119 | 120 | if c.expect != 0 && c.expect != seen { 121 | fmt.Printf("\nMissing %d server(s)\n", c.expect-atomic.LoadUint32(&seen)) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (c *SrvPingCmd) summarize(times []float64) { 128 | fmt.Println() 129 | fmt.Println("---- ping statistics ----") 130 | 131 | if len(times) > 0 { 132 | sum := 0.0 133 | min := 999999.0 134 | max := -1.0 135 | avg := 0.0 136 | 137 | for _, value := range times { 138 | sum += value 139 | if value < min { 140 | min = value 141 | } 142 | if value > max { 143 | max = value 144 | } 145 | } 146 | 147 | avg = sum / float64(len(times)) 148 | 149 | fmt.Printf("%d replies max: %.2f min: %.2f avg: %.2f\n", len(times), max, min, avg) 150 | 151 | if c.graph { 152 | fmt.Println() 153 | fmt.Println(c.chart(times)) 154 | } 155 | return 156 | } 157 | 158 | fmt.Println("no responses received") 159 | } 160 | 161 | func (c *SrvPingCmd) chart(times []float64) string { 162 | sort.Float64s(times) 163 | 164 | latest := times[len(times)-1] 165 | bcount := int(latest/25) + 1 166 | buckets := make([]float64, bcount) 167 | 168 | for _, t := range times { 169 | b := t / 25.0 170 | buckets[int(b)]++ 171 | } 172 | 173 | return asciigraph.Plot( 174 | buckets, 175 | asciigraph.Height(15), 176 | asciigraph.Width(60), 177 | asciigraph.Offset(5), 178 | asciigraph.Caption("Responses per 25ms"), 179 | ) 180 | } 181 | -------------------------------------------------------------------------------- /cli/server_watch_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "github.com/choria-io/fisk" 18 | ) 19 | 20 | func configureServerWatchCommand(srv *fisk.CmdClause) { 21 | watch := srv.Command("watch", "Live views of server conditions") 22 | 23 | configureServerWatchAccountCommand(watch) 24 | configureServerWatchJSCommand(watch) 25 | configureServerWatchServerCommand(watch) 26 | } 27 | -------------------------------------------------------------------------------- /cli/top_command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "fmt" 18 | "github.com/choria-io/fisk" 19 | "github.com/nats-io/nats-server/v2/server" 20 | "github.com/nats-io/natscli/top" 21 | ui "gopkg.in/gizak/termui.v1" 22 | ) 23 | 24 | type topCmd struct { 25 | host string 26 | conns int 27 | delay int 28 | sort string 29 | lookup bool 30 | output string 31 | outputDelimiter string 32 | raw bool 33 | maxRefresh int 34 | showSubs bool 35 | } 36 | 37 | func configureTopCommand(app commandHost) { 38 | c := &topCmd{} 39 | 40 | top := app.Command("top", "Shows top-like statistic for connections on a specific server").Action(c.topAction) 41 | top.Arg("name", "The server name to gather statistics for").Required().StringVar(&c.host) 42 | top.Flag("conns", "Maximum number of connections to show").Default("1024").Short('n').IntVar(&c.conns) 43 | top.Flag("interval", "Refresh interval").Default("1").Short('d').IntVar(&c.delay) 44 | top.Flag("sort", "Sort connections by").Default("cid").EnumVar(&c.sort, "cid", "start", "subs", "pending", "msgs_to", "msgs_from", "bytes_to", "bytes_from", "last", "idle", "uptime", "stop", "reason", "rtt") 45 | top.Flag("lookup", "Looks up client addresses in DNS").Default("false").UnNegatableBoolVar(&c.lookup) 46 | top.Flag("output", "Saves the first snapshot to a file").Short('o').StringVar(&c.output) 47 | top.Flag("delimiter", "Specifies a output delimiter, defaults to grid-like text").StringVar(&c.outputDelimiter) 48 | top.Flag("raw", "Show raw bytes").Short('b').Default("false").UnNegatableBoolVar(&c.raw) 49 | top.Flag("max-refresh", "Maximum refreshes").Short('r').Default("-1").IntVar(&c.maxRefresh) 50 | top.Flag("subs", "Shows the subscriptions column").Default("false").UnNegatableBoolVar(&c.showSubs) 51 | } 52 | 53 | func init() { 54 | registerCommand("top", 17, configureTopCommand) 55 | } 56 | 57 | func (c *topCmd) topAction(_ *fisk.ParseContext) error { 58 | nc, _, err := prepareHelper("", natsOpts()...) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | engine := top.NewEngine(nc, c.host, c.conns, c.delay, opts().Trace) 64 | 65 | _, err = engine.Request("VARZ") 66 | if err != nil { 67 | return fmt.Errorf("initial test request failed: %v", err) 68 | } 69 | 70 | sortOpt := server.SortOpt(c.sort) 71 | if !sortOpt.IsValid() { 72 | return fmt.Errorf("invalid sort option: %s", c.sort) 73 | } 74 | engine.SortOpt = sortOpt 75 | engine.DisplaySubs = c.showSubs 76 | 77 | if c.output != "" { 78 | return top.SaveStatsSnapshotToFile(engine, c.output, c.outputDelimiter) 79 | } 80 | 81 | err = ui.Init() 82 | if err != nil { 83 | panic(err) 84 | } 85 | defer ui.Close() 86 | 87 | go engine.MonitorStats() 88 | 89 | top.StartUI(engine, c.lookup, c.raw, c.maxRefresh) 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /cli/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/nats-io/jsm.go/api" 20 | ) 21 | 22 | func TestRenderCluster(t *testing.T) { 23 | cluster := &api.ClusterInfo{ 24 | Name: "test", 25 | Leader: "S2", 26 | Replicas: []*api.PeerInfo{ 27 | {Name: "S3", Current: false, Active: 30199700, Lag: 882130}, 28 | {Name: "S1", Current: false, Active: 30202300, Lag: 882354}, 29 | }, 30 | } 31 | 32 | if result := renderCluster(cluster); result != "S1!, S2*, S3!" { 33 | t.Fatalf("invalid result: %s", result) 34 | } 35 | 36 | if result := renderCluster(&api.ClusterInfo{Name: "test"}); result != "" { 37 | t.Fatalf("invalid result: %q", result) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli/yaml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cli 15 | 16 | import ( 17 | "bytes" 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/santhosh-tekuri/jsonschema/v5" 22 | "gopkg.in/yaml.v3" 23 | ) 24 | 25 | func constraintsForType(sch *jsonschema.Schema) []string { 26 | var constraints []string 27 | 28 | if len(sch.Types) == 1 { 29 | switch sch.Types[0] { 30 | case "integer": 31 | if sch.Minimum != nil { 32 | constraints = append(constraints, fmt.Sprintf("min: %s", sch.Minimum.FloatString(0))) 33 | } 34 | 35 | if sch.Maximum != nil { 36 | constraints = append(constraints, fmt.Sprintf("max: %s", sch.Maximum.FloatString(0))) 37 | } 38 | case "string": 39 | if sch.MinLength > -1 { 40 | constraints = append(constraints, fmt.Sprintf("min length: %d", sch.MinLength)) 41 | } 42 | if sch.MaxLength > -1 { 43 | constraints = append(constraints, fmt.Sprintf("max length: %d", sch.MaxLength)) 44 | } 45 | if sch.Pattern != nil { 46 | constraints = append(constraints, fmt.Sprintf("pattern: %s", sch.Pattern.String())) 47 | } 48 | case "array": 49 | if sch.MinItems > -1 { 50 | constraints = append(constraints, fmt.Sprintf("min items: %d", sch.MinItems)) 51 | } 52 | 53 | if items, ok := sch.Items.(*jsonschema.Schema); ok { 54 | if len(items.Types) > 0 && items.Types[0] != "object" { 55 | constraints = append(constraints, fmt.Sprintf("items: %v", items.Types[0])) 56 | } 57 | } 58 | } 59 | } 60 | 61 | return constraints 62 | } 63 | 64 | func decorateSequenceNode(n *yaml.Node, sch *jsonschema.Schema) { 65 | if len(sch.Types) != 1 { 66 | n.HeadComment = sch.Description 67 | return 68 | } 69 | 70 | if len(sch.Types) == 1 { 71 | switch sch.Types[0] { 72 | case "array": 73 | decorateScalarNode(n, sch) 74 | 75 | if ssch, ok := sch.Items.(*jsonschema.Schema); ok { 76 | for _, c := range n.Content { 77 | if c.Kind == yaml.MappingNode { 78 | decorateMappingNode(c, ssch) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | func decorateMappingNode(node *yaml.Node, sch *jsonschema.Schema) { 87 | if len(sch.Types) != 1 { 88 | node.HeadComment = sch.Description 89 | return 90 | } 91 | 92 | parent := &jsonschema.Schema{} 93 | for _, n := range node.Content { 94 | decorateNode(n, sch, parent) 95 | } 96 | 97 | } 98 | 99 | func decorateScalarNode(n *yaml.Node, sch *jsonschema.Schema) { 100 | if len(sch.Types) != 1 { 101 | n.HeadComment = sch.Description 102 | return 103 | } 104 | 105 | if strings.Contains(sch.Comment, "nanoseconds depicting a duration") { 106 | n.HeadComment = fmt.Sprintf("\n%s\n#\n Type: duration like 10s or 2h1m5s", sch.Description) 107 | return 108 | } 109 | 110 | constraints := constraintsForType(sch) 111 | if len(constraints) == 0 { 112 | n.HeadComment = fmt.Sprintf("\n%s\n#\n Type: %v", sch.Description, sch.Types[0]) 113 | } else { 114 | n.HeadComment = fmt.Sprintf("\n%s\n#\n Type: %v (%s)", sch.Description, sch.Types[0], f(constraints)) 115 | } 116 | 117 | if sch.Comment != "" { 118 | n.HeadComment = fmt.Sprintf("%s\n Comment: %s", n.HeadComment, sch.Comment) 119 | } 120 | 121 | if len(sch.Enum) > 0 { 122 | var valid []string 123 | for _, v := range sch.Enum { 124 | valid = append(valid, fmt.Sprintf("%v", v)) 125 | } 126 | n.HeadComment = fmt.Sprintf("%s\n Valid Values: %s", n.HeadComment, f(valid)) 127 | } 128 | } 129 | 130 | func decorateNode(n *yaml.Node, sch *jsonschema.Schema, parent *jsonschema.Schema) *jsonschema.Schema { 131 | switch n.Kind { 132 | case yaml.ScalarNode: 133 | schema := sch.Properties[n.Value] 134 | if schema == nil { 135 | return nil 136 | } 137 | 138 | decorateScalarNode(n, schema) 139 | return schema 140 | 141 | case yaml.MappingNode: 142 | if parent != nil { 143 | decorateMappingNode(n, parent) 144 | } 145 | 146 | case yaml.SequenceNode: 147 | if parent != nil { 148 | decorateSequenceNode(n, parent) 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | type typedData interface { 156 | Schema() ([]byte, error) 157 | SchemaType() string 158 | } 159 | 160 | func decoratedYamlMarshal(v typedData) ([]byte, error) { 161 | var node yaml.Node 162 | 163 | sb, err := v.Schema() 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | compiler := jsonschema.NewCompiler() 169 | compiler.ExtractAnnotations = true 170 | compiler.AddResource(v.SchemaType(), bytes.NewReader(sb)) 171 | sch := compiler.MustCompile(v.SchemaType()) 172 | 173 | node.Encode(v) 174 | 175 | node.HeadComment = fmt.Sprintf("Schema: %v", v.SchemaType()) 176 | if len(sch.Required) > 0 { 177 | node.HeadComment = fmt.Sprintf("%s\n#\n Required Items: %v", node.HeadComment, f(sch.Required)) 178 | } 179 | 180 | var parent *jsonschema.Schema 181 | for _, n := range node.Content { 182 | parent = decorateNode(n, sch, parent) 183 | } 184 | 185 | return yaml.Marshal(node) 186 | } 187 | -------------------------------------------------------------------------------- /dependencies.md: -------------------------------------------------------------------------------- 1 | # External Dependencies 2 | This file lists the dependencies used in this repository. 3 | 4 | | Dependency | License | 5 | |--------------------------------------------------|-----------------------------------------| 6 | | dario.cat/mergo | BSD 3-Clause "New" or "Revised" License | 7 | | github.com/AlecAivazis/survey | MIT License | 8 | | github.com/HdrHistogram/hdrhistogram-go | MIT License | 9 | | github.com/Masterminds/goutils | Apache License 2.0 | 10 | | github.com/Masterminds/semver | MIT License | 11 | | github.com/beorn7/perks | MIT License | 12 | | github.com/cespare/xxhash | MIT License | 13 | | github.com/choria-io/fisk | MIT License | 14 | | github.com/choria-io/scaffoild | Apache License 2.0 | 15 | | github.com/dustin/go-humanize | MIT License | 16 | | github.com/emicklei/dot | MIT License | 17 | | github.com/expr-lang/expr | MIT License | 18 | | github.com/fatih/color | MIT License | 19 | | github.com/ghodss/yaml | MIT License | 20 | | github.com/google/go-cmp | BSD 3-Clause "New" or "Revised" License | 21 | | github.com/google/shlex | Apache License 2.0 | 22 | | github.com/google/uuid | BSD 3-Clause "New" or "Revised" License | 23 | | github.com/gosuri/uilive | MIT License | 24 | | github.com/gosuri/uiprogress | MIT License | 25 | | github.com/guptarohit/asciigraph | BSD 3-Clause "New" or "Revised" License | 26 | | github.com/huandu/xstrings | MIT License | 27 | | github.com/jedib0t/go-pretty | MIT License | 28 | | github.com/kballard/go-shellquote | MIT License | 29 | | github.com/klauspost/compress | BSD 3-Clause "New" or "Revised" License | 30 | | github.com/mattn/go-colorable | MIT License | 31 | | github.com/mattn/go-runewidth | MIT License | 32 | | github.com/mgutz/ansi | MIT License | 33 | | github.com/minio/highwayhash | Apache License 2.0 | 34 | | github.com/mitchellh/copystructure | MIT License | 35 | | github.com/mitchellh/go-homedir | MIT License | 36 | | github.com/mitchellh/reflectwalk | MIT License | 37 | | github.com/nats-io/jsm.go | Apache License 2.0 | 38 | | github.com/nats-io/jwt | Apache License 2.0 | 39 | | github.com/nats-io/nats-server | Apache License 2.0 | 40 | | github.com/nats-io/nats.go | Apache License 2.0 | 41 | | github.com/nats-io/nkeys | Apache License 2.0 | 42 | | github.com/nats-io/nuid | Apache License 2.0 | 43 | | github.com/nats-io/nsc | Apache License 2.0 | 44 | | github.com/nsf/termbox-go | MIT License | 45 | | github.com/prometheus/client_golang | Apache License 2.0 | 46 | | github.com/prometheus/client_model | Apache License 2.0 | 47 | | github.com/prometheus/common | Apache License 2.0 | 48 | | github.com/prometheus/procfs | Apache License 2.0 | 49 | | github.com/rivo/uniseg | MIT License | 50 | | github.com/santhosh-tekuri/jsonschema | Apache License 2.0 | 51 | | github.com/shopspring/decimal | MIT License | 52 | | github.com/synadia-io/jwt-auth-builder.go | Apache License 2.0 | 53 | | github.com/tylertreat/hdrhistogram-writer | Apache License 2.0 | 54 | | golang.org/x/crypto | BSD 3-Clause "New" or "Revised" License | 55 | | golang.org/x/exp | BSD 3-Clause "New" or "Revised" License | 56 | | golang.org/x/net | BSD 3-Clause "New" or "Revised" License | 57 | | golang.org/x/sys | BSD 3-Clause "New" or "Revised" License | 58 | | golang.org/x/term | BSD 3-Clause "New" or "Revised" License | 59 | | golang.org/x/text | BSD 3-Clause "New" or "Revised" License | 60 | | golang.org/x/time | BSD 3-Clause "New" or "Revised" License | 61 | | google.golang.org/protobuf | BSD 3-Clause "New" or "Revised" License | 62 | | gopkg.in/yaml.v3 | Apache License 2.0 | 63 | | gopkg.in/gizak/termui.v1 | MIT License | 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nats-io/natscli 2 | 3 | go 1.23.9 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 8 | github.com/choria-io/fisk v0.7.1 9 | github.com/choria-io/scaffold v0.0.4 10 | github.com/dustin/go-humanize v1.0.1 11 | github.com/emicklei/dot v1.8.0 12 | github.com/expr-lang/expr v1.17.2 13 | github.com/fatih/color v1.18.0 14 | github.com/ghodss/yaml v1.0.0 15 | github.com/google/go-cmp v0.7.0 16 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 17 | github.com/gosuri/uiprogress v0.0.1 18 | github.com/jedib0t/go-pretty/v6 v6.6.7 19 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 20 | github.com/klauspost/compress v1.18.0 21 | github.com/nats-io/jsm.go v0.2.3 22 | github.com/nats-io/jwt/v2 v2.7.4 23 | github.com/nats-io/nats-server/v2 v2.11.3 24 | github.com/nats-io/nats.go v1.42.0 25 | github.com/nats-io/nkeys v0.4.11 26 | github.com/nats-io/nuid v1.0.1 27 | github.com/prometheus/client_golang v1.22.0 28 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 29 | github.com/synadia-io/jwt-auth-builder.go v0.0.9 30 | github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f 31 | golang.org/x/crypto v0.38.0 32 | golang.org/x/term v0.32.0 33 | gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a 34 | gopkg.in/yaml.v2 v2.4.0 35 | gopkg.in/yaml.v3 v3.0.1 36 | ) 37 | 38 | require ( 39 | dario.cat/mergo v1.0.2 // indirect 40 | github.com/Masterminds/goutils v1.1.1 // indirect 41 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 42 | github.com/beorn7/perks v1.0.1 // indirect 43 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 44 | github.com/google/go-tpm v0.9.5 // indirect 45 | github.com/google/uuid v1.6.0 // indirect 46 | github.com/gosuri/uilive v0.0.4 // indirect 47 | github.com/huandu/xstrings v1.5.0 // indirect 48 | github.com/mattn/go-colorable v0.1.14 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/mattn/go-runewidth v0.0.16 // indirect 51 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 52 | github.com/minio/highwayhash v1.0.3 // indirect 53 | github.com/mitchellh/copystructure v1.2.0 // indirect 54 | github.com/mitchellh/go-homedir v1.1.0 // indirect 55 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 57 | github.com/nats-io/nsc/v2 v2.11.0 // indirect 58 | github.com/nsf/termbox-go v1.1.1 // indirect 59 | github.com/prometheus/client_model v0.6.2 // indirect 60 | github.com/prometheus/common v0.63.0 // indirect 61 | github.com/prometheus/procfs v0.16.1 // indirect 62 | github.com/rivo/uniseg v0.4.7 // indirect 63 | github.com/shopspring/decimal v1.4.0 // indirect 64 | github.com/spf13/cast v1.8.0 // indirect 65 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 66 | golang.org/x/net v0.40.0 // indirect 67 | golang.org/x/sys v0.33.0 // indirect 68 | golang.org/x/text v0.25.0 // indirect 69 | golang.org/x/time v0.11.0 // indirect 70 | google.golang.org/protobuf v1.36.6 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /internal/asciigraph/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Rohit Gupta 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /internal/asciigraph/legend.go: -------------------------------------------------------------------------------- 1 | package asciigraph 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | // Create legend item as a colored box and text 11 | func createLegendItem(text string, color AnsiColor) (string, int) { 12 | return fmt.Sprintf( 13 | "%s■%s %s", 14 | color.String(), 15 | Default.String(), 16 | text, 17 | ), 18 | // Can't use len() because of AnsiColor, add 2 for box and space 19 | utf8.RuneCountInString(text) + 2 20 | } 21 | 22 | // Add legend for each series added to the graph 23 | func addLegends(lines *bytes.Buffer, config *config, lenMax int, leftPad int) { 24 | lines.WriteString("\n\n") 25 | lines.WriteString(strings.Repeat(" ", leftPad)) 26 | 27 | var legendsText string 28 | var legendsTextLen int 29 | rightPad := 3 30 | for i, text := range config.SeriesLegends { 31 | item, itemLen := createLegendItem(text, config.SeriesColors[i]) 32 | legendsText += item 33 | legendsTextLen += itemLen 34 | 35 | if i < len(config.SeriesLegends)-1 { 36 | legendsText += strings.Repeat(" ", rightPad) 37 | legendsTextLen += rightPad 38 | } 39 | } 40 | 41 | if legendsTextLen < lenMax { 42 | lines.WriteString(strings.Repeat(" ", (lenMax-legendsTextLen)/2)) 43 | } 44 | lines.WriteString(legendsText) 45 | } 46 | -------------------------------------------------------------------------------- /internal/asciigraph/options.go: -------------------------------------------------------------------------------- 1 | package asciigraph 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Option represents a configuration setting. 8 | type Option interface { 9 | apply(c *config) 10 | } 11 | 12 | // config holds various graph options 13 | type config struct { 14 | Width, Height int 15 | LowerBound, UpperBound *float64 16 | Offset int 17 | Caption string 18 | Precision uint 19 | CaptionColor AnsiColor 20 | AxisColor AnsiColor 21 | LabelColor AnsiColor 22 | SeriesColors []AnsiColor 23 | SeriesLegends []string 24 | ValueFormatter NumberFormatter 25 | AlwaysY bool 26 | } 27 | 28 | type NumberFormatter func(any) string 29 | 30 | // An optionFunc applies an option. 31 | type optionFunc func(*config) 32 | 33 | // apply implements the Option interface. 34 | func (of optionFunc) apply(c *config) { of(c) } 35 | 36 | func configure(defaults config, options []Option) *config { 37 | for _, o := range options { 38 | o.apply(&defaults) 39 | } 40 | return &defaults 41 | } 42 | 43 | // Width sets the graphs width. By default, the width of the graph is 44 | // determined by the number of data points. If the value given is a 45 | // positive number, the data points are interpolated on the x axis. 46 | // Values <= 0 reset the width to the default value. 47 | func Width(w int) Option { 48 | return optionFunc(func(c *config) { 49 | if w > 0 { 50 | c.Width = w 51 | } else { 52 | c.Width = 0 53 | } 54 | }) 55 | } 56 | 57 | // Height sets the graphs height. 58 | func Height(h int) Option { 59 | return optionFunc(func(c *config) { 60 | if h > 0 { 61 | c.Height = h 62 | } else { 63 | c.Height = 0 64 | } 65 | }) 66 | } 67 | 68 | // LowerBound sets the graph's minimum value for the vertical axis. It will be ignored 69 | // if the series contains a lower value. 70 | func LowerBound(min float64) Option { 71 | return optionFunc(func(c *config) { c.LowerBound = &min }) 72 | } 73 | 74 | // UpperBound sets the graph's maximum value for the vertical axis. It will be ignored 75 | // if the series contains a bigger value. 76 | func UpperBound(max float64) Option { 77 | return optionFunc(func(c *config) { c.UpperBound = &max }) 78 | } 79 | 80 | // Offset sets the graphs offset. 81 | func Offset(o int) Option { 82 | return optionFunc(func(c *config) { c.Offset = o }) 83 | } 84 | 85 | // Precision sets the graphs precision. 86 | func Precision(p uint) Option { 87 | return optionFunc(func(c *config) { c.Precision = p }) 88 | } 89 | 90 | // Caption sets the graphs caption. 91 | func Caption(caption string) Option { 92 | return optionFunc(func(c *config) { 93 | c.Caption = strings.TrimSpace(caption) 94 | }) 95 | } 96 | 97 | // CaptionColor sets the caption color. 98 | func CaptionColor(ac AnsiColor) Option { 99 | return optionFunc(func(c *config) { 100 | c.CaptionColor = ac 101 | }) 102 | } 103 | 104 | // AxisColor sets the axis color. 105 | func AxisColor(ac AnsiColor) Option { 106 | return optionFunc(func(c *config) { 107 | c.AxisColor = ac 108 | }) 109 | } 110 | 111 | // LabelColor sets the axis label color. 112 | func LabelColor(ac AnsiColor) Option { 113 | return optionFunc(func(c *config) { 114 | c.LabelColor = ac 115 | }) 116 | } 117 | 118 | // SeriesColors sets the series colors. 119 | func SeriesColors(ac ...AnsiColor) Option { 120 | return optionFunc(func(c *config) { 121 | c.SeriesColors = ac 122 | }) 123 | } 124 | 125 | // SeriesLegends sets the legend text for the corresponding series. 126 | func SeriesLegends(text ...string) Option { 127 | return optionFunc(func(c *config) { 128 | c.SeriesLegends = text 129 | }) 130 | } 131 | 132 | // ValueFormatter formats values printed to the side of graphs 133 | func ValueFormatter(f NumberFormatter) Option { 134 | return optionFunc(func(c *config) { 135 | c.ValueFormatter = f 136 | }) 137 | } 138 | 139 | // AxisColor sets the axis color. 140 | func AlwaysY(ay bool) Option { 141 | return optionFunc(func(c *config) { 142 | c.AlwaysY = ay 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /internal/asciigraph/utils.go: -------------------------------------------------------------------------------- 1 | package asciigraph 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | ) 11 | 12 | func minMaxFloat64Slice(v []float64) (min, max float64) { 13 | min = math.Inf(1) 14 | max = math.Inf(-1) 15 | 16 | if len(v) == 0 { 17 | panic("Empty slice") 18 | } 19 | 20 | for _, e := range v { 21 | if e < min { 22 | min = e 23 | } 24 | if e > max { 25 | max = e 26 | } 27 | } 28 | return 29 | } 30 | 31 | func round(input float64) float64 { 32 | if math.IsNaN(input) { 33 | return math.NaN() 34 | } 35 | sign := 1.0 36 | if input < 0 { 37 | sign = -1 38 | input *= -1 39 | } 40 | _, decimal := math.Modf(input) 41 | var rounded float64 42 | if decimal >= 0.5 { 43 | rounded = math.Ceil(input) 44 | } else { 45 | rounded = math.Floor(input) 46 | } 47 | return rounded * sign 48 | } 49 | 50 | func linearInterpolate(before, after, atPoint float64) float64 { 51 | return before + (after-before)*atPoint 52 | } 53 | 54 | func interpolateArray(data []float64, fitCount int) []float64 { 55 | var interpolatedData []float64 56 | 57 | springFactor := float64(len(data)-1) / float64(fitCount-1) 58 | interpolatedData = append(interpolatedData, data[0]) 59 | 60 | for i := 1; i < fitCount-1; i++ { 61 | spring := float64(i) * springFactor 62 | before := math.Floor(spring) 63 | after := math.Ceil(spring) 64 | atPoint := spring - before 65 | interpolatedData = append(interpolatedData, linearInterpolate(data[int(before)], data[int(after)], atPoint)) 66 | } 67 | interpolatedData = append(interpolatedData, data[len(data)-1]) 68 | return interpolatedData 69 | } 70 | 71 | // clear terminal screen 72 | var Clear func() 73 | 74 | func init() { 75 | platform := runtime.GOOS 76 | 77 | if platform == "windows" { 78 | Clear = func() { 79 | cmd := exec.Command("cmd", "/c", "cls") 80 | cmd.Stdout = os.Stdout 81 | if err := cmd.Run(); err != nil { 82 | log.Fatal(err) 83 | } 84 | } 85 | } else { 86 | Clear = func() { 87 | fmt.Print("\033[2J\033[H") 88 | } 89 | } 90 | } 91 | 92 | func calculateHeight(interval float64) int { 93 | if interval >= 1 { 94 | return int(interval) 95 | } 96 | 97 | scaleFactor := math.Pow(10, math.Floor(math.Log10(interval))) 98 | scaledDelta := interval / scaleFactor 99 | 100 | if scaledDelta < 2 { 101 | return int(math.Ceil(scaledDelta)) 102 | } 103 | return int(math.Floor(scaledDelta)) 104 | } 105 | -------------------------------------------------------------------------------- /internal/scaffold/funcs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package scaffold 15 | 16 | import ( 17 | "html/template" 18 | 19 | "github.com/nats-io/natscli/internal/auth" 20 | ab "github.com/synadia-io/jwt-auth-builder.go" 21 | ) 22 | 23 | func templateFuncs() template.FuncMap { 24 | return template.FuncMap{ 25 | "getOperator": func(op string) (ab.Operator, error) { 26 | ab, err := auth.GetAuthBuilder() 27 | if err != nil { 28 | return nil, err 29 | } 30 | return ab.Operators().Get(op) 31 | }, 32 | "getAccount": func(op string, account string) (ab.Account, error) { 33 | ab, err := auth.GetAuthBuilder() 34 | if err != nil { 35 | return nil, err 36 | } 37 | operator, err := ab.Operators().Get(op) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return operator.Accounts().Get(account) 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/scaffold/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package scaffold 15 | 16 | import ( 17 | "log" 18 | 19 | "github.com/choria-io/scaffold" 20 | ) 21 | 22 | type logger struct { 23 | debug bool 24 | } 25 | 26 | func (l logger) Debugf(format string, v ...any) { 27 | if l.debug { 28 | log.Printf(format, v...) 29 | } 30 | } 31 | 32 | func (l logger) Infof(format string, v ...any) { 33 | log.Printf(format, v...) 34 | } 35 | 36 | func newLogger(debug bool) scaffold.Logger { 37 | return &logger{ 38 | debug: debug, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/bundle.yaml: -------------------------------------------------------------------------------- 1 | description: Generates development NATS clusters for Docker Compose 2 | contact: https://github.com/nats-io/natscli/issues 3 | source: https://github.com/nats-io/natscli/ 4 | version: 0.0.1 5 | post_scaffold: | 6 | {{- $base_port := .port -}} 7 | {{- $domain := .domain -}} 8 | {{- $members := .members -}} 9 | Generated {{.clusters }} NATS Cluster each with {{.members }} servers 10 | 11 | JetStream: {{ if .jetstream}}yes{{else}}no{{end}} 12 | DNS Domain: {{ .domain }} 13 | Docker Image: {{ .image }} 14 | Container Configuration: {{ .configPath }} 15 | System Password: system / {{ .password }} 16 | User Password: user / {{ .password }} 17 | 18 | Port Configuration: 19 | {{ $base_port := .port -}} 20 | {{- $domain := .domain -}} 21 | {{- $members := .members -}} 22 | 23 | {{- range $cluster := .clusters | seq | split " " }} 24 | {{- range $node := $members | seq | split " " }} 25 | {{- $client_listen := (sub (add $base_port (mul (sub $cluster 1) 100) $node) 1) -}} 26 | {{- $node_name := cat "n" $node ".c" $cluster | nospace }} 27 | {{ $node_name }}.{{ $domain }}:{{ $client_listen }} 28 | {{- end }} 29 | {{- end }} 30 | 31 | Start the network by changing to the target directory and doing "docker compose up" 32 | 33 | Once running use the following command to start a shell: 34 | 35 | docker compose run cli.{{ $domain }} 36 | -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/form.yaml: -------------------------------------------------------------------------------- 1 | name: nats-cluster 2 | description: | 3 | Generate local development NATS Clusters for use with Docker Compose 4 | 5 | You will be asked for number of clusters and members per cluster, configuration 6 | for each node will be generated along with a Docker Compose configuration. 7 | 8 | The 'nats' command line will be setup with contexts for accessing the various 9 | clusters, system account and more. 10 | 11 | properties: 12 | - name: data 13 | description: The directory to store data in 14 | default: ./data 15 | 16 | - name: domain 17 | description: The DNS domain to use for the network 18 | default: nats.internal 19 | 20 | - name: image 21 | description: The docker image to use 22 | default: nats:latest 23 | 24 | - name: clusters 25 | description: The number of clusters to create 26 | type: integer 27 | default: 3 28 | 29 | - name: members 30 | description: The number of servers to create per cluster 31 | type: integer 32 | default: 3 33 | 34 | - name: port 35 | description: The port used by the first server in the first cluster 36 | default: 10000 37 | 38 | - name: configPath 39 | description: The location in the docker container to mount the configuration 40 | default: /nats-server.conf 41 | 42 | - name: password 43 | description: When set creates multiple accounts with this password 44 | default: s3cret 45 | 46 | - name: jetstream 47 | description: Enables JetStream in the cluster 48 | type: bool 49 | default: true 50 | 51 | - name: mount 52 | description: Mounts a specific directory into the containers 53 | -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/_partials/system_context.got: -------------------------------------------------------------------------------- 1 | { 2 | {{- if (get . "main") }} 3 | "description":"NATS System Account", 4 | {{- else }} 5 | "description":"NATS System Account in Cluster {{ get . "cluster" }}", 6 | {{- end }} 7 | "url":"{{ get . "urls" | join ","}}", 8 | "user":"system", 9 | "password":"{{ get . "password" }}", 10 | "color_scheme":"red" 11 | } -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/_partials/user_context.got: -------------------------------------------------------------------------------- 1 | { 2 | {{- if (get . "main") }} 3 | "description":"NATS User Account", 4 | {{- else }} 5 | "description":"NATS User Account in Cluster {{ get . "cluster" }}", 6 | {{- end }} 7 | "url":"{{ get . "urls" | join ","}}", 8 | "user":"user", 9 | "password":"{{ get . "password" }}", 10 | "color_scheme":"green" 11 | } -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/cli/context.txt: -------------------------------------------------------------------------------- 1 | user -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/cli/context/generate.got: -------------------------------------------------------------------------------- 1 | {{- $password := .password -}} 2 | {{- $members := .members -}} 3 | {{- $domain := .domain -}} 4 | {{- range $cluster := .clusters | seq | split " " }} 5 | {{- $data := dict "cluster" (cat "c" $cluster | nospace) }} 6 | {{- $data := set $data "password" $password }} 7 | {{- $data := set $data "urls" list }} 8 | {{- $data := set $data "members" ($members | seq) }} 9 | {{- range $node := $members | seq | split " " }} 10 | {{- $node_name := cat "n" $node ".c" $cluster | nospace }} 11 | {{- $data := set $data "urls" (append (get $data "urls") (cat "nats://" $node_name "." $domain ":4222" | nospace)) }} 12 | {{- end }} 13 | {{- render "_partials/user_context.got" $data | write (cat "cli/context/" (get $data "cluster") ".json" | nospace) }} 14 | {{- render "_partials/system_context.got" $data | write (cat "cli/context/system_" (get $data "cluster") ".json" | nospace) }} 15 | {{ if eq $cluster "1" }} 16 | {{ $_ := set $data "main" true }} 17 | {{- render "_partials/user_context.got" $data | write "cli/context/user.json" }} 18 | {{- render "_partials/system_context.got" $data | write "cli/context/system.json" }} 19 | {{ end }} 20 | {{- end }} -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/cluster.conf: -------------------------------------------------------------------------------- 1 | port: 4222 2 | monitor_port: 8222 3 | server_name: $NAME 4 | client_advertise: $ADVERTISE 5 | 6 | server_tags: $GATEWAY 7 | 8 | {{- if .jetstream }} 9 | jetstream { 10 | store_dir: /data 11 | } 12 | {{- end }} 13 | 14 | cluster { 15 | port: 6222 16 | 17 | routes = [ 18 | {{- range $node := .members | seq | split " " }} 19 | nats-route://n{{ $node }}:6222 20 | {{- end }} 21 | ] 22 | } 23 | 24 | gateway { 25 | name: $GATEWAY 26 | port: 7222 27 | 28 | gateways: [ 29 | {{- $members := .members -}} 30 | {{- $domain := .domain -}} 31 | {{- range $cluster := .clusters | seq | split " " }} 32 | { 33 | name: "c{{ $cluster }}" 34 | urls: [ 35 | {{- range $node := $members | seq | split " " }} 36 | "nats://n{{ $node }}.c{{ $cluster }}.{{ $domain }}:7222" 37 | {{- end }} 38 | ] 39 | } 40 | {{- end }} 41 | ] 42 | } 43 | 44 | {{- if .password }} 45 | accounts { 46 | users: { 47 | {{- if .jetstream }} 48 | jetstream: enabled 49 | {{- end }} 50 | users = [ 51 | {user: user, password: {{ .password }}} 52 | ] 53 | } 54 | 55 | system: { 56 | users = [ 57 | {user: system, password: {{ .password }}} 58 | ] 59 | } 60 | } 61 | 62 | system_account: system 63 | {{- end }} -------------------------------------------------------------------------------- /internal/scaffold/store/natsbuilder/scaffold/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | {{- $base_port := .port -}} 2 | {{- $members := .members -}} 3 | {{- $configPath := .configPath }} 4 | {{- $domain := .domain -}} 5 | {{- $image := .image -}} 6 | {{- $mount := .mount -}} 7 | {{- $data := .data -}} 8 | 9 | version: "3" 10 | services: 11 | cli.{{ $domain }}: 12 | image: synadia/nats-server:nightly 13 | dns_search: {{ $domain }} 14 | entrypoint: /bin/sh 15 | networks: 16 | - shared 17 | volumes: 18 | - "./cli:/root/.config/nats" 19 | {{- if $mount }} 20 | - {{ $mount }} 21 | {{- end }} 22 | 23 | {{- range $cluster := .clusters | seq | split " " }} 24 | {{- range $node := $members | seq | split " " }} 25 | {{- $client_listen := (sub (add $base_port (mul (sub $cluster 1) 100) $node) 1) -}} 26 | {{- $node_name := cat "n" $node ".c" $cluster | nospace }} 27 | {{ $node_name }}.{{ $domain }}: 28 | dns_search: c{{ $cluster}}.{{ $domain }} 29 | image: {{ $image }} 30 | environment: 31 | GATEWAY: c{{$cluster}} 32 | NAME: {{ $node_name }} 33 | ADVERTISE: {{ $node_name }}.{{ $domain }}:{{ $client_listen }} 34 | networks: 35 | - shared 36 | - nats-cluster{{ $cluster }} 37 | ports: 38 | - {{ $client_listen }}:4222 39 | volumes: 40 | - ./cluster.conf:{{ $configPath }} 41 | - {{ $data }}/{{ $node_name }}:/data 42 | {{- if $mount }} 43 | - {{ $mount }} 44 | {{- end }} 45 | {{end }} 46 | {{- end }} 47 | 48 | networks: 49 | {{- range $cluster := .clusters | seq | split " " }} 50 | nats-cluster{{ $cluster }}: {} 51 | {{- end }} 52 | shared: {} 53 | -------------------------------------------------------------------------------- /internal/scaffold/store/ngsleafnodeconfig/bundle.yaml: -------------------------------------------------------------------------------- 1 | description: Generates configuration to connect a leafnode to Synadia Cloud 2 | contact: https://github.com/nats-io/natscli/issues 3 | source: https://github.com/nats-io/natscli/ 4 | version: 0.0.1 5 | post_scaffold: | 6 | A NATS Server Leafnode configuration was written in {{ ._target }}/leafnode.conf 7 | 8 | Credentials: {{ .credentials }} 9 | Client Port: {{ .port }} 10 | JetStream: {{ if .jetstream}}yes storage in "{{ .store_dir }}"{{else}}no{{end}} 11 | 12 | Store your Synadia Cloud credentials in "{{ .credentials }}" and run: 13 | 14 | nats-server --config leafnode.conf -------------------------------------------------------------------------------- /internal/scaffold/store/ngsleafnodeconfig/form.yaml: -------------------------------------------------------------------------------- 1 | name: ngs-leafnode 2 | description: | 3 | Generates configuration to connect a Leafnode to the Synadia Cloud 4 | 5 | Further information can be found at: 6 | 7 | https://docs.nats.io/running-a-nats-service/nats_docker/ngs-leafnodes-docker 8 | 9 | properties: 10 | - name: port 11 | description: The port used by the first server in the first cluster 12 | default: "4222" 13 | 14 | - name: monitor_port 15 | description: Configures the monitor port, 0 disables 16 | type: integer 17 | default: "0" 18 | 19 | - name: jetstream 20 | description: Enables JetStream in the cluster 21 | type: bool 22 | default: "false" 23 | 24 | - name: store_dir 25 | conditional: input.jetstream == true 26 | description: Directory to write JetStream data 27 | default: data 28 | 29 | - name: jetstream_domain 30 | conditional: input.jetstream == true 31 | description: The JetStream Domain for the Leafnode 32 | default: LEAF 33 | 34 | - name: credentials 35 | description: Path to the Synadia Cloud credential file 36 | required: true 37 | default: ngs.creds -------------------------------------------------------------------------------- /internal/scaffold/store/ngsleafnodeconfig/scaffold.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /internal/scaffold/store/ngsleafnodeconfig/scaffold/leafnode.conf: -------------------------------------------------------------------------------- 1 | # A NATS Server Leafnode configured to connect to Synadia Cloud 2 | # 3 | # Please see https://docs.nats.io/running-a-nats-service/nats_docker/ngs-leafnodes-docker 4 | # for further details. 5 | 6 | # The address and port clients must connect to 7 | listen: 0.0.0.0:{{ .port }} 8 | {{- if .monitor_port }} 9 | 10 | # HTTP requests can be made to this port for monitoring purpose 11 | monitor_port: {{ .monitor_port }} 12 | {{ end }} 13 | {{- if .jetstream }} 14 | 15 | jetstream { 16 | store_dir: "{{ .store_dir }}" 17 | domain: "{{ .jetstream_domain }}" 18 | } 19 | {{ end }} 20 | 21 | leafnodes { 22 | remotes = [ 23 | { 24 | url: "tls://connect.ngs.global" 25 | credentials: "{{ .credentials }}" 26 | }, 27 | ] 28 | } -------------------------------------------------------------------------------- /internal/scaffold/store/operator/bundle.yaml: -------------------------------------------------------------------------------- 1 | description: Generates configuration for an operator based NATS Server 2 | contact: https://github.com/nats-io/natscli/issues 3 | source: https://github.com/nats-io/natscli/ 4 | version: 0.0.1 5 | requires: 6 | operator: true -------------------------------------------------------------------------------- /internal/scaffold/store/operator/form.yaml: -------------------------------------------------------------------------------- 1 | name: nats-server.conf 2 | description: | 3 | Operator Managed NATS Server 4 | 5 | This will guide you through a series of question to create a NATS Server 6 | configuration managed by a NATS Operator. 7 | 8 | To use this an Operator should have been created using the "nats auth" command. 9 | 10 | For more information about Decentralized Authentication please read: 11 | 12 | https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt 13 | 14 | properties: 15 | - name: server_name 16 | description: Unique name for this server 17 | help: Each server needs a Unique name, by default this uses the FQDN but in JetStream scenarios it is worth setting stable names and adjusting DNS pointing at the right node. 18 | default: nats.example.net 19 | required: true 20 | - name: address 21 | description: Address to listen on 22 | default: "0.0.0.0" 23 | validation: isIP(value) 24 | - name: port 25 | description: The port to listen on for client connections 26 | default: "4222" 27 | type: integer 28 | - name: monitor_port 29 | description: Port to listen on for monitoring requests 30 | default: "8222" 31 | type: integer 32 | - name: streams 33 | description: Enables JetStream in the cluster 34 | type: bool 35 | default: true 36 | 37 | - name: jetstream 38 | description: | 39 | JetStream configuration 40 | 41 | We will now configure the NATS JetStream persistence layer. Setting the limits 42 | to -1 means a dynamic value will be chosen by the server at start. We strongly 43 | suggest setting specific limits. 44 | 45 | See https://docs.nats.io/nats-concepts/jetstream for more information 46 | conditional: "input.streams == true" 47 | properties: 48 | - name: store_dir 49 | description: Directory to store JetStream data 50 | default: "/var/lib/nats/jetstream" 51 | required: true 52 | - name: max_mem 53 | description: Maximum amount of RAM that can be used by JetStream 54 | help: Valid values are -1 for unlimited or strings like 1GB 55 | default: "-1" 56 | - name: max_file 57 | description: Maximum amount of disk storage that can be used by JetStream 58 | help: Valid values are -1 for unlimited or strings like 1GB 59 | default: "-1" 60 | 61 | - name: resolver 62 | description: | 63 | NATS Resolver Configuration 64 | 65 | We will now configure where the NATS Server will store account JWT files. 66 | JWT files are pushed to the server using 'nats auth account push' and 67 | describe the full configuration for each account. 68 | 69 | Every server in a cluster needs a resolver configuration. 70 | properties: 71 | - name: dir 72 | description: The directory to store JWTs in 73 | default: "/var/lib/nats/resolver" 74 | - name: allow_delete 75 | description: Should the server allow accounts to be deleted 76 | default: "true" 77 | type: bool 78 | - name: limit 79 | description: The maximum amount of accounts to allow 80 | default: "1000" 81 | type: integer 82 | -------------------------------------------------------------------------------- /internal/scaffold/store/operator/scaffold.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /internal/scaffold/store/operator/scaffold/server.conf: -------------------------------------------------------------------------------- 1 | # Generated NATS Server configuration operated by operator {{ .Requirements.Operator.Name }} 2 | 3 | # Unique name for the server 4 | server_name: {{ .server_name }} 5 | 6 | # The address and port clients must connect to 7 | listen: {{ .address}}:{{ .port }} 8 | 9 | # HTTP requests can be made to this port for monitoring purpose 10 | monitor_port: {{ .monitor_port }} 11 | 12 | # The JWT token of the operator running the server ({{ .Requirements.Operator.Name }}) 13 | operator: {{ .Requirements.Operator.JWT }} 14 | 15 | {{ if .Requirements.Operator.SystemAccount }} 16 | # The JWT token of the system account managing the server ({{ .Requirements.Operator.Name }}) 17 | system_account: {{ .Requirements.Operator.SystemAccount.Subject }} 18 | 19 | resolver_preload { 20 | // Account: {{ .Requirements.Operator.SystemAccount.Name }} 21 | {{ .Requirements.Operator.SystemAccount.Subject }}: {{ .Requirements.Operator.SystemAccount.JWT }} 22 | } 23 | {{ end }} 24 | 25 | {{ if .jetstream }} 26 | jetstream { 27 | store_dir: {{ .jetstream.store_dir }} 28 | max_mem: {{ .jetstream.max_mem }} 29 | max_file: {{ .jetstream.max_file }} 30 | } 31 | {{ end }} 32 | 33 | # Configures the Full NATS Resolver 34 | resolver { 35 | type: full 36 | dir: {{ .resolver.dir }} 37 | allow_delete: {{ .resolver.allow_delete }} 38 | interval: "2m" 39 | limit: {{ .resolver.limit }} 40 | } 41 | 42 | -------------------------------------------------------------------------------- /internal/scaffold/store/operatork8s/bundle.yaml: -------------------------------------------------------------------------------- 1 | description: Generates configuration for an operator based NATS Server managed by Kubernetes 2 | contact: https://github.com/nats-io/natscli/issues 3 | source: https://github.com/nats-io/natscli/ 4 | version: 0.0.1 5 | requires: 6 | operator: true -------------------------------------------------------------------------------- /internal/scaffold/store/operatork8s/form.yaml: -------------------------------------------------------------------------------- 1 | name: nats-server.conf 2 | description: | 3 | Operator Managed NATS Server for Kubernetes 4 | 5 | This will guide you through a series of question to create a NATS Cluster 6 | configuration managed by a NATS Operator using the nats Helm Chart. 7 | 8 | To use this an Operator should have been created using the "nats auth" command. 9 | 10 | For more information about Decentralized Authentication please read: 11 | 12 | https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt 13 | 14 | For more information about the Helm chart please read: 15 | 16 | https://github.com/nats-io/k8s/tree/main/helm/charts/nats 17 | 18 | Deploy the resulting Cluster using helm: 19 | 20 | helm repo add nats https://nats-io.github.io/k8s/helm/charts/ 21 | helm upgrade --install nats nats/nats -f values.yaml 22 | 23 | To access the cluster you can use kubectl: 24 | 25 | kubectl port-forward service/nats 4222 26 | 27 | Once set up you can create credentials and push your accounts. 28 | 29 | properties: 30 | - name: replicas 31 | description: How many server pods to start 32 | type: integer 33 | default: "3" 34 | 35 | - name: streams 36 | description: Enables JetStream in the cluster 37 | type: bool 38 | default: "true" 39 | 40 | - name: jetstream 41 | description: | 42 | JetStream configuration 43 | 44 | We will now configure the NATS JetStream persistence layer. Setting the limits 45 | to -1 means a dynamic value will be chosen by the server at start. We strongly 46 | suggest setting specific limits. 47 | 48 | See https://docs.nats.io/nats-concepts/jetstream for more information 49 | conditional: "input.streams == true" 50 | properties: 51 | - name: storage 52 | description: The maximum amount of PVC resources to allocate 53 | help: Valid values look like '10Gi' 54 | default: "10Gi" 55 | 56 | - name: resolver 57 | description: | 58 | NATS Resolver Configuration 59 | 60 | We will now configure where the NATS Server will store account JWT files. 61 | JWT files are pushed to the server using 'nats auth account push' and 62 | describe the full configuration for each account. 63 | 64 | Every server in a cluster needs a resolver configuration. 65 | properties: 66 | - name: allow_delete 67 | description: Should the server allow accounts to be deleted 68 | default: "true" 69 | type: bool 70 | - name: limit 71 | description: The maximum amount of accounts to allow 72 | default: "1000" 73 | type: integer -------------------------------------------------------------------------------- /internal/scaffold/store/operatork8s/scaffold.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /internal/scaffold/store/operatork8s/scaffold/values.yaml: -------------------------------------------------------------------------------- 1 | natsBox: 2 | enabled: false 3 | 4 | config: 5 | cluster: 6 | enabled: true 7 | replicas: {{ .replicas }} 8 | resolver: 9 | enabled: true 10 | merge: 11 | type: full 12 | interval: 2m 13 | timeout: 1.9s 14 | {{ if .jetstream }} 15 | jetstream: 16 | enabled: true 17 | fileStore: 18 | pvc: 19 | size: {{ .jetstream.storage }} 20 | {{ end }} 21 | merge: 22 | operator: {{ .Requirements.Operator.JWT }} 23 | system_account: {{ .Requirements.Operator.SystemAccount.Subject }} 24 | resolver_preload: 25 | SYS_ACCOUNT_ID: {{ .Requirements.Operator.SystemAccount.JWT }} -------------------------------------------------------------------------------- /internal/sysclient/healthstatus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package sysclient 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | 20 | "github.com/nats-io/nats-server/v2/server" 21 | "github.com/nats-io/natscli/internal/util" 22 | ) 23 | 24 | const ( 25 | StatusOK HealthStatus = iota 26 | StatusUnavailable 27 | StatusError 28 | ) 29 | 30 | type ( 31 | HealthzResp struct { 32 | Server server.ServerInfo `json:"server"` 33 | Healthz Healthz `json:"data"` 34 | } 35 | 36 | Healthz struct { 37 | Status HealthStatus `json:"status"` 38 | Error string `json:"error,omitempty"` 39 | } 40 | 41 | HealthStatus int 42 | ) 43 | 44 | func (hs *HealthStatus) UnmarshalJSON(data []byte) error { 45 | switch string(data) { 46 | case util.JSONString("ok"): 47 | *hs = StatusOK 48 | case util.JSONString("na"), util.JSONString("unavailable"): 49 | *hs = StatusUnavailable 50 | case util.JSONString("error"): 51 | *hs = StatusError 52 | default: 53 | return fmt.Errorf("cannot unmarshal %q", data) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (hs HealthStatus) MarshalJSON() ([]byte, error) { 60 | switch hs { 61 | case StatusOK: 62 | return json.Marshal("ok") 63 | case StatusUnavailable: 64 | return json.Marshal("na") 65 | case StatusError: 66 | return json.Marshal("error") 67 | default: 68 | return nil, fmt.Errorf("unknown health status: %v", hs) 69 | } 70 | } 71 | 72 | func (hs HealthStatus) String() string { 73 | switch hs { 74 | case StatusOK: 75 | return "ok" 76 | case StatusUnavailable: 77 | return "na" 78 | case StatusError: 79 | return "error" 80 | default: 81 | return "unknown health status" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/util/backoff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | // https://blog.gopheracademy.com/advent-2014/backoff/ 17 | 18 | import ( 19 | "context" 20 | "math/rand/v2" 21 | "time" 22 | ) 23 | 24 | // BackoffPolicy implements a backoff policy, randomizing its delays 25 | // and saturating at the final value in Millis. 26 | type BackoffPolicy struct { 27 | Millis []int 28 | } 29 | 30 | // DefaultBackoff is the default backoff policy to use 31 | var DefaultBackoff = BackoffPolicy{ 32 | Millis: []int{ 33 | 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 34 | 5500, 5750, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 9500, 10000, 35 | 10500, 10750, 11000, 11500, 12000, 12500, 13000, 13500, 14000, 14500, 15000, 36 | 15500, 15750, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 37 | }, 38 | } 39 | 40 | // Duration returns the time duration of the n'th wait cycle in a 41 | // backoff policy. This is b.Millis[n], randomized to avoid thundering 42 | // herds. 43 | func (b BackoffPolicy) Duration(n int) time.Duration { 44 | if n >= len(b.Millis) { 45 | n = len(b.Millis) - 1 46 | } 47 | 48 | return time.Duration(jitter(b.Millis[n])) * time.Millisecond 49 | } 50 | 51 | // TrySleep sleeps for the duration of the n'th try cycle 52 | // in a way that can be interrupted by the context. An error is returned 53 | // if the context cancels the sleep 54 | func (b BackoffPolicy) TrySleep(ctx context.Context, n int) error { 55 | return b.Sleep(ctx, b.Duration(n)) 56 | } 57 | 58 | // Sleep sleeps for the duration t and can be interrupted by ctx. An error 59 | // is returns if the context cancels the sleep 60 | func (b BackoffPolicy) Sleep(ctx context.Context, t time.Duration) error { 61 | timer := time.NewTimer(t) 62 | 63 | select { 64 | case <-timer.C: 65 | return nil 66 | case <-ctx.Done(): 67 | return ctx.Err() 68 | } 69 | } 70 | 71 | // For is a for{} loop that stops on context and has a backoff based sleep between loops 72 | // if the context completes the loop ends returning the context error 73 | func (b BackoffPolicy) For(ctx context.Context, cb func(try int) error) error { 74 | tries := 0 75 | for { 76 | if ctx.Err() != nil { 77 | return ctx.Err() 78 | } 79 | 80 | tries++ 81 | 82 | err := cb(tries) 83 | if err == nil { 84 | return nil 85 | } 86 | 87 | err = b.TrySleep(ctx, tries) 88 | if err != nil { 89 | return err 90 | } 91 | } 92 | } 93 | 94 | // jitter returns a random integer uniformly distributed in the range 95 | // [0.5 * millis .. 1.5 * millis] 96 | func jitter(millis int) int { 97 | if millis == 0 { 98 | return 0 99 | } 100 | 101 | return millis/2 + rand.N(millis) 102 | } 103 | -------------------------------------------------------------------------------- /internal/util/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "os" 20 | "os/user" 21 | "path/filepath" 22 | ) 23 | 24 | type Config struct { 25 | SelectedOperator string `json:"select_operator"` 26 | } 27 | 28 | func LoadConfig() (*Config, error) { 29 | parent, err := ConfigDir() 30 | if err != nil { 31 | return nil, fmt.Errorf("could not determine configuration directory: %w", err) 32 | } 33 | cfile := filepath.Join(parent, "config.json") 34 | 35 | cfg := Config{} 36 | 37 | if !FileExists(cfile) { 38 | return &Config{}, nil 39 | } 40 | 41 | cj, err := os.ReadFile(cfile) 42 | if err != nil { 43 | return nil, fmt.Errorf("could not read configuration file: %w", err) 44 | } 45 | err = json.Unmarshal(cj, &cfg) 46 | if err != nil { 47 | return nil, fmt.Errorf("could not parse configuration file: %w", err) 48 | } 49 | 50 | return &cfg, nil 51 | } 52 | 53 | func SaveConfig(cfg *Config) error { 54 | parent, err := ConfigDir() 55 | if err != nil { 56 | return err 57 | } 58 | cfile := filepath.Join(parent, "config.json") 59 | 60 | j, err := json.Marshal(cfg) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = os.WriteFile(cfile, j, 0600) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // XdgShareHome is where to store data like nsc stored 74 | func XdgShareHome() (string, error) { 75 | parent := os.Getenv("XDG_DATA_HOME") 76 | if parent != "" { 77 | return parent, nil 78 | } 79 | 80 | u, err := user.Current() 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | if u.HomeDir == "" { 86 | return "", fmt.Errorf("cannot determine home directory") 87 | } 88 | 89 | return filepath.Join(u.HomeDir, ".local", "share"), nil 90 | } 91 | 92 | // ConfigDir is the directory holding configuration files 93 | func ConfigDir() (string, error) { 94 | parent, err := ParentDir() 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | dir := filepath.Join(parent, "nats", "cli") 100 | err = os.MkdirAll(dir, 0700) 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | return dir, nil 106 | } 107 | 108 | // ParentDir is the parent, controlled by XDG_CONFIG_HOME, for any configuration 109 | func ParentDir() (string, error) { 110 | parent := os.Getenv("XDG_CONFIG_HOME") 111 | if parent != "" { 112 | return parent, nil 113 | } 114 | 115 | u, err := user.Current() 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | if u.HomeDir == "" { 121 | return "", fmt.Errorf("cannot determine home directory") 122 | } 123 | 124 | return filepath.Join(u.HomeDir, parent, ".config"), nil 125 | } 126 | -------------------------------------------------------------------------------- /internal/util/header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "github.com/nats-io/nats.go" 18 | "testing" 19 | ) 20 | 21 | func TestParseStringsToHeader(t *testing.T) { 22 | _, err := ParseStringsToHeader([]string{"A:1", "B"}, 0) 23 | if err == nil || err.Error() != `invalid header "B"` { 24 | t.Fatalf("expected invalid header error, got: %v", err) 25 | } 26 | 27 | res, err := ParseStringsToHeader([]string{"A:1", "B:2", "C:{{ Count }}"}, 10) 28 | if err != nil { 29 | t.Fatalf("expected no error, got: %v", err) 30 | } 31 | 32 | if res.Get("A") != "1" { 33 | t.Fatalf("expected 1, got: %v", res.Get("A")) 34 | } 35 | 36 | if res.Get("B") != "2" { 37 | t.Fatalf("expected 2, got: %v", res.Get("B")) 38 | } 39 | 40 | if res.Get("C") != "10" { 41 | t.Fatalf("expected 10, got: %v", res.Get("C")) 42 | } 43 | } 44 | 45 | func TestParseStringsToMsgHeader(t *testing.T) { 46 | msg := nats.NewMsg("") 47 | err := ParseStringsToMsgHeader([]string{"A:1", "B"}, 0, msg) 48 | if err == nil || err.Error() != `invalid header "B"` { 49 | t.Fatalf("expected invalid header error, got: %v", err) 50 | } 51 | 52 | err = ParseStringsToMsgHeader([]string{"A:1", "B:2", "C:{{ Count }}"}, 10, msg) 53 | if err != nil { 54 | t.Fatalf("expected no error, got: %v", err) 55 | } 56 | 57 | if msg.Header.Get("A") != "1" { 58 | t.Fatalf("expected 1, got: %v", msg.Header.Get("A")) 59 | } 60 | 61 | if msg.Header.Get("B") != "2" { 62 | t.Fatalf("expected 2, got: %v", msg.Header.Get("B")) 63 | } 64 | 65 | if msg.Header.Get("C") != "10" { 66 | t.Fatalf("expected 10, got: %v", msg.Header.Get("C")) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/util/headers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "fmt" 20 | "github.com/nats-io/nats.go" 21 | "github.com/nats-io/nuid" 22 | "net/textproto" 23 | "strings" 24 | "text/template" 25 | "time" 26 | ) 27 | 28 | // ParseStringsToHeader creates a nats.Header from a list of strings like X:Y 29 | func ParseStringsToHeader(hdrs []string, seq int) (nats.Header, error) { 30 | res := nats.Header{} 31 | 32 | err := parseStringsToHeader(hdrs, seq, res) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return res, nil 38 | } 39 | 40 | // ParseStringsToMsgHeader parsers strings of headers like X:Y into a supplied msg headers 41 | func ParseStringsToMsgHeader(hdrs []string, seq int, msg *nats.Msg) error { 42 | err := parseStringsToHeader(hdrs, seq, msg.Header) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func parseStringsToHeader(hdrs []string, seq int, res nats.Header) error { 51 | for _, hdr := range hdrs { 52 | parts := strings.SplitN(hdr, ":", 2) 53 | if len(parts) != 2 { 54 | return fmt.Errorf("invalid header %q", hdr) 55 | } 56 | 57 | val, err := PubReplyBodyTemplate(strings.TrimSpace(parts[1]), "", seq) 58 | if err != nil { 59 | return fmt.Errorf("failed to parse Header template for %s: %s", parts[0], err) 60 | } 61 | 62 | res.Add(strings.TrimSpace(parts[0]), string(val)) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | type pubData struct { 69 | Cnt int 70 | Count int 71 | Unix int64 72 | UnixNano int64 73 | TimeStamp string 74 | Time string 75 | Request string 76 | } 77 | 78 | func (p *pubData) ID() string { 79 | return nuid.Next() 80 | } 81 | 82 | // PubReplyBodyTemplate parses a message body using the usual template functions we support as standard 83 | func PubReplyBodyTemplate(body string, request string, ctr int) ([]byte, error) { 84 | now := time.Now() 85 | funcMap := template.FuncMap{ 86 | "Random": RandomString, 87 | "Count": func() int { return ctr }, 88 | "Cnt": func() int { return ctr }, 89 | "Unix": func() int64 { return now.Unix() }, 90 | "UnixNano": func() int64 { return now.UnixNano() }, 91 | "TimeStamp": func() string { return now.Format(time.RFC3339) }, 92 | "Time": func() string { return now.Format(time.Kitchen) }, 93 | "ID": func() string { return nuid.Next() }, 94 | } 95 | 96 | if request != "" { 97 | funcMap["Request"] = func() string { return request } 98 | } 99 | 100 | templ, err := template.New("body").Funcs(funcMap).Parse(body) 101 | if err != nil { 102 | return []byte(body), err 103 | } 104 | 105 | var b bytes.Buffer 106 | err = templ.Execute(&b, &pubData{ 107 | Cnt: ctr, 108 | Count: ctr, 109 | Unix: now.Unix(), 110 | UnixNano: now.UnixNano(), 111 | TimeStamp: now.Format(time.RFC3339), 112 | Time: now.Format(time.Kitchen), 113 | Request: request, 114 | }) 115 | if err != nil { 116 | return []byte(body), err 117 | } 118 | 119 | return b.Bytes(), nil 120 | } 121 | 122 | const ( 123 | hdrLine = "NATS/1.0\r\n" 124 | crlf = "\r\n" 125 | hdrPreEnd = len(hdrLine) - len(crlf) 126 | statusLen = 3 127 | statusHdr = "Status" 128 | descrHdr = "Description" 129 | ) 130 | 131 | // DecodeHeadersMsg parses the data that includes headers into a header. Copied from nats.go 132 | func DecodeHeadersMsg(data []byte) (nats.Header, error) { 133 | tp := textproto.NewReader(bufio.NewReader(bytes.NewReader(data))) 134 | l, err := tp.ReadLine() 135 | if err != nil || len(l) < hdrPreEnd || l[:hdrPreEnd] != hdrLine[:hdrPreEnd] { 136 | return nil, nats.ErrBadHeaderMsg 137 | } 138 | 139 | mh, err := readMIMEHeader(tp) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // Check if we have an inlined status. 145 | if len(l) > hdrPreEnd { 146 | var description string 147 | status := strings.TrimSpace(l[hdrPreEnd:]) 148 | if len(status) != statusLen { 149 | description = strings.TrimSpace(status[statusLen:]) 150 | status = status[:statusLen] 151 | } 152 | mh.Add(statusHdr, status) 153 | if len(description) > 0 { 154 | mh.Add(descrHdr, description) 155 | } 156 | } 157 | return nats.Header(mh), nil 158 | } 159 | 160 | // copied from nats.go 161 | func readMIMEHeader(tp *textproto.Reader) (textproto.MIMEHeader, error) { 162 | m := make(textproto.MIMEHeader) 163 | for { 164 | kv, err := tp.ReadLine() 165 | if len(kv) == 0 { 166 | return m, err 167 | } 168 | 169 | // Process key fetching original case. 170 | i := bytes.IndexByte([]byte(kv), ':') 171 | if i < 0 { 172 | return nil, nats.ErrBadHeaderMsg 173 | } 174 | key := kv[:i] 175 | if key == "" { 176 | // Skip empty keys. 177 | continue 178 | } 179 | i++ 180 | for i < len(kv) && (kv[i] == ' ' || kv[i] == '\t') { 181 | i++ 182 | } 183 | value := string(kv[i:]) 184 | m[key] = append(m[key], value) 185 | if err != nil { 186 | return m, err 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /internal/util/jetstream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "github.com/nats-io/jsm.go/api" 18 | "github.com/nats-io/natscli/columns" 19 | ) 20 | 21 | // RenderMetaApi draws the _nats.* metadata on streams and consumers 22 | func RenderMetaApi(cols *columns.Writer, metadata map[string]string) { 23 | versionMeta := metadata[api.JSMetaCurrentServerVersion] 24 | levelMeta := metadata[api.JSMetaCurrentServerLevel] 25 | requiredMeta := metadata[api.JsMetaRequiredServerLevel] 26 | 27 | if versionMeta != "" || levelMeta != "" || requiredMeta != "" { 28 | if versionMeta != "" { 29 | cols.AddRow("Host Version", versionMeta) 30 | } 31 | 32 | if levelMeta != "" || requiredMeta != "" { 33 | if requiredMeta == "" { 34 | requiredMeta = "0" 35 | } 36 | cols.AddRowf("Required API Level", "%s hosted at level %s", requiredMeta, levelMeta) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/progress.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "github.com/dustin/go-humanize" 18 | "github.com/jedib0t/go-pretty/v6/progress" 19 | "github.com/jedib0t/go-pretty/v6/text" 20 | "github.com/nats-io/natscli/options" 21 | "time" 22 | ) 23 | 24 | var ProgressUnitsIBytes = progress.Units{ 25 | Notation: "", 26 | NotationPosition: progress.UnitsNotationPositionBefore, 27 | Formatter: func(v int64) string { return humanize.IBytes(uint64(v)) }, 28 | } 29 | 30 | func NewProgress(opts *options.Options, tracker *progress.Tracker) (progress.Writer, *progress.Tracker, error) { 31 | progbar := progress.NewWriter() 32 | natsStyle := progress.StyleBlocks 33 | natsStyle.Visibility.ETA = false 34 | natsStyle.Visibility.Speed = true 35 | natsStyle.Colors.Tracker = contextColor(opts) 36 | natsStyle.Options.Separator = " " 37 | natsStyle.Options.TimeInProgressPrecision = time.Millisecond 38 | progbar.SetStyle(natsStyle) 39 | progbar.SetAutoStop(true) 40 | pw := ProgressWidth() 41 | progbar.SetTrackerLength(pw / 2) 42 | progbar.SetNumTrackersExpected(1) 43 | progbar.SetUpdateFrequency(250 * time.Millisecond) 44 | 45 | progbar.AppendTracker(tracker) 46 | go progbar.Render() 47 | 48 | return progbar, tracker, nil 49 | } 50 | 51 | func contextColor(opts *options.Options) text.Colors { 52 | cs := opts.Config.ColorScheme() 53 | s, ok := styles[cs] 54 | if !ok { 55 | return text.Colors{text.FgWhite} 56 | } 57 | return s.Color.Border 58 | } 59 | -------------------------------------------------------------------------------- /internal/util/random.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import "math/rand/v2" 17 | 18 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 19 | var passwordRunes = append(letterRunes, []rune("@#_-%^&()")...) 20 | 21 | // RandomPassword generates a random string like RandomString() but includes some special characters 22 | func RandomPassword(length int) string { 23 | b := make([]rune, length) 24 | for i := range b { 25 | b[i] = passwordRunes[rand.IntN(len(passwordRunes))] 26 | } 27 | 28 | return string(b) 29 | } 30 | 31 | // RandomString generates a random string that includes only a-zA-Z0-9 32 | func RandomString(shortest uint, longest uint) string { 33 | if shortest > longest { 34 | shortest, longest = longest, shortest 35 | } 36 | 37 | var desired int 38 | 39 | switch { 40 | case int(longest)-int(shortest) < 0: 41 | desired = int(shortest) + rand.IntN(int(longest)) 42 | case longest == shortest: 43 | desired = int(shortest) 44 | default: 45 | desired = int(shortest) + rand.IntN(int(longest-shortest)) 46 | } 47 | 48 | b := make([]rune, desired) 49 | for i := range b { 50 | b[i] = letterRunes[rand.IntN(len(letterRunes))] 51 | } 52 | 53 | return string(b) 54 | } 55 | -------------------------------------------------------------------------------- /internal/util/random_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import "testing" 17 | 18 | func TestRandomString(t *testing.T) { 19 | for i := 0; i < 1000; i++ { 20 | if len(RandomString(1024, 1024)) != 1024 { 21 | t.Fatalf("got a !1024 length string") 22 | } 23 | } 24 | 25 | for i := 0; i < 1000; i++ { 26 | n := RandomString(2024, 1024) 27 | if len(n) > 2024 { 28 | t.Fatalf("got a > 2024 length string") 29 | } 30 | 31 | if len(n) < 1024 { 32 | t.Fatalf("got a < 1024 length string (%d)", len(n)) 33 | } 34 | } 35 | 36 | for i := 0; i < 1000; i++ { 37 | n := RandomString(1024, 2024) 38 | if len(n) > 2024 { 39 | t.Fatalf("got a > 2024 length string") 40 | } 41 | 42 | if len(n) < 1024 { 43 | t.Fatalf("got a < 1024 length string (%d)", len(n)) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/util/tables.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "sort" 20 | 21 | "github.com/jedib0t/go-pretty/v6/table" 22 | "github.com/jedib0t/go-pretty/v6/text" 23 | "github.com/nats-io/natscli/options" 24 | terminal "golang.org/x/term" 25 | ) 26 | 27 | type Table struct { 28 | writer table.Writer 29 | } 30 | 31 | var styles = map[string]table.Style{ 32 | "": table.StyleRounded, 33 | "rounded": table.StyleRounded, 34 | "double": table.StyleDouble, 35 | "yellow": coloredBorderStyle(text.FgYellow), 36 | "blue": coloredBorderStyle(text.FgBlue), 37 | "cyan": coloredBorderStyle(text.FgCyan), 38 | "green": coloredBorderStyle(text.FgGreen), 39 | "magenta": coloredBorderStyle(text.FgMagenta), 40 | "red": coloredBorderStyle(text.FgRed), 41 | } 42 | 43 | func coloredBorderStyle(c text.Color) table.Style { 44 | s := table.StyleRounded 45 | s.Color.Border = text.Colors{c} 46 | s.Color.Separator = text.Colors{c} 47 | s.Format.Footer = text.FormatDefault 48 | 49 | return s 50 | } 51 | 52 | // ValidStyles are valid color styles this package supports 53 | func ValidStyles() []string { 54 | var res []string 55 | 56 | for k := range styles { 57 | if k == "" { 58 | continue 59 | } 60 | 61 | res = append(res, k) 62 | } 63 | 64 | sort.Strings(res) 65 | 66 | return res 67 | } 68 | 69 | func (t *Table) AddHeaders(items ...any) { 70 | t.writer.AppendHeader(items) 71 | } 72 | 73 | func (t *Table) AddFooter(items ...any) { 74 | t.writer.AppendFooter(items) 75 | } 76 | 77 | func (t *Table) AddSeparator() { 78 | t.writer.AppendSeparator() 79 | } 80 | 81 | func (t *Table) AddRow(items ...any) { 82 | t.writer.AppendRow(items) 83 | } 84 | 85 | func (t *Table) Render() string { 86 | return fmt.Sprintln(t.writer.Render()) 87 | } 88 | 89 | func (t *Table) RenderCSV() string { 90 | return fmt.Sprintln(t.writer.RenderCSV()) 91 | } 92 | 93 | func NewTableWriter(opts *options.Options, format string, a ...any) *Table { 94 | tbl := &Table{ 95 | writer: table.NewWriter(), 96 | } 97 | 98 | tbl.writer.SuppressTrailingSpaces() 99 | tbl.writer.SetStyle(styles["rounded"]) 100 | 101 | if terminal.IsTerminal(int(os.Stdout.Fd())) { 102 | if opts.Config != nil { 103 | style, ok := styles[opts.Config.ColorScheme()] 104 | if ok { 105 | tbl.writer.SetStyle(style) 106 | } 107 | } 108 | w, _, _ := terminal.GetSize(int(os.Stdout.Fd())) 109 | if w > 0 { 110 | tbl.writer.Style().Size.WidthMax = w 111 | } 112 | } 113 | 114 | tbl.writer.Style().Title.Align = text.AlignCenter 115 | tbl.writer.Style().Format.Header = text.FormatDefault 116 | tbl.writer.Style().Format.Footer = text.FormatDefault 117 | 118 | if format != "" { 119 | tbl.writer.SetTitle(fmt.Sprintf(format, a...)) 120 | } 121 | 122 | return tbl 123 | } 124 | -------------------------------------------------------------------------------- /nats/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | iu "github.com/nats-io/natscli/internal/util" 18 | "log" 19 | "os" 20 | "runtime" 21 | "runtime/debug" 22 | 23 | "github.com/choria-io/fisk" 24 | "github.com/nats-io/natscli/plugins" 25 | 26 | "github.com/nats-io/natscli/cli" 27 | ) 28 | 29 | var version = "development" 30 | 31 | func main() { 32 | help := `NATS Utility 33 | 34 | NATS Server and JetStream administration. 35 | 36 | See 'nats cheat' for a quick cheatsheet of commands` 37 | 38 | ncli := fisk.New("nats", help) 39 | ncli.Author("NATS Authors ") 40 | ncli.UsageWriter(os.Stdout) 41 | ncli.Version(getVersion()) 42 | ncli.HelpFlag.Short('h') 43 | ncli.WithCheats().CheatCommand.Hidden() 44 | 45 | opts, err := cli.ConfigureInApp(ncli, nil, true) 46 | if err != nil { 47 | return 48 | } 49 | cli.SetVersion(getVersion()) 50 | 51 | ncli.Flag("server", "NATS server urls").Short('s').Envar("NATS_URL").PlaceHolder("URL").StringVar(&opts.Servers) 52 | ncli.Flag("user", "Username or Token").Envar("NATS_USER").PlaceHolder("USER").StringVar(&opts.Username) 53 | ncli.Flag("password", "Password").Envar("NATS_PASSWORD").PlaceHolder("PASSWORD").StringVar(&opts.Password) 54 | ncli.Flag("token", "Token").Envar("NATS_TOKEN").PlaceHolder("TOKEN").StringVar(&opts.Token) 55 | ncli.Flag("connection-name", "Nickname to use for the underlying NATS Connection").Default("NATS CLI Version " + getVersion()).PlaceHolder("NAME").StringVar(&opts.ConnectionName) 56 | ncli.Flag("creds", "User credentials").Envar("NATS_CREDS").PlaceHolder("FILE").StringVar(&opts.Creds) 57 | ncli.Flag("nkey", "User NKEY").Envar("NATS_NKEY").PlaceHolder("FILE").StringVar(&opts.Nkey) 58 | ncli.Flag("tlscert", "TLS public certificate").Envar("NATS_CERT").PlaceHolder("FILE").ExistingFileVar(&opts.TlsCert) 59 | ncli.Flag("tlskey", "TLS private key").Envar("NATS_KEY").PlaceHolder("FILE").ExistingFileVar(&opts.TlsKey) 60 | ncli.Flag("tlsca", "TLS certificate authority chain").Envar("NATS_CA").PlaceHolder("FILE").ExistingFileVar(&opts.TlsCA) 61 | ncli.Flag("tlsfirst", "Perform TLS handshake before expecting the server greeting").BoolVar(&opts.TlsFirst) 62 | if runtime.GOOS == "windows" { 63 | ncli.Flag("certstore", "Uses a Windows Certificate Store for TLS (user, machine)").PlaceHolder("TYPE").EnumVar(&opts.WinCertStoreType, "user", "windowscurrentuser", "machine", "windowslocalmachine") 64 | ncli.Flag("certstore-match", "Which certificate to use in the store").PlaceHolder("QUERY").StringVar(&opts.WinCertStoreMatch) 65 | ncli.Flag("certstore-match-by", "Configures the way certificates are searched for (subject, issuer)").PlaceHolder("MATCH").Default("subject").EnumVar(&opts.WinCertStoreMatchBy, "subject", "issuer") 66 | ncli.Flag("certstore-ca-match", "Which certificate authority should be used from the store").StringsVar(&opts.WinCertCaStoreMatch) 67 | } 68 | ncli.Flag("timeout", "Time to wait on responses from NATS").Default("5s").Envar("NATS_TIMEOUT").PlaceHolder("DURATION").DurationVar(&opts.Timeout) 69 | ncli.Flag("socks-proxy", "SOCKS5 proxy for connecting to NATS server").Envar("NATS_SOCKS_PROXY").PlaceHolder("PROXY").StringVar(&opts.SocksProxy) 70 | ncli.Flag("js-api-prefix", "Subject prefix for access to JetStream API").PlaceHolder("PREFIX").StringVar(&opts.JsApiPrefix) 71 | ncli.Flag("js-event-prefix", "Subject prefix for access to JetStream Advisories").PlaceHolder("PREFIX").StringVar(&opts.JsEventPrefix) 72 | ncli.Flag("js-domain", "JetStream domain to access").PlaceHolder("DOMAIN").StringVar(&opts.JsDomain) 73 | ncli.Flag("inbox-prefix", "Custom inbox prefix to use for inboxes").PlaceHolder("PREFIX").StringVar(&opts.InboxPrefix) 74 | ncli.Flag("domain", "JetStream domain to access").PlaceHolder("DOMAIN").Hidden().StringVar(&opts.JsDomain) 75 | ncli.Flag("colors", "Sets a color scheme to use").PlaceHolder("SCHEME").Envar("NATS_COLOR").EnumVar(&opts.ColorScheme, iu.ValidStyles()...) 76 | ncli.Flag("context", "Configuration context").Envar("NATS_CONTEXT").PlaceHolder("NAME").StringVar(&opts.CfgCtx) 77 | ncli.Flag("trace", "Trace API interactions").UnNegatableBoolVar(&opts.Trace) 78 | ncli.Flag("no-context", "Disable the selected context").UnNegatableBoolVar(&cli.SkipContexts) 79 | 80 | log.SetFlags(log.Ltime) 81 | 82 | plugins.AddToApp(ncli) 83 | 84 | ncli.MustParseWithUsage(os.Args[1:]) 85 | } 86 | 87 | func getVersion() string { 88 | if version != "development" { 89 | return version 90 | } 91 | 92 | nfo, ok := debug.ReadBuildInfo() 93 | if !ok || (nfo != nil && nfo.Main.Version == "") { 94 | return version 95 | } 96 | 97 | return nfo.Main.Version 98 | } 99 | -------------------------------------------------------------------------------- /nats/tests/auth_command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | "regexp" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | const ( 26 | TEST_DIR = "/tmp/natscli/auth_command_test" 27 | ) 28 | 29 | var ( 30 | JSON = ` 31 | { 32 | "test.a": [ 33 | { 34 | "subject": "test.b", 35 | "weight": 100, 36 | "cluster": "test_cluster" 37 | } 38 | ] 39 | } 40 | ` 41 | 42 | YAML = ` 43 | test.a: 44 | - subject: test.b 45 | weight: 100 46 | cluster: test_cluster 47 | ` 48 | ) 49 | 50 | func setup(operator, account string, t *testing.T) { 51 | teardown(t) 52 | err := os.Setenv("XDG_CONFIG_HOME", TEST_DIR) 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | err = os.Setenv("XDG_DATA_HOME", TEST_DIR) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | runNatsCli(t, fmt.Sprintf("auth operator add %s", operator)) 62 | runNatsCli(t, fmt.Sprintf("auth account add --operator=%s --defaults %s", operator, account)) 63 | } 64 | 65 | func teardown(t *testing.T) { 66 | err := os.RemoveAll(TEST_DIR) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | err = os.Unsetenv("NSC_HOME") 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | } 75 | 76 | func TestMapping(t *testing.T) { 77 | t.Run("--add", func(t *testing.T) { 78 | accountName, operatorName := "test_account", "test_operator" 79 | setup(operatorName, accountName, t) 80 | t.Cleanup(func() { 81 | teardown(t) 82 | }) 83 | 84 | fields := map[string]any{ 85 | "Configuration": map[string]any{ 86 | "Source": "test.a", 87 | "Target": "test.b", 88 | "Weight": "100", 89 | "Total weight": "100", 90 | }, 91 | } 92 | 93 | output := string(runNatsCli(t, fmt.Sprintf("auth account mappings add %s test.a test.b 100 --operator=%s", accountName, operatorName))) 94 | err := expectMatchJSON(t, output, fields) 95 | if err != nil { 96 | t.Errorf("failed to add account: %s. %s", err, output) 97 | } 98 | }) 99 | 100 | t.Run("--add from config", func(t *testing.T) { 101 | tests := []struct { 102 | name string 103 | fileExt string 104 | data string 105 | }{ 106 | {"--add from config json", "json", JSON}, 107 | {"--add from config yaml", "yaml", YAML}, 108 | } 109 | 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | accountName, operatorName := "test_account", "test_operator" 113 | setup(operatorName, accountName, t) 114 | t.Cleanup(func() { teardown(t) }) 115 | 116 | fields := map[string]any{ 117 | "Configuration": map[string]any{ 118 | "Source": "test.a", 119 | "Target": "test.b", 120 | "Weight": "100", 121 | "Total weight": "100", 122 | "Cluster": "test_cluster", 123 | }, 124 | } 125 | 126 | fp := filepath.Join(TEST_DIR, fmt.Sprintf("test.%s", tt.fileExt)) 127 | file, err := os.OpenFile(fp, os.O_CREATE|os.O_WRONLY, 0644) 128 | if err != nil { 129 | t.Fatalf("Error opening file: %s", err) 130 | } 131 | defer file.Close() 132 | 133 | _, err = file.WriteString(strings.TrimSpace(tt.data)) 134 | if err != nil { 135 | t.Fatalf("Error writing to file: %s", err) 136 | } 137 | 138 | output := string(runNatsCli(t, fmt.Sprintf("auth account mappings add %s --operator=%s --config='%s'", accountName, operatorName, fp))) 139 | 140 | err = expectMatchJSON(t, output, fields) 141 | if err != nil { 142 | t.Errorf("failed to add account: %s. %s", err, output) 143 | } 144 | }) 145 | } 146 | }) 147 | t.Run("--ls", func(t *testing.T) { 148 | accountName, operatorName := "test_account", "test_operator" 149 | setup(operatorName, accountName, t) 150 | t.Cleanup(func() { 151 | teardown(t) 152 | }) 153 | 154 | colums := map[string]*regexp.Regexp{ 155 | "top": regexp.MustCompile("Subject mappings for account test_account"), 156 | "middle": regexp.MustCompile("Source Subject │ Target Subject │ Weight │ Cluster"), 157 | "bottom": regexp.MustCompile("test.a │ test.b │ 100"), 158 | } 159 | 160 | runNatsCli(t, fmt.Sprintf("auth account mappings add %s test.a test.b 100 --operator=%s", accountName, operatorName)) 161 | output := runNatsCli(t, fmt.Sprintf("auth account mappings ls %s --operator=%s", accountName, operatorName)) 162 | 163 | for name, pattern := range colums { 164 | if !pattern.Match(output) { 165 | t.Errorf("%s value does not match expected %s", name, pattern) 166 | } 167 | } 168 | }) 169 | 170 | t.Run("--info", func(t *testing.T) { 171 | accountName, operatorName := "test_account", "test_operator" 172 | setup(operatorName, accountName, t) 173 | t.Cleanup(func() { 174 | teardown(t) 175 | }) 176 | 177 | fields := map[string]any{ 178 | "Configuration": map[string]any{ 179 | "Source": "test.a", 180 | "Target": "test.b", 181 | "Weight": "100", 182 | "Total weight": "100", 183 | }, 184 | } 185 | 186 | runNatsCli(t, fmt.Sprintf("auth account mappings add %s test.a test.b 100 --operator=%s", accountName, operatorName)) 187 | output := string(runNatsCli(t, fmt.Sprintf("auth account mappings info %s test.a --operator=%s", accountName, operatorName))) 188 | 189 | err := expectMatchJSON(t, output, fields) 190 | if err != nil { 191 | t.Errorf("failed to get account info: %s. %s", err, output) 192 | } 193 | }) 194 | 195 | t.Run("--delete", func(t *testing.T) { 196 | accountName, operatorName := "test_account", "test_operator" 197 | setup(operatorName, accountName, t) 198 | t.Cleanup(func() { 199 | teardown(t) 200 | }) 201 | 202 | expected := regexp.MustCompile("Deleted mapping {test.a}") 203 | 204 | runNatsCli(t, fmt.Sprintf("auth account mappings add %s test.a test.b 100 --operator=%s", accountName, operatorName)) 205 | output := runNatsCli(t, fmt.Sprintf("auth account mappings rm %s test.a --operator=%s", accountName, operatorName)) 206 | 207 | if !expected.Match(output) { 208 | t.Errorf("failed to delete mapping: %s", output) 209 | } 210 | }) 211 | } 212 | -------------------------------------------------------------------------------- /nats/tests/nats_nix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "os/exec" 9 | "strings" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | func runCommand(cmd string, input string, args ...string) ([]byte, error) { 15 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 16 | defer cancel() 17 | 18 | execution := exec.Command(cmd, args...) 19 | 20 | execution.SysProcAttr = &syscall.SysProcAttr{ 21 | Setpgid: true, 22 | } 23 | 24 | if input != "" { 25 | execution.Stdin = strings.NewReader(input) 26 | } 27 | 28 | type result struct { 29 | out []byte 30 | err error 31 | } 32 | 33 | resCh := make(chan result, 1) 34 | 35 | go func() { 36 | out, err := execution.CombinedOutput() 37 | resCh <- result{out: out, err: err} 38 | }() 39 | 40 | select { 41 | case <-ctx.Done(): 42 | if execution.Process != nil { 43 | _ = syscall.Kill(-execution.Process.Pid, syscall.SIGKILL) 44 | } 45 | return nil, fmt.Errorf("nats utility timed out") 46 | case res := <-resCh: 47 | if res.err != nil { 48 | return nil, fmt.Errorf("nats utility failed: %v\n%v", res.err, string(res.out)) 49 | } 50 | return res.out, nil 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /nats/tests/nats_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func runCommand(cmd string, input string, args ...string) ([]byte, error) { 16 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 17 | defer cancel() 18 | 19 | execution := exec.Command(cmd, args...) 20 | execution.SysProcAttr = &syscall.SysProcAttr{ 21 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, 22 | } 23 | 24 | if input != "" { 25 | execution.Stdin = strings.NewReader(input) 26 | } 27 | 28 | type result struct { 29 | out []byte 30 | err error 31 | } 32 | 33 | resCh := make(chan result, 1) 34 | 35 | go func() { 36 | out, err := execution.CombinedOutput() 37 | resCh <- result{out: out, err: err} 38 | }() 39 | 40 | select { 41 | case <-ctx.Done(): 42 | if execution.Process != nil { 43 | killCmd := exec.Command("cmd", "/c", "taskkill", "/F", "/T", "/PID", strconv.Itoa(execution.Process.Pid)) 44 | _ = killCmd.Run() 45 | } 46 | return nil, fmt.Errorf("nats utility timed out") 47 | case res := <-resCh: 48 | if res.err != nil { 49 | return nil, fmt.Errorf("nats utility failed: %v\n%v", res.err, string(res.out)) 50 | } 51 | return res.out, nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /nats/tests/pub_command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/nats-io/jsm.go" 9 | "github.com/nats-io/nats-server/v2/server" 10 | "github.com/nats-io/nats.go" 11 | ) 12 | 13 | func TestCLIPubSendOnNewline(t *testing.T) { 14 | t.Run("Publish with body argument", func(t *testing.T) { 15 | withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error { 16 | subject := "test-body" 17 | var messages []string 18 | expected := "Test Message" 19 | sub, _ := nc.Subscribe(subject, func(m *nats.Msg) { 20 | messages = append(messages, string(m.Data)) 21 | }) 22 | 23 | runNatsCli(t, fmt.Sprintf("--server='%s' pub --send-on newline %s '%s'", srv.ClientURL(), subject, expected)) 24 | _ = sub.Unsubscribe() 25 | 26 | if len(messages) != 1 { 27 | t.Errorf("expected 1 message and received %d", len(messages)) 28 | } 29 | if len(messages) > 0 && messages[0] != expected { 30 | t.Errorf("expected message %q got %q", expected, messages[0]) 31 | } 32 | return nil 33 | }) 34 | }) 35 | 36 | t.Run("Publish with body argument and --quiet", func(t *testing.T) { 37 | withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error { 38 | subject := "test-quiet" 39 | var messages []string 40 | expected := "Test Message" 41 | sub, _ := nc.Subscribe(subject, func(m *nats.Msg) { 42 | messages = append(messages, string(m.Data)) 43 | }) 44 | 45 | out := runNatsCli(t, fmt.Sprintf("--server='%s' pub --send-on newline -q %s '%s'", srv.ClientURL(), subject, expected)) 46 | _ = sub.Unsubscribe() 47 | 48 | if len(out) != 0 { 49 | t.Errorf("expected cli to output to be 0 but got %d\n %s", len(out), out) 50 | } 51 | if len(messages) != 1 { 52 | t.Errorf("expected 1 message and received %d", len(messages)) 53 | } 54 | if len(messages) > 0 && messages[0] != expected { 55 | t.Errorf("expected message %q got %q", expected, messages[0]) 56 | } 57 | return nil 58 | }) 59 | }) 60 | 61 | t.Run("Publish --send-on newline from stdin", func(t *testing.T) { 62 | withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error { 63 | subject := "test-sendon" 64 | var messages []string 65 | expected := []string{"test", "pub", "input"} 66 | sub, _ := nc.Subscribe(subject, func(m *nats.Msg) { 67 | messages = append(messages, string(m.Data)) 68 | }) 69 | 70 | // --force-stdin required for testing as a terminal is not present 71 | runNatsCliWithInput(t, strings.Join(expected, "\n"), fmt.Sprintf("--server='%s' pub --send-on newline --force-stdin %s", srv.ClientURL(), subject)) 72 | _ = sub.Unsubscribe() 73 | 74 | if len(messages) != len(expected) { 75 | t.Errorf("expected %d messages and received %d", len(expected), len(messages)) 76 | } 77 | for i, msg := range messages { 78 | if messages[i] != expected[i] { 79 | t.Errorf("expected message(%d) %q got %q", i, expected[i], msg) 80 | } 81 | } 82 | return nil 83 | }) 84 | }) 85 | 86 | t.Run("Publish --send-on eof from stdin", func(t *testing.T) { 87 | withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error { 88 | subject := "test-eof" 89 | var messages []string 90 | expected := "test\npub\ninput" 91 | sub, _ := nc.Subscribe(subject, func(m *nats.Msg) { 92 | messages = append(messages, string(m.Data)) 93 | }) 94 | 95 | // --force-stdin required for testing as a terminal is not present 96 | runNatsCliWithInput(t, expected, fmt.Sprintf("--server='%s' pub --force-stdin %s", srv.ClientURL(), subject)) 97 | _ = sub.Unsubscribe() 98 | 99 | if len(messages) != 1 { 100 | t.Errorf("expected 1 message and received %d", len(messages)) 101 | } 102 | if len(messages) > 0 && messages[0] != expected { 103 | t.Errorf("expected message %q got %q", expected, messages[0]) 104 | } 105 | return nil 106 | }) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /nats/tests/service_command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/nats-io/nats.go/micro" 9 | ) 10 | 11 | func TestServiceInfo(t *testing.T) { 12 | srv, _, _ := setupJStreamTest(t) 13 | defer srv.Shutdown() 14 | nc, _, _ := prepareHelper(srv.ClientURL()) 15 | 16 | svc, err := micro.AddService(nc, micro.Config{ 17 | Name: "svc", 18 | Version: "1.0.0", 19 | }) 20 | if err != nil { 21 | t.Errorf("failed to add service: %v", err) 22 | } 23 | defer svc.Stop() 24 | 25 | err = svc.AddEndpoint("ep1", micro.HandlerFunc(func(req micro.Request) { 26 | req.Respond(nil) 27 | })) 28 | if err != nil { 29 | t.Errorf("failed to add service endpoint: %v", err) 30 | } 31 | 32 | err = svc.AddEndpoint("ep2", micro.HandlerFunc(func(req micro.Request) { 33 | req.Respond(nil) 34 | })) 35 | if err != nil { 36 | t.Errorf("failed to add service endpoint: %v", err) 37 | } 38 | 39 | t.Run("Info", func(t *testing.T) { 40 | output := runNatsCli(t, fmt.Sprintf("--server='%s' service info svc --json", srv.ClientURL())) 41 | 42 | var resp struct { 43 | Info *micro.Info `json:"info"` 44 | Stats *micro.Stats `json:"stats"` 45 | } 46 | err = json.Unmarshal(output, &resp) 47 | if err != nil { 48 | t.Errorf("failed to unmarshal response: %v", err) 49 | } 50 | 51 | if resp.Info.Name != "svc" { 52 | t.Errorf("expected service name to be svc, got %s", resp.Info.Name) 53 | } 54 | 55 | if resp.Info.Endpoints[0].Name != "ep1" { 56 | t.Errorf("expected endpoint name to be ep1, got %s", resp.Info.Endpoints[0].Name) 57 | } 58 | 59 | if resp.Info.Endpoints[1].Name != "ep2" { 60 | t.Errorf("expected endpoint name to be ep2, got %s", resp.Info.Endpoints[1].Name) 61 | } 62 | }) 63 | 64 | t.Run("Info with endpoint filter", func(t *testing.T) { 65 | output := runNatsCli(t, fmt.Sprintf("--server='%s' service info svc --json --endpoint='.*2'", srv.ClientURL())) 66 | 67 | var resp struct { 68 | Info *micro.Info `json:"info"` 69 | Stats *micro.Stats `json:"stats"` 70 | } 71 | err = json.Unmarshal(output, &resp) 72 | if err != nil { 73 | t.Errorf("failed to unmarshal response: %v", err) 74 | } 75 | 76 | if resp.Info.Name != "svc" { 77 | t.Errorf("expected service name to be svc, got %s", resp.Info.Name) 78 | } 79 | 80 | if len(resp.Info.Endpoints) != 1 { 81 | t.Errorf("expected 1 endpoint, got %d", len(resp.Info.Endpoints)) 82 | } 83 | 84 | if resp.Info.Endpoints[0].Name != "ep2" { 85 | t.Errorf("expected endpoint name to be ep2, got %s", resp.Info.Endpoints[0].Name) 86 | } 87 | }) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /nats/tests/testdata/ORDERS_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "subjects": [ 3 | "ORDERS.*" 4 | ], 5 | "retention": "limits", 6 | "max_consumers": -1, 7 | "max_msgs": -1, 8 | "max_bytes": -1, 9 | "max_age": 31536000000000000, 10 | "max_msg_size": -1, 11 | "storage": "file", 12 | "num_replicas": 1, 13 | "allow_msg_ttl": true, 14 | "duplicate_window": 3600000000000 15 | } 16 | -------------------------------------------------------------------------------- /nats/tests/testdata/mem1_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mem1", 3 | "subjects": [ 4 | "MEMORY.*" 5 | ], 6 | "retention": "limits", 7 | "max_consumers": -1, 8 | "max_msgs": -1, 9 | "max_bytes": -1, 10 | "max_age": 0, 11 | "max_msg_size": -1, 12 | "storage": "memory", 13 | "num_replicas": 1 14 | } 15 | -------------------------------------------------------------------------------- /nats/tests/testdata/mem1_pull1_consumer.json: -------------------------------------------------------------------------------- 1 | { 2 | "delivery_subject": "out.mem1.pull1", 3 | "durable_name": "pull1", 4 | "deliver_subject": "_IB.x", 5 | "start_time": "0001-01-01T00:00:00Z", 6 | "deliver_policy": "all", 7 | "ack_policy": "explicit", 8 | "ack_wait": 30000000000, 9 | "max_deliver": 20, 10 | "replay_policy": "instant" 11 | } 12 | -------------------------------------------------------------------------------- /options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package options 15 | 16 | import ( 17 | "time" 18 | 19 | "github.com/nats-io/jsm.go" 20 | "github.com/nats-io/jsm.go/natscontext" 21 | "github.com/nats-io/nats.go" 22 | "github.com/nats-io/nats.go/jetstream" 23 | ) 24 | 25 | var DefaultOptions *Options 26 | 27 | // Options configure the CLI 28 | type Options struct { 29 | // Config is a nats configuration context 30 | Config *natscontext.Context 31 | // Servers is the list of servers to connect to 32 | Servers string 33 | // Creds is nats credentials to authenticate with 34 | Creds string 35 | // TlsCert is the TLS Public Certificate 36 | TlsCert string 37 | // TlsKey is the TLS Private Key 38 | TlsKey string 39 | // TlsCA is the certificate authority to verify the connection with 40 | TlsCA string 41 | // Timeout is how long to wait for operations 42 | Timeout time.Duration 43 | // ConnectionName is the name to use for the underlying NATS connection 44 | ConnectionName string 45 | // Username is the username or token to connect with 46 | Username string 47 | // Password is the password to connect with 48 | Password string 49 | // Token is the token to connect with 50 | Token string 51 | // Nkey is the file holding a nkey to connect with 52 | Nkey string 53 | // JsApiPrefix is the JetStream API prefix 54 | JsApiPrefix string 55 | // JsEventPrefix is the JetStream events prefix 56 | JsEventPrefix string 57 | // JsDomain is the domain to connect to 58 | JsDomain string 59 | // CfgCtx is the context name to use 60 | CfgCtx string 61 | // Trace enables verbose debug logging 62 | Trace bool 63 | // Customer inbox Prefix 64 | InboxPrefix string 65 | // Conn sets a prepared connect to connect with 66 | Conn *nats.Conn 67 | // Mgr sets a prepared jsm Manager to use for JetStream access 68 | Mgr *jsm.Manager 69 | // JSc is a prepared NATS JetStream context to use for KV and Object access 70 | JSc jetstream.JetStream 71 | // Disables registering of CLI cheats 72 | NoCheats bool 73 | // PrometheusNamespace is the namespace to use for prometheus format output in server check 74 | PrometheusNamespace string 75 | // SocksProxy is a SOCKS5 proxy to use for NATS connections 76 | SocksProxy string 77 | // ColorScheme influence table colors and more based on ValidStyles() 78 | ColorScheme string 79 | // TlsFirst configures the TLSHandshakeFirst behavior in nats.go 80 | TlsFirst bool 81 | // WinCertStoreType enables windows cert store - user or machine 82 | WinCertStoreType string 83 | // WinCertStoreMatchBy configures how to search for certs when using match - subject or issuer 84 | WinCertStoreMatchBy string 85 | // WinCertStoreMatch is the query to match with 86 | WinCertStoreMatch string 87 | // WinCertCaStoreMatch is the queries for CAs to use 88 | WinCertCaStoreMatch []string 89 | } 90 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 The NATS Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package plugins 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "log" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "regexp" 24 | "strings" 25 | 26 | "github.com/choria-io/fisk" 27 | iu "github.com/nats-io/natscli/internal/util" 28 | ) 29 | 30 | var validNames = regexp.MustCompile(`^[a-z]+$`) 31 | 32 | type plugin struct { 33 | Cmd string `json:"cmd"` 34 | Definition json.RawMessage `json:"def"` 35 | } 36 | 37 | func AddToApp(app *fisk.Application) error { 38 | parent, err := pluginDir() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | entries, err := os.ReadDir(parent) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for _, entry := range entries { 49 | if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { 50 | continue 51 | } 52 | 53 | pb, err := os.ReadFile(filepath.Join(parent, entry.Name())) 54 | if err != nil { 55 | log.Printf("Could not read plugin %v: %v", entry.Name(), err) 56 | continue 57 | } 58 | 59 | var p plugin 60 | err = json.Unmarshal(pb, &p) 61 | if err != nil { 62 | log.Printf("Could not read plugin %v: %v", entry.Name(), err) 63 | continue 64 | } 65 | 66 | _, err = app.ExternalPluginCommand(p.Cmd, p.Definition, strings.TrimSuffix(entry.Name(), ".json"), "") 67 | if err != nil { 68 | log.Printf("Invalid plugin %v: %v", entry.Name(), err) 69 | continue 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func Register(name string, command string, force bool) error { 77 | if !validNames.MatchString(name) { 78 | return fmt.Errorf("plugins names must match ^[a-z]$") 79 | } 80 | 81 | cmd, err := filepath.Abs(command) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | store, err := pluginDir() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | pluginPath := filepath.Join(store, fmt.Sprintf("%s.json", name)) 92 | 93 | if !force { 94 | exist, _ := fileAccessible(pluginPath) 95 | if exist { 96 | return fmt.Errorf("plugins %s already registered, use --force to update", name) 97 | } 98 | } 99 | 100 | intro := exec.Command(cmd, "--fisk-introspect") 101 | out, err := intro.CombinedOutput() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | pj, err := json.Marshal(plugin{cmd, out}) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | err = os.WriteFile(pluginPath, pj, 0600) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func fileAccessible(f string) (bool, error) { 120 | stat, err := os.Stat(f) 121 | if err != nil { 122 | return false, err 123 | } 124 | 125 | if stat.IsDir() { 126 | return false, fmt.Errorf("is a directory") 127 | } 128 | 129 | file, err := os.Open(f) 130 | if err != nil { 131 | return false, err 132 | } 133 | file.Close() 134 | 135 | return true, nil 136 | } 137 | 138 | func pluginDir() (string, error) { 139 | parent, err := iu.XdgShareHome() 140 | if err != nil { 141 | return "", err 142 | } 143 | 144 | dir := filepath.Join(parent, "nats", "cli", "plugins") 145 | err = os.MkdirAll(dir, 0700) 146 | if err != nil { 147 | return "", err 148 | } 149 | 150 | return dir, nil 151 | } 152 | -------------------------------------------------------------------------------- /top/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 NATS Messaging System 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /top/toputils_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPsize(t *testing.T) { 9 | const kibibyte = 1024 10 | const mebibyte = 1024 * 1024 11 | const gibibyte = 1024 * 1024 * 1024 12 | 13 | type Args struct { 14 | displayRawBytes bool 15 | input int64 16 | } 17 | 18 | testcases := map[string]struct { 19 | args Args 20 | want string 21 | }{ 22 | "given input 1023 and display_raw_bytes false": { 23 | args: Args{ 24 | input: int64(1023), 25 | displayRawBytes: false, 26 | }, 27 | want: "1023", 28 | }, 29 | "given input kibibyte and display_raw_bytes false": { 30 | args: Args{ 31 | input: int64(kibibyte), 32 | displayRawBytes: false, 33 | }, 34 | want: "1.0K", 35 | }, 36 | "given input mebibyte and display_raw_bytes false": { 37 | args: Args{ 38 | input: int64(mebibyte), 39 | displayRawBytes: false, 40 | }, 41 | want: "1.0M", 42 | }, 43 | "given input gibibyte and display_raw_bytes false": { 44 | args: Args{ 45 | input: int64(gibibyte), 46 | displayRawBytes: false, 47 | }, 48 | want: "1.0G", 49 | }, 50 | 51 | "given input 1023 and display_raw_bytes true": { 52 | args: Args{ 53 | input: int64(1023), 54 | displayRawBytes: true, 55 | }, 56 | want: "1023", 57 | }, 58 | "given input kibibyte and display_raw_bytes true": { 59 | args: Args{ 60 | input: int64(kibibyte), 61 | displayRawBytes: true, 62 | }, 63 | want: fmt.Sprintf("%d", kibibyte), 64 | }, 65 | "given input mebibyte and display_raw_bytes true": { 66 | args: Args{ 67 | input: int64(mebibyte), 68 | displayRawBytes: true, 69 | }, 70 | want: fmt.Sprintf("%d", mebibyte), 71 | }, 72 | "given input gibibyte and display_raw_bytes true": { 73 | args: Args{ 74 | input: int64(gibibyte), 75 | displayRawBytes: true, 76 | }, 77 | want: fmt.Sprintf("%d", gibibyte), 78 | }, 79 | } 80 | 81 | for name, testcase := range testcases { 82 | t.Run(name, func(t *testing.T) { 83 | got := Psize(testcase.args.displayRawBytes, testcase.args.input) 84 | 85 | if got != testcase.want { 86 | t.Errorf("wanted %q, got %q", testcase.want, got) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestNsize(t *testing.T) { 93 | type Args struct { 94 | displayRawBytes bool 95 | input int64 96 | } 97 | 98 | testcases := map[string]struct { 99 | args Args 100 | want string 101 | }{ 102 | "given input 999 and display_raw_bytes false": { 103 | args: Args{ 104 | input: int64(999), 105 | displayRawBytes: false, 106 | }, 107 | want: "999", 108 | }, 109 | "given input 1000 and display_raw_bytes false": { 110 | args: Args{ 111 | input: int64(1000), 112 | displayRawBytes: false, 113 | }, 114 | want: "1.0K", 115 | }, 116 | "given input 1_000_000 and display_raw_bytes false": { 117 | args: Args{ 118 | input: int64(1_000_000), 119 | displayRawBytes: false, 120 | }, 121 | want: "1.0M", 122 | }, 123 | "given input 1_000_000_000 and display_raw_bytes false": { 124 | args: Args{ 125 | input: int64(1_000_000_000), 126 | displayRawBytes: false, 127 | }, 128 | want: "1.0B", 129 | }, 130 | "given input 1_000_000_000_000 and display_raw_bytes false": { 131 | args: Args{ 132 | input: int64(1_000_000_000_000), 133 | displayRawBytes: false, 134 | }, 135 | want: "1.0T", 136 | }, 137 | 138 | "given input 999 and display_raw_bytes true": { 139 | args: Args{ 140 | input: int64(999), 141 | displayRawBytes: true, 142 | }, 143 | want: "999", 144 | }, 145 | "given input 1000 and display_raw_bytes true": { 146 | args: Args{ 147 | input: int64(1000), 148 | displayRawBytes: true, 149 | }, 150 | want: "1000", 151 | }, 152 | "given input 1_000_000 and display_raw_bytes true": { 153 | args: Args{ 154 | input: int64(1_000_000), 155 | displayRawBytes: true, 156 | }, 157 | want: "1000000", 158 | }, 159 | "given input 1_000_000_000 and display_raw_bytes true": { 160 | args: Args{ 161 | input: int64(1_000_000_000), 162 | displayRawBytes: true, 163 | }, 164 | want: "1000000000", 165 | }, 166 | "given input 1_000_000_000_000 and display_raw_bytes true": { 167 | args: Args{ 168 | input: int64(1_000_000_000_000), 169 | displayRawBytes: true, 170 | }, 171 | want: "1000000000000", 172 | }, 173 | } 174 | 175 | for name, testcase := range testcases { 176 | t.Run(name, func(t *testing.T) { 177 | got := Nsize(testcase.args.displayRawBytes, testcase.args.input) 178 | 179 | if got != testcase.want { 180 | t.Errorf("wanted %q, got %q", testcase.want, got) 181 | } 182 | }) 183 | } 184 | } 185 | --------------------------------------------------------------------------------