diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edda880..de1f7fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [master] + branches: [master, v3] pull_request: - branches: [master] + branches: [master, v3] jobs: tests: @@ -12,12 +12,12 @@ jobs: steps: - name: Set up Go 1.x - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: ~1.17 + go-version: ~1.23 id: go - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test run: go test -timeout 1m ./... @@ -25,11 +25,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: ~1.17 + go-version: ~1.23 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - version: v1.33 + version: v2.5.0 diff --git a/.github/workflows/tags.yml b/.github/workflows/tags.yml index dc6ea02..23734b3 100644 --- a/.github/workflows/tags.yml +++ b/.github/workflows/tags.yml @@ -9,31 +9,18 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: ~1.17 + go-version: ~1.23 - name: Create release id: goreleaser - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@v6 with: version: latest - args: release --rm-dist + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Update install links - run: | - wget -q https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -O jq - chmod +x ./jq - tag=$(echo '${{steps.goreleaser.outputs.metadata}}' | ./jq --raw-output '.tag') - linux_name=$(echo '${{steps.goreleaser.outputs.artifacts}}' | ./jq --raw-output '.[] | select((.goos=="linux") and (.type=="Archive")) | .name') - mac_name=$(echo '${{steps.goreleaser.outputs.artifacts}}' | ./jq --raw-output '.[] | select((.goos=="darwin") and (.type=="Archive")) | .name') - win_name=$(echo '${{steps.goreleaser.outputs.artifacts}}' | ./jq --raw-output '.[] | select((.goos=="windows") and (.type=="Archive")) | .name') - download_url_prefix="https://github.com/${{github.repository}}/releases/download/${tag}" - short_url_api_prefix="https://go.enapter.com/rest/v3/short-urls" - curl -q -H "X-Api-Key: ${{secrets.ENAPTER_SHLINK_API_KEY}}" -H "Content-Type: application/json" -X PATCH ${short_url_api_prefix}/enaptercli-linux-install -d "{\"longUrl\":\"${download_url_prefix}/${linux_name}\"}" - curl -q -H "X-Api-Key: ${{secrets.ENAPTER_SHLINK_API_KEY}}" -H "Content-Type: application/json" -X PATCH ${short_url_api_prefix}/enaptercli-macos-install -d "{\"longUrl\":\"${download_url_prefix}/${mac_name}\"}" - curl -q -H "X-Api-Key: ${{secrets.ENAPTER_SHLINK_API_KEY}}" -H "Content-Type: application/json" -X PATCH ${short_url_api_prefix}/enaptercli-windows-install -d "{\"longUrl\":\"${download_url_prefix}/${win_name}\"}" + TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c8ac242..69518d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode -^enapter$ +enapter3 dist -.DS_Store \ No newline at end of file +.DS_Store +.env diff --git a/.golangci.yml b/.golangci.yml index 65ed0c1..f6ec344 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,22 +1,16 @@ -run: - timeout: 5m - +version: "2" linters: - disable-all: true + default: none enable: - asciicheck - bodyclose - - deadcode - - depguard + - copyloopvar - dogsled - - dupl + - err113 - errcheck - errorlint - exhaustive - # - exhaustivestruct - - exportloopref - funlen - - gci - gochecknoglobals - gochecknoinits - gocognit @@ -24,71 +18,72 @@ linters: - gocritic - gocyclo - godot - # - godox - - goerr113 - - gofmt - - gofumpt - goheader - - goimports - - golint - - gomnd - gomodguard - goprintffuncname - gosec - - gosimple - govet - ineffassign - # - interfacer # is prone to bad suggestions (officialy deprecated) - lll - - maligned - misspell + - mnd - nakedret - nestif - # - nlreturn - noctx - nolintlint - prealloc + - revive - rowserrcheck - - scopelint - sqlclosecheck - staticcheck - - structcheck - - stylecheck - testpackage - tparallel - - typecheck - unconvert - unparam - unused - - varcheck - whitespace - # - wrapcheck - # - wsl - -linters-settings: - lll: - line-length: 110 - gci: - local-prefixes: github.com/enapter/enapter-cli - -issues: - exclude-rules: - # Exclude gosec from running on tests files because this makes no sense. - - path: _test\.go - linters: - - gosec - - # Exclude lll issues for long lines with go:generate. - - linters: - - lll - source: "^//go:generate " - - # Import paths can be long. - - linters: - - lll - source: "^import " - - # Links to articles can be long. - - linters: - - lll - source: "//.*(http|https)://" + settings: + lll: + line-length: 110 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - gosec + path: _test\.go + - linters: + - lll + source: '^//go:generate ' + - linters: + - lll + source: '^import ' + - linters: + - lll + source: //.*(http|https):// + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/enapter/enapter-cli/) + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index be29afc..a12d9cb 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,26 +1,59 @@ ---- -project_name: enapter-cli +version: 2 -release: - github: - owner: enapter - name: enapter-cli +project_name: enapter-cli builds: - - binary: enapter + - binary: enapter3 + main: ./cmd/enapter/ + ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} + env: + - CGO_ENABLED=0 goos: - - darwin - - windows - linux + - windows + - darwin goarch: - amd64 - env: - - CGO_ENABLED=0 - main: ./cmd/enapter/ - ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} + - arm64 + ignore: + - goos: linux + goarch: arm64 + - goos: windows + goarch: arm64 + +release: + github: + owner: enapter + name: enapter-cli + +archives: + - formats: ['tar.gz'] + wrap_in_directory: true + format_overrides: + - goos: windows + formats: ['zip'] + name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}' checksum: name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' +snapshot: + version_template: 'SNAPSHOT-{{ .Tag }}' + changelog: - skip: true + disable: true + +brews: + - repository: + owner: enapter + name: homebrew-tap + token: "{{ .Env.TAP_GITHUB_TOKEN }}" + name: enapter@3 + directory: Formula + homepage: https://github.com/Enapter/enapter-cli + description: Command-line tool for Enapter Energy Management System Toolkit + + install: | + bin.install "enapter3" + test: | + assert_match "Enapter CLI #{version}", shell_output("#{bin}/enapter3 --version") diff --git a/README.md b/README.md index 4678657..5fc04fd 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,30 @@ [![Release](https://img.shields.io/github/release/enapter/enapter-cli.svg)](https://github.com/enapter/enapter-cli/releases/latest) -This tool helps Enapter customers to work with devices. It useful in the following cases: -1. Develop devices via blueprints. -2. Update and monitor devices. +This tool helps Enapter customers to work with devices it is alternative for [Enapter IDE for EMS Toolkit 3.0](https://marketplace.visualstudio.com/items?itemName=Enapter.enapter-ems-toolkit-ide). +It helpful in the following cases: + +1. Managing all your EMS setup as a code with Git and Ansible / Puppet +2. Establishing CI/CD workflow +3. Development and debugging of Enapter Blueprints +4. Development and debugging of Enapter Gateway Rules ## How to install ###  macOS - recommended +Version 1: + ```bash brew tap enapter/tap && brew install enapter ``` +Version 3: + +```bash +brew tap enapter/tap && brew install enapter@3 +``` + ### Get prebuilt binaries Choose your platform and required release on the [Releases page](https://github.com/Enapter/enapter-cli/releases). @@ -32,7 +44,10 @@ Also you can pass custom output path: ./build.sh /usr/local/bin/enapter ``` -## How to use +## How to use Version 1: + +> [!NOTE] +> Version 1 works only with Enapter Cloud connection. ### API token @@ -44,16 +59,43 @@ Enapter CLI requires access token for authentication. Obtaining of the token is 4. Follow the instructions on the screen -5. Set environment variable `ENAPTER_API_TOKEN` with new token. To make it permanent don't forget to add it to configuration files of your shell. +5. Set environment variable `ENAPTER3_API_TOKEN` with new token. To make it permanent don't forget to add it to configuration files of your shell. ```bash - export ENAPTER_API_TOKEN="your token" + export ENAPTER3_API_TOKEN="your token" ``` Please note that if you don't save your token, it is not possible to reveal it anymore. You need generate new token. +## How to use Version 3: + +### API token + +Enapter CLI requires access token for authentication. Obtaining of the token is easy and can be done by following few steps. + +1. Navigate to your Enapter Gateway 3.0 Web Interface `Settings` page by using IP address or mDNS name [http://enapter-gateway.local/settings](https://enapter-gateway.local/settings) +2. Enapter your Enapter Gateway password +3. Click `API Token` and copy token to clipboard +4. Set environment variables `ENAPTER3_API_TOKEN`, `ENAPTER3_API_URL` and `ENAPTER3_API_ALLOW_INSECURE`. To make it permanent don't forget to add it to configuration files of your shell. + + ```bash + export ENAPTER3_API_TOKEN="your token" + export ENAPTER3_API_URL="http://ip_address/api" + export ENAPTER3_API_ALLOW_INSECURE=true + ``` + +5. Check connection works by running + + ```bash + enapter3 device list + ``` + ### Autocompletion in your favourite terminal app -In order to make life easier with command line interface, you may use [Fig - the next-generation command line](https://fig.io/). This autocompletion tool has native support for the Enapter CLI for Mac OS X and Linux. +> [!NOTE] +> Available for Version 1 now. + +In order to make life easier with command line interface, you may use [Amazon Q](https://aws.amazon.com/q/). This autocompletion tool has native support for the Enapter CLI for Mac OS X and Linux. + + - \ No newline at end of file diff --git a/build.sh b/build.sh index 4b7a413..74fadde 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ set -ex -output=${1:-enapter} +output=${1:-enapter3} BUILD_VERSION=$(git describe --tag 2> /dev/null) BUILD_COMMIT=$(git rev-parse --short HEAD) diff --git a/go.mod b/go.mod index 865d5be..215c2f5 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,20 @@ module github.com/enapter/enapter-cli -go 1.19 +go 1.24.0 require ( - github.com/bxcodec/faker/v3 v3.5.0 - github.com/gorilla/websocket v1.4.2 - github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a - github.com/stretchr/testify v1.6.1 - github.com/urfave/cli/v2 v2.3.0 + github.com/gorilla/websocket v1.5.3 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/term v0.37.0 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/net v0.17.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index abef50e..2e6ee48 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,24 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/bxcodec/faker/v3 v3.5.0 h1:Rahy6dwbd6up0wbwbV7dFyQb+jmdC51kpATuUdnzfMg= -github.com/bxcodec/faker/v3 v3.5.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/cliflags/duration.go b/internal/app/cliflags/duration.go new file mode 100644 index 0000000..cd68e37 --- /dev/null +++ b/internal/app/cliflags/duration.go @@ -0,0 +1,27 @@ +package cliflags + +import ( + "github.com/urfave/cli/v2" +) + +// Duration is a wrapper around cli.DurationFlag to implement cli.Flag interface. +// It differs from cli.DurationFlag in that it does not return a default text if the value is zero. +type Duration struct { + cli.DurationFlag +} + +var ( + _ cli.Flag = (*Duration)(nil) + _ cli.DocGenerationFlag = (*Duration)(nil) +) + +func (d *Duration) String() string { + return cli.FlagStringer(d) +} + +func (d *Duration) GetDefaultText() string { + if d.Value == 0 { + return "" + } + return d.DurationFlag.GetDefaultText() +} diff --git a/internal/app/configfile/config.go b/internal/app/configfile/config.go new file mode 100644 index 0000000..e479e8f --- /dev/null +++ b/internal/app/configfile/config.go @@ -0,0 +1,94 @@ +package configfile + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" +) + +type Config struct { + DefaultConn string `json:"default_connection,omitempty"` + Connections map[string]Connection `json:"connections,omitempty"` +} + +type Connection struct { + Gateway bool `json:"gateway,omitempty"` + URL string `json:"url"` + SiteID string `json:"site_id,omitempty"` + Token Token `json:"token"` + AllowInsecure bool `json:"allow_insecure,omitempty"` +} + +type Token struct { + Value string `json:"value"` +} + +const ( + dirName = ".enapter3" + fileName = "config.json" +) + +func Load() (Config, error) { + dir, err := configDir() + if err != nil { + return Config{}, err + } + + path := filepath.Join(dir, fileName) + f, err := os.Open(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return Config{}, nil + } + return Config{}, fmt.Errorf("open config file: %w", err) + } + defer f.Close() + + var config Config + if err := json.NewDecoder(f).Decode(&config); err != nil { + return Config{}, fmt.Errorf("decode config file: %w", err) + } + + return config, nil +} + +func Save(c Config) error { + dir, err := configDir() + if err != nil { + return err + } + + const perm = 0o755 + if err := os.MkdirAll(dir, perm); err != nil { + return fmt.Errorf("create config dir: %w", err) + } + + path := filepath.Join(dir, fileName) + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("create config file: %w", err) + } + defer f.Close() + + encoder := json.NewEncoder(f) + encoder.SetIndent("", " ") + if err := encoder.Encode(c); err != nil { + return fmt.Errorf("encode config file: %w", err) + } + + return f.Sync() +} + +func configDir() (string, error) { + if p := os.Getenv("ENAPTER3_CONFIG"); p != "" { + return p, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("get home dir: %w", err) + } + return filepath.Join(home, dirName), nil +} diff --git a/internal/app/enaptercli/app_test.go b/internal/app/enaptercli/app_test.go index 2b066a6..271ad05 100644 --- a/internal/app/enaptercli/app_test.go +++ b/internal/app/enaptercli/app_test.go @@ -19,19 +19,17 @@ var errExitTimeout = errors.New("exit timed out") type testApp struct { app *cli.App outBuf *lineBuffer - errBuf *bytes.Buffer errCh chan error cancel func() } func startTestApp(args ...string) *testApp { outBuf := newLineBuffer() - errBuf := &bytes.Buffer{} app := enaptercli.NewApp() app.HideVersion = true app.Writer = outBuf - app.ErrWriter = errBuf + app.ErrWriter = outBuf app.ExitErrHandler = func(*cli.Context, error) {} errCh := make(chan error, 1) @@ -43,7 +41,6 @@ func startTestApp(args ...string) *testApp { return &testApp{ app: app, outBuf: outBuf, - errBuf: errBuf, errCh: errCh, cancel: cancel, } @@ -63,7 +60,7 @@ func (a *testApp) Wait() error { } } -func (a *testApp) Stdout() *lineBuffer { +func (a *testApp) Output() *lineBuffer { return a.outBuf } diff --git a/internal/app/enaptercli/cmd_base.go b/internal/app/enaptercli/cmd_base.go index e941a90..a111cee 100644 --- a/internal/app/enaptercli/cmd_base.go +++ b/internal/app/enaptercli/cmd_base.go @@ -1,66 +1,440 @@ package enaptercli import ( + "bytes" + "cmp" + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" "io" + "net/http" + "net/url" + "slices" + "strings" + "time" + "github.com/gorilla/websocket" "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/configfile" ) +const defaultURL = "https://api.enapter.com" + type cmdBase struct { - token string - apiHost string - graphqlURL string - websocketsURL string - writer io.Writer + connName string + token string + apiURL string + siteID string + apiAllowInsecure bool + verbose bool + userAgent string + writer io.Writer + errWriter io.Writer + httpClient *http.Client } func (c *cmdBase) Flags() []cli.Flag { return []cli.Flag{ + &cli.StringFlag{ + Name: "connection", + Usage: "name of the connection to use", + Aliases: []string{"c"}, + Destination: &c.connName, + }, &cli.StringFlag{ Name: "token", Usage: "Enapter API token", - EnvVars: []string{"ENAPTER_API_TOKEN"}, + EnvVars: []string{"ENAPTER3_API_TOKEN"}, Hidden: true, Destination: &c.token, }, &cli.StringFlag{ - Name: "api-host", - Usage: "Override API endpoint", - EnvVars: []string{"ENAPTER_API_HOST"}, + Name: "api-url", + Usage: "override API base URL", + EnvVars: []string{"ENAPTER3_API_URL"}, + Value: defaultURL, Hidden: true, - Value: "https://api.enapter.com", - Destination: &c.apiHost, + Destination: &c.apiURL, + Action: func(_ *cli.Context, v string) error { + c.apiURL = strings.TrimSuffix(v, "/") + return nil + }, }, - &cli.StringFlag{ - Name: "gql-api-url", - Usage: "Override Cloud API endpoint", - EnvVars: []string{"ENAPTER_GQL_API_URL"}, - Hidden: true, - Value: "https://cli.enapter.com/graphql", - Destination: &c.graphqlURL, + &cli.BoolFlag{ + Name: "api-allow-insecure", + Usage: "allow insecure connections to the Enapter API", + EnvVars: []string{"ENAPTER3_API_ALLOW_INSECURE"}, + Destination: &c.apiAllowInsecure, }, - &cli.StringFlag{ - Name: "ws-api-url", - Usage: "Override Cloud API endpoint", - EnvVars: []string{"ENAPTER_WS_API_URL"}, - Hidden: true, - Value: "wss://cli.enapter.com/cable", - Destination: &c.websocketsURL, + &cli.BoolFlag{ + Name: "verbose", + Usage: "log extra details about the operation", + Destination: &c.verbose, }, } } func (c *cmdBase) Before(cliCtx *cli.Context) error { - if cliCtx.String("token") == "" { - return errAPITokenMissed + if err := c.setupCredentials(cliCtx); err != nil { + return err } + + c.userAgent = "enapter-cli/" + cliCtx.App.Version c.writer = cliCtx.App.Writer + c.errWriter = cliCtx.App.ErrWriter + c.httpClient = &http.Client{ + Transport: &http.Transport{ + //nolint:gosec // This is needed to allow self-signed certificates on Gateway. + TLSClientConfig: &tls.Config{InsecureSkipVerify: c.apiAllowInsecure}, + }, + } + return nil } -func (c *cmdBase) HelpTemplate() string { - return cli.CommandHelpTemplate + `ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token +func (c *cmdBase) setupCredentials(cliCtx *cli.Context) error { + config, err := configfile.Load() + if err != nil { + return err + } + + if c.connName != "" { + conn, ok := config.Connections[c.connName] + if !ok { + return cli.Exit("Unknown connection name.", 1) + } + if cliCtx.IsSet("token") || cliCtx.IsSet("api-url") || cliCtx.IsSet("api-allow-insecure") { + fmt.Fprintln(cliCtx.App.ErrWriter, + "WARNING: credentials set via environment variables or flags are ignored.") + } + c.token = conn.Token.Value + c.apiURL = conn.URL + c.siteID = conn.SiteID + c.apiAllowInsecure = conn.AllowInsecure + return nil + } + + if c.token != "" { + return nil + } + + if config.DefaultConn != "" { + conn, ok := config.Connections[config.DefaultConn] + if !ok { + return cli.Exit("Default connection is invalid.", 1) + } + c.token = conn.Token.Value + c.apiURL = conn.URL + c.siteID = conn.SiteID + c.apiAllowInsecure = conn.AllowInsecure + return nil + } + + return cli.Exit("No connection configured.\n\n"+ + "Please, specify connection using --connection flag.\n\n"+ + "To list available connections:\n$ enapter3 connection list\n\n"+ + "To add a new connection:\n$ enapter3 connection add\n", 1) +} + +const enapterAPIEnvVarsHelp = ` +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) ` + +func (c *cmdBase) CommandHelpTemplate() string { + return cli.CommandHelpTemplate + enapterAPIEnvVarsHelp +} + +func (c *cmdBase) SubcommandHelpTemplate() string { + return cli.SubcommandHelpTemplate + enapterAPIEnvVarsHelp +} + +func (c *cmdBase) chooseSiteID(cmdSiteID string) (string, error) { + if cmdSiteID != "" && c.siteID != "" && c.siteID != cmdSiteID { + return "", errSiteIDMismatch + } + siteID := cmp.Or(cmdSiteID, c.siteID) + if siteID == "" { + return "", errSiteIDMissing + } + return siteID, nil +} + +type doHTTPRequestParams struct { + Method string + Path string + Query url.Values + Body io.Reader + ContentType string + RespProcessor func(*http.Response) error +} + +func (c *cmdBase) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + req, err := http.NewRequestWithContext(ctx, p.Method, c.apiURL+"/v3"+p.Path, p.Body) + if err != nil { + return fmt.Errorf("build http request: %w", err) + } + + req.Header.Set("X-Enapter-Auth-Token", c.token) + req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("Content-Type", p.ContentType) + req.URL.RawQuery = p.Query.Encode() + + if c.verbose { + bodyStr, err := getRequestBodyString(req, p.ContentType) + if err != nil { + return err + } + + fmt.Fprintf(c.errWriter, "== Do http request %s %s\n", p.Method, req.URL.String()) + fmt.Fprintf(c.errWriter, "=== Begin body\n%s\n=== End body\n", bodyStr) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + if e := (&tls.CertificateVerificationError{}); errors.As(err, &e) { + return fmt.Errorf("do http request: %w (try to use --api-allow-insecure)", err) + } + return fmt.Errorf("do http request: %w", err) + } + defer resp.Body.Close() + + if p.RespProcessor == nil { + return c.defaultRespProcessor(resp) + } + return p.RespProcessor(resp) +} + +type runWebSocketParams struct { + Path string + Query url.Values + RespProcessor func(io.Reader) error +} + +func (c *cmdBase) runWebSocket(ctx context.Context, p runWebSocketParams) error { + url, err := url.Parse(c.apiURL + "/v3" + p.Path) + if err != nil { + return fmt.Errorf("parse url: %w", err) + } + url.RawQuery = p.Query.Encode() + + headers := make(http.Header) + headers.Set("X-Enapter-Auth-Token", c.token) + headers.Set("User-Agent", c.userAgent) + + for retry := false; ; retry = true { + if retry { + fmt.Fprintln(c.errWriter, "Reconnecting...") + time.Sleep(time.Second) + } + + conn, err := c.dialWebSocket(ctx, url, headers) + if err != nil { + if e := cli.ExitCoder(nil); errors.As(err, &e) { + return err + } + select { + case <-ctx.Done(): + return nil + default: + fmt.Fprintln(c.errWriter, "Failed to retrieve data:", err) + continue + } + } + fmt.Fprintln(c.errWriter, "Connection established") + + closeCh := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + case <-closeCh: + } + conn.Close() + }() + + if err := c.readWebSocket(conn, p.RespProcessor); err != nil { + select { + case <-ctx.Done(): + return nil + default: + fmt.Fprintln(c.errWriter, "Failed to retrieve data:", err) + close(closeCh) + } + } + } +} + +func (c *cmdBase) defaultRespProcessor(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(resp), 1) + } + + n, _ := io.Copy(c.writer, resp.Body) + if n == 0 { + _, _ = io.WriteString(c.writer, "Request finished without body\n") + } + + return nil +} + +func (c *cmdBase) dialWebSocket( + ctx context.Context, url *url.URL, headers http.Header, +) (*websocket.Conn, error) { + const timeout = 5 * time.Second + dialer := websocket.Dialer{ + HandshakeTimeout: timeout, + //nolint:gosec // This is needed to allow self-signed certificates on Gateway. + TLSClientConfig: &tls.Config{InsecureSkipVerify: c.apiAllowInsecure}, + } + + const maxRetries = 2 + for i := 0; i < maxRetries; i++ { + url.Scheme = websocketScheme(url.Scheme) + + if c.verbose { + fmt.Fprintf(c.errWriter, "== Dialing WebSocket at %s\n", url.String()) + } + + //nolint:bodyclose // body should be closed by callers + conn, resp, err := dialer.DialContext(ctx, url.String(), headers) + if err != nil { + if loc, err := redirectLocation(resp); err != nil { + return nil, err + } else if loc != nil { + url = loc + continue + } + if e := (&tls.CertificateVerificationError{}); errors.As(err, &e) { + message := fmt.Sprintf("dial: %v (try to use --api-allow-insecure)", err) + return nil, cli.Exit(message, 1) + } + if resp != nil { + message := parseRespErrorMessage(resp) + return nil, fmt.Errorf("dial: %w: %s", err, message) + } + return nil, fmt.Errorf("dial: %w", err) + } + + return conn, nil + } + + return nil, cli.Exit("Too many redirects", 1) +} + +func (c *cmdBase) readWebSocket( + conn *websocket.Conn, processor func(io.Reader) error, +) error { + for { + _, r, err := conn.NextReader() + if err != nil { + return fmt.Errorf("read: %w", err) + } + if err := processor(r); err != nil { + return err + } + } +} + +func getRequestBodyString(req *http.Request, contentType string) (string, error) { + if req.Body == nil { + return "", nil + } + bb := &bytes.Buffer{} + if _, err := io.Copy(bb, req.Body); err != nil { + return "", fmt.Errorf("reading body for verbose log: %w", err) + } + if err := req.Body.Close(); err != nil { + return "", fmt.Errorf("closing body for verbose log: %w", err) + } + req.Body = io.NopCloser(bb) + + if contentType != contentTypeJSON { + return base64.RawStdEncoding.EncodeToString(bb.Bytes()), nil + } + + return bb.String(), nil +} + +func okRespBodyProcessor(fn func(body io.Reader) error) func(resp *http.Response) error { + return func(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(resp), 1) + } + return fn(resp.Body) + } +} + +func parseRespErrorMessage(resp *http.Response) string { + var errs struct { + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + bodyBytes, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(bodyBytes, &errs); err != nil { + if !errors.Is(err, io.EOF) { + return fmt.Sprintf("Request finished with HTTP status %q, but body is not valid JSON error response. "+ + "Please, check API URL is correct.\n\nReceived body:\n%s\n", resp.Status, bodyBytes) + } + } + + if len(errs.Errors) > 0 { + msg := errs.Errors[0].Message + if len(msg) > 0 { + return msg + } + } + + return fmt.Sprintf("Request finished with HTTP status %q, but without error message", resp.Status) +} + +func validateExpandFlag(cliCtx *cli.Context, supportedFields []string) error { + for _, field := range cliCtx.StringSlice("expand") { + if err := validateFlag("expand", field, supportedFields); err != nil { + return err + } + } + return nil +} + +func validateFlag(context, value string, allowedValues []string) error { + slices.Sort(allowedValues) + if _, ok := slices.BinarySearch(allowedValues, value); !ok { + return fmt.Errorf("%w: %s is not supported for %s, should be one of %s", + errUnsupportedFlagValue, value, context, allowedValues) + } + return nil +} + +func websocketScheme(s string) string { + switch s { + case "https": + return "wss" + case "http": + return "ws" + default: + return s + } +} + +func redirectLocation(resp *http.Response) (*url.URL, error) { + if resp == nil { + return nil, nil + } + if resp.StatusCode != http.StatusPermanentRedirect { + return nil, nil + } + location := resp.Header.Get("Location") + url, err := url.Parse(location) + if err != nil { + return nil, fmt.Errorf("parse location: %w", err) + } + return url, nil } diff --git a/internal/app/enaptercli/cmd_base_pagination.go b/internal/app/enaptercli/cmd_base_pagination.go new file mode 100644 index 0000000..f8318f7 --- /dev/null +++ b/internal/app/enaptercli/cmd_base_pagination.go @@ -0,0 +1,123 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/urfave/cli/v2" +) + +var errEndPagination = errors.New("end pagination") + +type paginateHTTPRequestParams struct { + BaseParams doHTTPRequestParams + DoFn func(ctx context.Context, p doHTTPRequestParams) error + Limit int + ObjectName string +} + +func (c *cmdBase) doPaginateRequest(ctx context.Context, p paginateHTTPRequestParams) error { + const maxPageLimit = 50 + if p.BaseParams.Query == nil { + p.BaseParams.Query = url.Values{} + } + if p.Limit > 0 && p.Limit < maxPageLimit { + p.BaseParams.Query.Set("limit", strconv.Itoa(p.Limit)) + return p.DoFn(ctx, p.BaseParams) + } + + paginateRespProcesor := &paginateRespProcesor{ + ObjectName: p.ObjectName, + seenObjects: make(map[string]struct{}), + } + for { + reqPageParams := p.BaseParams + reqPageParams.Query.Set("offset", strconv.Itoa(len(paginateRespProcesor.Objects))) + reqPageParams.Query.Set("limit", strconv.Itoa(maxPageLimit)) + reqPageParams.RespProcessor = paginateRespProcesor.Process + + err := p.DoFn(ctx, reqPageParams) + if err != nil { + if errors.Is(err, errEndPagination) { + break + } + return fmt.Errorf("failed to retrieve page: %w", err) + } + if p.Limit > 0 && len(paginateRespProcesor.Objects) >= p.Limit { + break + } + } + + returnCount := len(paginateRespProcesor.Objects) + if p.Limit > 0 && returnCount > p.Limit { + returnCount = p.Limit + } + respBytes, err := json.Marshal(map[string]any{ + "total_count": paginateRespProcesor.TotalCount, + p.ObjectName: paginateRespProcesor.Objects[:returnCount], + }) + if err != nil { + return cli.Exit("Failed to marshal response: "+err.Error(), 1) + } + resp := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(respBytes)), + } + return c.defaultRespProcessor(resp) +} + +type paginateRespProcesor struct { + TotalCount int + Objects []any + ObjectName string + seenObjects map[string]struct{} +} + +func (p *paginateRespProcesor) Process(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return cli.Exit("Unexpected response status: "+resp.Status, 1) + } + + var pageBody map[string]json.RawMessage + if err := json.NewDecoder(resp.Body).Decode(&pageBody); err != nil { + return cli.Exit("Failed to parse response: "+err.Error(), 1) + } + + if err := json.Unmarshal(pageBody["total_count"], &p.TotalCount); err != nil { + return cli.Exit("Failed to parse total_count: "+err.Error(), 1) + } + + var objects []json.RawMessage + if err := json.Unmarshal(pageBody[p.ObjectName], &objects); err != nil { + return cli.Exit("Failed to parse "+p.ObjectName+": "+err.Error(), 1) + } + + if len(objects) == 0 { + return errEndPagination + } + + for _, obj := range objects { + var objMap map[string]any + if err := json.Unmarshal(obj, &objMap); err != nil { + return cli.Exit("Failed to parse object: "+err.Error(), 1) + } + + id, ok := objMap["id"].(string) + if !ok || id == "" { + return cli.Exit("Object ID is missing or not a string", 1) + } + + if _, seen := p.seenObjects[id]; !seen { + p.seenObjects[id] = struct{}{} + p.Objects = append(p.Objects, objMap) + } + } + return nil +} diff --git a/internal/app/enaptercli/cmd_blueprint.go b/internal/app/enaptercli/cmd_blueprint.go new file mode 100644 index 0000000..62f563a --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint.go @@ -0,0 +1,59 @@ +package enaptercli + +import ( + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprint struct { + cmdBase +} + +func buildCmdBlueprint() *cli.Command { + cmd := &cmdBlueprint{} + return &cli.Command{ + Name: "blueprint", + Usage: "Manage blueprints", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdBlueprintProfiles(), + buildCmdBlueprintUpload(), + buildCmdBlueprintDownload(), + buildCmdBlueprintGet(), + }, + } +} + +func isBlueprintID(s string) bool { + const blueprintIDLen = 36 + if len(s) != blueprintIDLen { + return false + } + + isDashPos := func(i int) bool { return i == 8 || i == 13 || i == 18 || i == 23 } + for i := 0; i < blueprintIDLen; i++ { + if isDashPos(i) { + if s[i] != '-' { + return false + } + } else { + isHexDigit := (s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f') + if !isHexDigit { + return false + } + } + } + return true +} + +func parseBlueprintName(n string) (name, tag string) { + const blueprintNameParts = 2 + nameTag := strings.SplitN(n, ":", blueprintNameParts) + name = nameTag[0] + tag = "latest" + if len(nameTag) > 1 { + tag = nameTag[1] + } + return name, tag +} diff --git a/internal/app/enaptercli/cmd_blueprint_download.go b/internal/app/enaptercli/cmd_blueprint_download.go new file mode 100644 index 0000000..346d8a1 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_download.go @@ -0,0 +1,97 @@ +package enaptercli + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprintDownload struct { + cmdBlueprint + blueprintID string + outputFileName string +} + +func buildCmdBlueprintDownload() *cli.Command { + cmd := &cmdBlueprintDownload{} + return &cli.Command{ + Name: "download", + Usage: "Download the blueprint zip from the Platform", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdBlueprintDownload) Flags() []cli.Flag { + flags := c.cmdBlueprint.Flags() + return append(flags, &cli.StringFlag{ + Name: "blueprint-id", + Aliases: []string{"b"}, + Usage: "blueprint name or ID to download", + Destination: &c.blueprintID, + Required: true, + }, &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "blueprint file name to save the blueprint", + Destination: &c.outputFileName, + }) +} + +func (c *cmdBlueprintDownload) do(ctx context.Context) error { + if c.outputFileName == "" { + c.outputFileName = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(c.blueprintID, + ":", "_"), ".", "_"), "/", "_") + ".enbp" + } + + if !isBlueprintID(c.blueprintID) { + blueprintName, blueprintTag := parseBlueprintName(c.blueprintID) + err := c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/blueprints/enapter/" + blueprintName + "/" + blueprintTag, + //nolint:bodyclose //body is closed in doHTTPRequest + RespProcessor: okRespBodyProcessor(func(body io.Reader) error { + var resp struct { + Blueprint struct { + ID string `json:"id"` + } `json:"blueprint"` + } + if err := json.NewDecoder(body).Decode(&resp); err != nil { + return fmt.Errorf("parse response body: %w", err) + } + c.blueprintID = resp.Blueprint.ID + return nil + }), + }) + if err != nil { + return fmt.Errorf("get blueprint info by name: %w", err) + } + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/blueprints/" + c.blueprintID + "/zip", + //nolint:bodyclose //body is closed in doHTTPRequest + RespProcessor: okRespBodyProcessor(func(body io.Reader) error { + outFile, err := os.Create(c.outputFileName) + if err != nil { + return fmt.Errorf("create output file %q: %w", c.outputFileName, err) + } + if _, err := io.Copy(outFile, body); err != nil { + return fmt.Errorf("write output file %q: %w", c.outputFileName, err) + } + fmt.Fprintln(c.writer, c.outputFileName) + return nil + }), + }) +} diff --git a/internal/app/enaptercli/cmd_blueprint_get.go b/internal/app/enaptercli/cmd_blueprint_get.go new file mode 100644 index 0000000..09153c0 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_get.go @@ -0,0 +1,53 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprintGet struct { + cmdBlueprint + blueprintID string +} + +func buildCmdBlueprintGet() *cli.Command { + cmd := &cmdBlueprintGet{} + return &cli.Command{ + Name: "get", + Usage: "Retrieve blueprint metadata", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.get(cliCtx.Context) + }, + } +} + +func (c *cmdBlueprintGet) Flags() []cli.Flag { + flags := c.cmdBlueprint.Flags() + return append(flags, &cli.StringFlag{ + Name: "blueprint-id", + Aliases: []string{"b"}, + Usage: "blueprint name or ID to retrieve", + Destination: &c.blueprintID, + Required: true, + }) +} + +func (c *cmdBlueprintGet) get(ctx context.Context) error { + if isBlueprintID(c.blueprintID) { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/blueprints/" + c.blueprintID, + }) + } + + blueprintName, blueprintTag := parseBlueprintName(c.blueprintID) + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/blueprints/enapter/" + blueprintName + "/" + blueprintTag, + }) +} diff --git a/internal/app/enaptercli/cmd_blueprint_profiles.go b/internal/app/enaptercli/cmd_blueprint_profiles.go new file mode 100644 index 0000000..8976de7 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_profiles.go @@ -0,0 +1,22 @@ +package enaptercli + +import ( + "github.com/urfave/cli/v2" +) + +type cmdBlueprintProfiles struct { + cmdBase +} + +func buildCmdBlueprintProfiles() *cli.Command { + cmd := &cmdBlueprintProfiles{} + return &cli.Command{ + Name: "profiles", + Usage: "Manage blueprint profiles", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdBlueprintProfilesDownload(), + buildCmdBlueprintProfilesUpload(), + }, + } +} diff --git a/internal/app/enaptercli/cmd_blueprint_profiles_download.go b/internal/app/enaptercli/cmd_blueprint_profiles_download.go new file mode 100644 index 0000000..130f273 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_profiles_download.go @@ -0,0 +1,63 @@ +package enaptercli + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprintProfilesDownload struct { + cmdBlueprintProfiles + outputFileName string +} + +func buildCmdBlueprintProfilesDownload() *cli.Command { + cmd := &cmdBlueprintProfilesDownload{} + return &cli.Command{ + Name: "download", + Usage: "Download profiles zip from the Platform", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdBlueprintProfilesDownload) Flags() []cli.Flag { + flags := c.cmdBlueprintProfiles.Flags() + return append(flags, &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "file name to save the downloaded profiles", + Destination: &c.outputFileName, + }) +} + +func (c *cmdBlueprintProfilesDownload) do(ctx context.Context) error { + if c.outputFileName == "" { + c.outputFileName = "profiles.zip" + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/blueprints/download_device_profiles", + //nolint:bodyclose //body is closed in doHTTPRequest + RespProcessor: okRespBodyProcessor(func(body io.Reader) error { + outFile, err := os.Create(c.outputFileName) + if err != nil { + return fmt.Errorf("create output file %q: %w", c.outputFileName, err) + } + if _, err := io.Copy(outFile, body); err != nil { + return fmt.Errorf("write output file %q: %w", c.outputFileName, err) + } + fmt.Fprintln(c.writer, c.outputFileName) + return nil + }), + }) +} diff --git a/internal/app/enaptercli/cmd_blueprint_profiles_upload.go b/internal/app/enaptercli/cmd_blueprint_profiles_upload.go new file mode 100644 index 0000000..bafbe19 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_profiles_upload.go @@ -0,0 +1,54 @@ +package enaptercli + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprintProfilesUpload struct { + cmdBlueprintProfiles + profilesPath string +} + +func buildCmdBlueprintProfilesUpload() *cli.Command { + cmd := &cmdBlueprintProfilesUpload{} + return &cli.Command{ + Name: "upload", + Usage: "Upload profiles to the Platform", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.upload(cliCtx.Context) + }, + } +} + +func (c *cmdBlueprintProfilesUpload) Flags() []cli.Flag { + flags := c.cmdBlueprintProfiles.Flags() + return append(flags, &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Usage: "profiles zip file path", + Destination: &c.profilesPath, + Required: true, + }) +} + +func (c *cmdBlueprintProfilesUpload) upload(ctx context.Context) error { + data, err := os.ReadFile(c.profilesPath) + if err != nil { + return fmt.Errorf("read zip file: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/blueprints/upload_device_profiles", + Body: bytes.NewReader(data), + }) +} diff --git a/internal/app/enaptercli/cmd_blueprint_upload.go b/internal/app/enaptercli/cmd_blueprint_upload.go new file mode 100644 index 0000000..ae02529 --- /dev/null +++ b/internal/app/enaptercli/cmd_blueprint_upload.go @@ -0,0 +1,101 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + + "github.com/urfave/cli/v2" +) + +type cmdBlueprintUpload struct { + cmdBlueprint + blueprintPath string +} + +func buildCmdBlueprintUpload() *cli.Command { + cmd := &cmdBlueprintUpload{} + return &cli.Command{ + Name: "upload", + Usage: "Upload the blueprint to the Platform", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.upload(cliCtx.Context) + }, + } +} + +func (c *cmdBlueprintUpload) Flags() []cli.Flag { + flags := c.cmdBlueprint.Flags() + return append(flags, &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Usage: "blueprint path (zip file or directory)", + Destination: &c.blueprintPath, + Required: true, + }) +} + +func (c *cmdBlueprintUpload) upload(ctx context.Context) error { + return uploadBlueprint(ctx, c.blueprintPath, c.doHTTPRequest) +} + +func uploadBlueprintAndReturnBlueprintID(ctx context.Context, blueprintPath string, + doHTTPRequest func(context.Context, doHTTPRequestParams) error, +) (string, error) { + var blueprintID string + err := uploadBlueprint(ctx, blueprintPath, func(ctx context.Context, reqParams doHTTPRequestParams) error { + reqParams.RespProcessor = func(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(resp), 1) + } + + var respBlueprint struct { + Blueprint struct { + ID string `json:"id"` + } `json:"blueprint"` + } + if err := json.NewDecoder(resp.Body).Decode(&respBlueprint); err != nil { + return fmt.Errorf("decode blueprint response: %w", err) + } + blueprintID = respBlueprint.Blueprint.ID + return nil + } + return doHTTPRequest(ctx, reqParams) + }) + return blueprintID, err +} + +func uploadBlueprint( + ctx context.Context, blueprintPath string, + doHTTPRequest func(context.Context, doHTTPRequestParams) error, +) error { + fi, err := os.Stat(blueprintPath) + if err != nil { + return fmt.Errorf("check blueprint path: %w", err) + } + + var data []byte + if fi.IsDir() { + data, err = zipDir(blueprintPath) + if err != nil { + return fmt.Errorf("zip blueprint directory: %w", err) + } + } else { + data, err = os.ReadFile(blueprintPath) + if err != nil { + return fmt.Errorf("read blueprint zip file: %w", err) + } + } + + return doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/blueprints/upload", + Body: bytes.NewReader(data), + }) +} diff --git a/internal/app/enaptercli/cmd_connection.go b/internal/app/enaptercli/cmd_connection.go new file mode 100644 index 0000000..e8d91d8 --- /dev/null +++ b/internal/app/enaptercli/cmd_connection.go @@ -0,0 +1,18 @@ +package enaptercli + +import ( + "github.com/urfave/cli/v2" +) + +func buildCmdConnection() *cli.Command { + return &cli.Command{ + Name: "connection", + Usage: "Manage connections to Enapter Cloud and Gateways", + Subcommands: []*cli.Command{ + buildCmdConnectionAdd(), + buildCmdConnectionRemove(), + buildCmdConnectionList(), + buildCmdConnectionSetDefault(), + }, + } +} diff --git a/internal/app/enaptercli/cmd_connection_add.go b/internal/app/enaptercli/cmd_connection_add.go new file mode 100644 index 0000000..f962682 --- /dev/null +++ b/internal/app/enaptercli/cmd_connection_add.go @@ -0,0 +1,150 @@ +package enaptercli + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/configfile" +) + +type cmdConnectionAdd struct { + name string + url string + token string + siteID string + gateway bool + allowInsecure bool +} + +func buildCmdConnectionAdd() *cli.Command { + cmd := &cmdConnectionAdd{} + return &cli.Command{ + Name: "add", + Usage: "Add a new connection", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "connection name", + Destination: &cmd.name, + Required: true, + }, + &cli.BoolFlag{ + Name: "gateway", + Usage: "indicates that the connection is to a Gateway", + Destination: &cmd.gateway, + }, + &cli.StringFlag{ + Name: "url", + Usage: "Enapter API base URL", + Destination: &cmd.url, + Value: defaultURL, + }, + &cli.StringFlag{ + Name: "token", + Usage: "Enapter API access token", + Destination: &cmd.token, + Required: true, + }, + &cli.StringFlag{ + Name: "site-id", + Usage: "if specified, the connection will be limited to this site " + + "(available only for Cloud connections)", + Destination: &cmd.siteID, + }, + &cli.BoolFlag{ + Name: "allow-insecure", + Usage: "allow insecure connections to the Enapter API", + Destination: &cmd.allowInsecure, + }, + }, + Action: cmd.do, + } +} + +func (c *cmdConnectionAdd) do(cliCtx *cli.Context) error { + config, err := configfile.Load() + if err != nil { + return err + } + + if _, exists := config.Connections[c.name]; exists { + return cli.Exit("Connection with the given name already exists.", 1) + } + + if u, err := url.Parse(c.url); err != nil { + return cli.Exit("Invalid URL format: "+err.Error()+".", 1) + } else if u.Scheme != "https" && u.Scheme != "http" { + return cli.Exit("URL scheme must be http or https.", 1) + } + + if c.gateway { + if c.url == defaultURL { + return cli.Exit("Gateway connections require a custom URL.", 1) + } + if c.siteID != "" { + return cli.Exit("The site-id option cannot be used with gateway connections.", 1) + } + siteID, err := c.resolveGatewaySiteID(cliCtx) + if err != nil { + return err + } + c.siteID = siteID + } + + if config.Connections == nil { + config.Connections = make(map[string]configfile.Connection) + } + config.Connections[c.name] = configfile.Connection{ + Gateway: c.gateway, + URL: c.url, + SiteID: c.siteID, + Token: configfile.Token{Value: c.token}, + AllowInsecure: c.allowInsecure, + } + + return configfile.Save(config) +} + +func (c *cmdConnectionAdd) resolveGatewaySiteID(cliCtx *cli.Context) (string, error) { + client := &http.Client{ + Transport: &http.Transport{ + //nolint:gosec // This is needed to allow self-signed certificates on Gateway. + TLSClientConfig: &tls.Config{InsecureSkipVerify: c.allowInsecure}, + }, + } + + req, err := http.NewRequestWithContext( + cliCtx.Context, http.MethodGet, c.url+"/v3/site", nil) + if err != nil { + return "", fmt.Errorf("new http request: %w", err) + } + + req.Header.Set("X-Enapter-Auth-Token", c.token) + req.Header.Set("User-Agent", "enapter-cli/"+cliCtx.App.Version) + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("send http request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", cli.Exit("Unexpected response from Gateway: "+resp.Status+". ", 1) + } + + var siteResp struct { + Site struct { + ID string `json:"id"` + } `json:"site"` + } + if err := json.NewDecoder(resp.Body).Decode(&siteResp); err != nil { + return "", fmt.Errorf("decode http response: %w", err) + } + + return siteResp.Site.ID, nil +} diff --git a/internal/app/enaptercli/cmd_connection_list.go b/internal/app/enaptercli/cmd_connection_list.go new file mode 100644 index 0000000..90a0cd7 --- /dev/null +++ b/internal/app/enaptercli/cmd_connection_list.go @@ -0,0 +1,60 @@ +package enaptercli + +import ( + "fmt" + "maps" + "slices" + "text/tabwriter" + + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/configfile" +) + +type cmdConnectionList struct{} + +func buildCmdConnectionList() *cli.Command { + cmd := &cmdConnectionList{} + return &cli.Command{ + Name: "list", + Usage: "List all connections", + Action: cmd.do, + } +} + +func (c *cmdConnectionList) do(cliCtx *cli.Context) error { + config, err := configfile.Load() + if err != nil { + return err + } + + const padding = 3 + w := tabwriter.NewWriter(cliCtx.App.Writer, 0, 0, padding, ' ', 0) + + fmt.Fprintln(w, "NAME\tTYPE\tURL\tALLOW INSECURE\tSITE ID") + + names := slices.Sorted(maps.Keys(config.Connections)) + for _, name := range names { + conn := config.Connections[name] + + displayName := name + if name == config.DefaultConn { + displayName += " *" + } + + typ := "cloud" + if conn.Gateway { + typ = "gateway" + } + + allowInsecure := "no" + if conn.AllowInsecure { + allowInsecure = "yes" + } + + fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\n", + displayName, typ, conn.URL, allowInsecure, conn.SiteID) + } + + return w.Flush() +} diff --git a/internal/app/enaptercli/cmd_connection_remove.go b/internal/app/enaptercli/cmd_connection_remove.go new file mode 100644 index 0000000..55bd849 --- /dev/null +++ b/internal/app/enaptercli/cmd_connection_remove.go @@ -0,0 +1,50 @@ +package enaptercli + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/configfile" +) + +type cmdConnectionRemove struct { + name string +} + +func buildCmdConnectionRemove() *cli.Command { + cmd := &cmdConnectionRemove{} + return &cli.Command{ + Name: "remove", + Usage: "Remove a connection", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "connection name", + Destination: &cmd.name, + Required: true, + }, + }, + Action: cmd.do, + } +} + +func (c *cmdConnectionRemove) do(cliCtx *cli.Context) error { + config, err := configfile.Load() + if err != nil { + return err + } + + if _, ok := config.Connections[c.name]; !ok { + fmt.Fprintln(cliCtx.App.ErrWriter, "WARNING: unknown connection.") + return nil + } + + delete(config.Connections, c.name) + if config.DefaultConn == c.name { + fmt.Fprintln(cliCtx.App.ErrWriter, "WARNING: removed connection was set as default.") + config.DefaultConn = "" + } + + return configfile.Save(config) +} diff --git a/internal/app/enaptercli/cmd_connection_set_default.go b/internal/app/enaptercli/cmd_connection_set_default.go new file mode 100644 index 0000000..403ed4f --- /dev/null +++ b/internal/app/enaptercli/cmd_connection_set_default.go @@ -0,0 +1,42 @@ +package enaptercli + +import ( + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/configfile" +) + +type cmdConnectionSetDefault struct { + name string +} + +func buildCmdConnectionSetDefault() *cli.Command { + cmd := &cmdConnectionSetDefault{} + return &cli.Command{ + Name: "set-default", + Usage: "Set default connection", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "connection name", + Destination: &cmd.name, + Required: true, + }, + }, + Action: cmd.do, + } +} + +func (c *cmdConnectionSetDefault) do(*cli.Context) error { + config, err := configfile.Load() + if err != nil { + return err + } + + if _, ok := config.Connections[c.name]; !ok { + return cli.Exit("Unknown connection.", 1) + } + + config.DefaultConn = c.name + return configfile.Save(config) +} diff --git a/internal/app/enaptercli/cmd_device.go b/internal/app/enaptercli/cmd_device.go new file mode 100644 index 0000000..85a4ae2 --- /dev/null +++ b/internal/app/enaptercli/cmd_device.go @@ -0,0 +1,94 @@ +package enaptercli + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +type cmdDevice struct { + cmdBase + siteID string +} + +func buildCmdDevice() *cli.Command { + cmd := &cmdDevice{} + return &cli.Command{ + Name: "device", + Usage: "Manage devices", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdDeviceCreate(), + buildCmdDeviceList(), + buildCmdDeviceGet(), + buildCmdDeviceChangeBlueprint(), + buildCmdDeviceLogs(), + buildCmdDeviceUpdate(), + buildCmdDeviceDelete(), + buildCmdDeviceCommand(), + buildCmdDeviceTelemetry(), + buildCmdDeviceCommunicationConfig(), + buildCmdDeviceRunTerminal(), + }, + } +} + +func (c *cmdDevice) Flags() []cli.Flag { + flags := c.cmdBase.Flags() + return append(flags, &cli.StringFlag{ + Name: "site-id", + Usage: "site ID", + Destination: &c.siteID, + }) +} + +func (c *cmdDevice) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + path, err := c.buildPath(p.Path) + if err != nil { + return err + } + p.Path = path + return c.cmdBase.doHTTPRequest(ctx, p) +} + +func (c *cmdDevice) runWebSocket(ctx context.Context, p runWebSocketParams) error { + path, err := c.buildPath(p.Path) + if err != nil { + return err + } + p.Path = path + return c.cmdBase.runWebSocket(ctx, p) +} + +func (c *cmdDevice) validateExpandFlag(cliCtx *cli.Context) error { + return validateExpandFlag(cliCtx, c.supportedExpandFields()) +} + +func (c *cmdDevice) supportedExpandFields() []string { + return []string{"connectivity", "manifest", "properties", "communication", "site"} +} + +func (c *cmdDevice) buildPath(p string) (string, error) { + path, err := url.JoinPath("/devices", p) + if err != nil { + return "", fmt.Errorf("join path: %w", err) + } + + siteID, err := c.chooseSiteID(c.siteID) + if err != nil { + if errors.Is(err, errSiteIDMissing) { + return path, nil + } + return "", err + } + + path, err = url.JoinPath("/sites", siteID, path) + if err != nil { + return "", fmt.Errorf("join path: %w", err) + } + + return path, nil +} diff --git a/internal/app/enaptercli/cmd_device_change_blueprint.go b/internal/app/enaptercli/cmd_device_change_blueprint.go new file mode 100644 index 0000000..9ad3271 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_change_blueprint.go @@ -0,0 +1,88 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceChangeBlueprint struct { + cmdDevice + deviceID string + blueprintID string + blueprintPath string +} + +func buildCmdDeviceChangeBlueprint() *cli.Command { + cmd := &cmdDeviceChangeBlueprint{} + return &cli.Command{ + Name: "change-blueprint", + Usage: "Change device blueprint", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceChangeBlueprint) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, &cli.StringFlag{ + Name: "blueprint-id", + Aliases: []string{"b"}, + Usage: "blueprint ID to use as new device blueprint", + Destination: &c.blueprintID, + }, &cli.StringFlag{ + Name: "blueprint-path", + Usage: "blueprint path (zip file or directory) to use as new device blueprint", + Destination: &c.blueprintPath, + }) +} + +func (c *cmdDeviceChangeBlueprint) Before(cliCtx *cli.Context) error { + if err := c.cmdDevice.Before(cliCtx); err != nil { + return err + } + if c.blueprintID != "" && c.blueprintPath != "" { + return errOnlyOneBlueprinFlag + } + if c.blueprintID == "" && c.blueprintPath == "" { + return errMissedBlueprintFlag + } + return c.validateExpandFlag(cliCtx) +} + +func (c *cmdDeviceChangeBlueprint) do(ctx context.Context) error { + if c.blueprintPath != "" { + blueprintID, err := uploadBlueprintAndReturnBlueprintID(ctx, c.blueprintPath, c.cmdBase.doHTTPRequest) + if err != nil { + return fmt.Errorf("upload blueprint: %w", err) + } + c.blueprintID = blueprintID + } + + body, err := json.Marshal(map[string]any{ + "blueprint_id": c.blueprintID, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/" + c.deviceID + "/assign_blueprint", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_device_command.go b/internal/app/enaptercli/cmd_device_command.go new file mode 100644 index 0000000..db90388 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_command.go @@ -0,0 +1,50 @@ +package enaptercli + +import ( + "context" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommand struct { + cmdDevice + deviceID string +} + +func buildCmdDeviceCommand() *cli.Command { + cmd := &cmdDeviceCommand{} + return &cli.Command{ + Name: "command", + Usage: "Manage device commands", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdDeviceCommandExecute(), + buildCmdDeviceCommandList(), + buildCmdDeviceCommandGet(), + }, + } +} + +func (c *cmdDeviceCommand) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, + &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, + ) +} + +func (c *cmdDeviceCommand) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + path, err := url.JoinPath(c.deviceID, "command_executions", p.Path) + if err != nil { + return fmt.Errorf("join path: %w", err) + } + p.Path = path + return c.cmdDevice.doHTTPRequest(ctx, p) +} diff --git a/internal/app/enaptercli/cmd_device_command_execute.go b/internal/app/enaptercli/cmd_device_command_execute.go new file mode 100644 index 0000000..a04600a --- /dev/null +++ b/internal/app/enaptercli/cmd_device_command_execute.go @@ -0,0 +1,85 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommandExecute struct { + cmdDevice + deviceID string + cmdName string + cmdArgs string + ephemeral bool +} + +func buildCmdDeviceCommandExecute() *cli.Command { + cmd := &cmdDeviceCommandExecute{} + return &cli.Command{ + Name: "execute", + Usage: "Execute a device command", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCommandExecute) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, + &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Usage: "command name", + Destination: &c.cmdName, + Required: true, + }, + &cli.StringFlag{ + Name: "arguments", + Usage: "command arguments (should be a JSON string)", + Destination: &c.cmdArgs, + }, + &cli.BoolFlag{ + Name: "ephemeral", + Usage: "run command in ephemeral mode", + Destination: &c.ephemeral, + Hidden: true, + }, + ) +} + +func (c *cmdDeviceCommandExecute) do(ctx context.Context) error { + reqBody := struct { + Name string `json:"name"` + Args json.RawMessage `json:"arguments,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + }{ + Name: c.cmdName, + Args: json.RawMessage(c.cmdArgs), + Ephemeral: c.ephemeral, + } + data, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/" + c.deviceID + "/execute_command", + Body: bytes.NewReader(data), + }) +} diff --git a/internal/app/enaptercli/cmd_device_command_get.go b/internal/app/enaptercli/cmd_device_command_get.go new file mode 100644 index 0000000..3ddfb25 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_command_get.go @@ -0,0 +1,67 @@ +package enaptercli + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommandGet struct { + cmdDeviceCommand + executionID string + expand []string +} + +func buildCmdDeviceCommandGet() *cli.Command { + cmd := &cmdDeviceCommandGet{} + return &cli.Command{ + Name: "get", + Usage: "Retrieve a device command execution", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCommandGet) Flags() []cli.Flag { + flags := c.cmdDeviceCommand.Flags() + return append(flags, + &cli.StringFlag{ + Name: "execution-id", + Usage: "execution ID", + Destination: &c.executionID, + Required: true, + }, &cli.MultiStringFlag{ + Target: &cli.StringSliceFlag{ + Name: "expand", + Usage: "coma-separated list of expanded options (supported values: log)", + }, + Destination: &c.expand, + }, + ) +} + +func (c *cmdDeviceCommandGet) Before(cliCtx *cli.Context) error { + if err := c.cmdDevice.Before(cliCtx); err != nil { + return err + } + return validateExpandFlag(cliCtx, []string{"log"}) +} + +func (c *cmdDeviceCommandGet) do(ctx context.Context) error { + query := url.Values{} + if len(c.expand) != 0 { + query.Set("expand", strings.Join(c.expand, ",")) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.executionID, + Query: query, + }) +} diff --git a/internal/app/enaptercli/cmd_device_command_list.go b/internal/app/enaptercli/cmd_device_command_list.go new file mode 100644 index 0000000..3cc6ee4 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_command_list.go @@ -0,0 +1,32 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommandList struct { + cmdDeviceCommand +} + +func buildCmdDeviceCommandList() *cli.Command { + cmd := &cmdDeviceCommandList{} + return &cli.Command{ + Name: "list", + Usage: "List device command executions", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCommandList) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + }) +} diff --git a/internal/app/enaptercli/cmd_device_communication_config.go b/internal/app/enaptercli/cmd_device_communication_config.go new file mode 100644 index 0000000..a060118 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_communication_config.go @@ -0,0 +1,48 @@ +package enaptercli + +import ( + "context" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommunicationConfig struct { + cmdDevice + deviceID string +} + +func buildCmdDeviceCommunicationConfig() *cli.Command { + cmd := &cmdDeviceCommunicationConfig{} + return &cli.Command{ + Name: "communication-config", + Usage: "Manage device communication config", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdDeviceCommunicationConfigGenerate(), + }, + } +} + +func (c *cmdDeviceCommunicationConfig) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, + &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, + ) +} + +func (c *cmdDeviceCommunicationConfig) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + path, err := url.JoinPath(c.deviceID, p.Path) + if err != nil { + return fmt.Errorf("join path: %w", err) + } + p.Path = path + return c.cmdDevice.doHTTPRequest(ctx, p) +} diff --git a/internal/app/enaptercli/cmd_device_communication_config_generate.go b/internal/app/enaptercli/cmd_device_communication_config_generate.go new file mode 100644 index 0000000..8853668 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_communication_config_generate.go @@ -0,0 +1,60 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCommunicationConfigGenerate struct { + cmdDeviceCommunicationConfig + protocol string +} + +func buildCmdDeviceCommunicationConfigGenerate() *cli.Command { + cmd := &cmdDeviceCommunicationConfigGenerate{} + return &cli.Command{ + Name: "generate", + Usage: "Generate a new communication config for device", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCommunicationConfigGenerate) Flags() []cli.Flag { + flags := c.cmdDeviceCommunicationConfig.Flags() + return append(flags, + &cli.StringFlag{ + Name: "protocol", + Usage: "connection protocol (supported values: MQTT, MQTTS)", + Destination: &c.protocol, + Required: true, + }, + ) +} + +func (c *cmdDeviceCommunicationConfigGenerate) do(ctx context.Context) error { + reqBody := struct { + Protocol string `json:"protocol"` + }{ + Protocol: c.protocol, + } + data, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/generate_config", + Body: bytes.NewReader(data), + }) +} diff --git a/internal/app/enaptercli/cmd_device_create.go b/internal/app/enaptercli/cmd_device_create.go new file mode 100644 index 0000000..1154c1a --- /dev/null +++ b/internal/app/enaptercli/cmd_device_create.go @@ -0,0 +1,22 @@ +package enaptercli + +import ( + "github.com/urfave/cli/v2" +) + +type cmdDeviceCreate struct { + cmdBase +} + +func buildCmdDeviceCreate() *cli.Command { + cmd := &cmdDeviceCreate{} + return &cli.Command{ + Name: "create", + Usage: "Create devices of different types", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdDeviceCreateStandalone(), + buildCmdDeviceCreateLua(), + }, + } +} diff --git a/internal/app/enaptercli/cmd_device_create_lua_device.go b/internal/app/enaptercli/cmd_device_create_lua_device.go new file mode 100644 index 0000000..8edbf66 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_create_lua_device.go @@ -0,0 +1,146 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCreateLua struct { + cmdDeviceCreate + siteID string + deviceName string + deviceSlug string + runtimeID string + blueprintID string + blueprintPath string +} + +func buildCmdDeviceCreateLua() *cli.Command { + cmd := &cmdDeviceCreateLua{} + return &cli.Command{ + Name: "lua-device", + Usage: "Create a new Lua device", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCreateLua) Flags() []cli.Flag { + flags := c.cmdDeviceCreate.Flags() + return append(flags, &cli.StringFlag{ + Name: "site-id", + Usage: "site ID", + Destination: &c.siteID, + }, &cli.StringFlag{ + Name: "runtime-id", + Aliases: []string{"r"}, + Usage: "UCM device ID where the new Lua device will run", + Destination: &c.runtimeID, + Required: true, + }, &cli.StringFlag{ + Name: "device-name", + Aliases: []string{"n"}, + Usage: "name for the new Lua device", + Destination: &c.deviceName, + Required: true, + }, &cli.StringFlag{ + Name: "device-slug", + Usage: "slug for the new Lua device", + Destination: &c.deviceSlug, + }, &cli.StringFlag{ + Name: "blueprint-id", + Aliases: []string{"b"}, + Usage: "blueprint ID to use for the new Lua device", + Destination: &c.blueprintID, + }, &cli.StringFlag{ + Name: "blueprint-path", + Usage: "blueprint path (zip file or directory) to use for the new Lua device", + Destination: &c.blueprintPath, + }) +} + +func (c *cmdDeviceCreateLua) Before(cliCtx *cli.Context) error { + if err := c.cmdDeviceCreate.Before(cliCtx); err != nil { + return err + } + if c.blueprintID != "" && c.blueprintPath != "" { + return errOnlyOneBlueprinFlag + } + if c.blueprintID == "" && c.blueprintPath == "" { + return errMissedBlueprintFlag + } + return nil +} + +func (c *cmdDeviceCreateLua) do(ctx context.Context) error { + if c.blueprintPath != "" { + blueprintID, err := uploadBlueprintAndReturnBlueprintID(ctx, c.blueprintPath, c.doHTTPRequest) + if err != nil { + return fmt.Errorf("upload blueprint: %w", err) + } + c.blueprintID = blueprintID + } + + // Cloud API does not allow slugs as runtime ID for now + runtimeID, err := c.resolveRuntimeID(ctx) + if err != nil { + return fmt.Errorf("resolve runtime ID: %w", err) + } + + body, err := json.Marshal(map[string]interface{}{ + "runtime_id": runtimeID, + "name": c.deviceName, + "slug": c.deviceSlug, + "blueprint_id": c.blueprintID, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/provisioning/lua_device", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} + +func (c *cmdDeviceCreateLua) resolveRuntimeID(ctx context.Context) (string, error) { + siteID, err := c.chooseSiteID(c.siteID) + if err != nil { + if errors.Is(err, errSiteIDMissing) { + return c.runtimeID, nil + } + return "", err + } + + var resp struct { + Device struct { + ID string `json:"id"` + } `json:"device"` + } + + if err := c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/sites/" + siteID + "/devices/" + c.runtimeID, + RespProcessor: func(r *http.Response) error { + if r.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(r), 1) + } + return json.NewDecoder(r.Body).Decode(&resp) + }, + }); err != nil { + return "", err + } + + return resp.Device.ID, nil +} diff --git a/internal/app/enaptercli/cmd_device_create_standalone.go b/internal/app/enaptercli/cmd_device_create_standalone.go new file mode 100644 index 0000000..39d7090 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_create_standalone.go @@ -0,0 +1,74 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceCreateStandalone struct { + cmdDeviceCreate + siteID string + deviceName string + deviceSlug string +} + +func buildCmdDeviceCreateStandalone() *cli.Command { + cmd := &cmdDeviceCreateStandalone{} + return &cli.Command{ + Name: "standalone", + Usage: "Create a new standalone device", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceCreateStandalone) Flags() []cli.Flag { + flags := c.cmdDeviceCreate.Flags() + return append(flags, &cli.StringFlag{ + Name: "site-id", + Aliases: []string{"s"}, + Usage: "site ID where the device will be created", + Destination: &c.siteID, + }, &cli.StringFlag{ + Name: "device-name", + Aliases: []string{"n"}, + Usage: "name for the new device", + Destination: &c.deviceName, + Required: true, + }, &cli.StringFlag{ + Name: "device-slug", + Usage: "slug for the new standalone device", + Destination: &c.deviceSlug, + }) +} + +func (c *cmdDeviceCreateStandalone) do(ctx context.Context) error { + siteID, err := c.chooseSiteID(c.siteID) + if err != nil { + return err + } + + body, err := json.Marshal(map[string]any{ + "site_id": siteID, + "name": c.deviceName, + "slug": c.deviceSlug, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/provisioning/standalone", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_device_delete.go b/internal/app/enaptercli/cmd_device_delete.go new file mode 100644 index 0000000..67d5ebf --- /dev/null +++ b/internal/app/enaptercli/cmd_device_delete.go @@ -0,0 +1,47 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceDelete struct { + cmdDevice + deviceID string +} + +func buildCmdDeviceDelete() *cli.Command { + cmd := &cmdDeviceDelete{} + return &cli.Command{ + Name: "delete", + Usage: "Delete a device", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceDelete) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, + &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, + ) +} + +func (c *cmdDeviceDelete) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodDelete, + Path: "/" + c.deviceID, + }) +} diff --git a/internal/app/enaptercli/cmd_device_get.go b/internal/app/enaptercli/cmd_device_get.go new file mode 100644 index 0000000..86a06d4 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_get.go @@ -0,0 +1,67 @@ +package enaptercli + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceGet struct { + cmdDevice + deviceID string + expand []string +} + +func buildCmdDeviceGet() *cli.Command { + cmd := &cmdDeviceGet{} + return &cli.Command{ + Name: "get", + Usage: "Retrieve device information", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceGet) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, &cli.MultiStringFlag{ + Target: &cli.StringSliceFlag{ + Name: "expand", + Usage: "coma-separated list of expanded device information (supported values: " + + strings.Join(c.supportedExpandFields(), ", ") + ")", + }, + Destination: &c.expand, + }) +} + +func (c *cmdDeviceGet) Before(cliCtx *cli.Context) error { + if err := c.cmdDevice.Before(cliCtx); err != nil { + return err + } + return c.validateExpandFlag(cliCtx) +} + +func (c *cmdDeviceGet) do(ctx context.Context) error { + query := url.Values{} + if len(c.expand) != 0 { + query.Set("expand", strings.Join(c.expand, ",")) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.deviceID, + Query: query, + }) +} diff --git a/internal/app/enaptercli/cmd_device_list.go b/internal/app/enaptercli/cmd_device_list.go new file mode 100644 index 0000000..808eb48 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_list.go @@ -0,0 +1,74 @@ +package enaptercli + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceList struct { + cmdDevice + expand []string + limit int +} + +func buildCmdDeviceList() *cli.Command { + cmd := &cmdDeviceList{} + return &cli.Command{ + Name: "list", + Usage: "List user devices ordered by device ID", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceList) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.MultiStringFlag{ + Target: &cli.StringSliceFlag{ + Name: "expand", + Usage: "coma-separated list of expanded device information (supported values: " + + strings.Join(c.supportedExpandFields(), ", ") + ")", + }, + Destination: &c.expand, + }, &cli.IntFlag{ + Name: "limit", + Usage: "maximum number of devices to retrieve", + Destination: &c.limit, + DefaultText: "retrieves all", + }) +} + +func (c *cmdDeviceList) Before(cliCtx *cli.Context) error { + if err := c.cmdDevice.Before(cliCtx); err != nil { + return err + } + return c.validateExpandFlag(cliCtx) +} + +func (c *cmdDeviceList) do(ctx context.Context) error { + query := url.Values{} + if len(c.expand) != 0 { + query.Set("expand", strings.Join(c.expand, ",")) + } + + doPaginateRequestParams := paginateHTTPRequestParams{ + ObjectName: "devices", + Limit: c.limit, + DoFn: c.doHTTPRequest, + BaseParams: doHTTPRequestParams{ + Method: http.MethodGet, + Path: "", + Query: query, + }, + } + + return c.doPaginateRequest(ctx, doPaginateRequestParams) +} diff --git a/internal/app/enaptercli/cmd_device_logs.go b/internal/app/enaptercli/cmd_device_logs.go new file mode 100644 index 0000000..b1307a9 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_logs.go @@ -0,0 +1,202 @@ +package enaptercli + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceLogs struct { + cmdDevice + deviceID string + follow bool + from cli.Timestamp + to cli.Timestamp + offset int + limit int + severity string + order string + showFilter string +} + +func buildCmdDeviceLogs() *cli.Command { + cmd := &cmdDeviceLogs{} + return &cli.Command{ + Name: "logs", + Usage: "Show device logs", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceLogs) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, &cli.BoolFlag{ + Name: "follow", + Aliases: []string{"f"}, + Usage: "follow the log output", + Destination: &c.follow, + }, &cli.TimestampFlag{ + Name: "from", + Usage: "from timestamp in RFC 3339 format (e.g. 2006-01-02T15:04:05Z)", + Destination: &c.from, + Layout: time.RFC3339, + }, &cli.TimestampFlag{ + Name: "to", + Usage: "to timestamp in RFC 3339 format (e.g. 2006-01-02T15:04:05Z)", + Destination: &c.to, + Layout: time.RFC3339, + }, &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "maximum number of logs to retrieve", + Destination: &c.limit, + }, &cli.IntFlag{ + Name: "offset", + Aliases: []string{"o"}, + Usage: "number of logs to skip when retrieving", + Destination: &c.offset, + }, &cli.StringFlag{ + Name: "severity", + Aliases: []string{"s"}, + Usage: "filter logs by severity", + Destination: &c.severity, + }, &cli.StringFlag{ + Name: "order", + Usage: "order logs by criteria (RECEIVED_AT_ASC[default], RECEIVED_AT_DESC)", + Destination: &c.order, + Action: func(_ *cli.Context, v string) error { + if v != "RECEIVED_AT_ASC" && v != "RECEIVED_AT_DESC" { + return fmt.Errorf("%w: should be one of [RECEIVED_AT_ASC, RECEIVED_AT_DESC]", errUnsupportedFlagValue) + } + return nil + }, + }, &cli.StringFlag{ + Name: "show", + Usage: "filter logs by criteria (ALL[default], PERSISTED_ONLY, TEMPORARY_ONLY)", + Destination: &c.showFilter, + Action: func(_ *cli.Context, v string) error { + if v != "ALL" && v != "PERSISTED_ONLY" && v != "TEMPORARY_ONLY" { + return fmt.Errorf("%w: should be one of [ALL, PERSISTED_ONLY, TEMPORARY_ONLY]", errUnsupportedFlagValue) + } + return nil + }, + }) +} + +func (c *cmdDeviceLogs) do(ctx context.Context) error { + if c.follow { + return c.doFollow(ctx) + } + return c.doList(ctx) +} + +func (c *cmdDeviceLogs) doFollow(ctx context.Context) error { + if c.from.Value() != nil { + return cli.Exit("Option received_at_from is unsupported in follow mode.", 1) + } + if c.to.Value() != nil { + return cli.Exit("Option received_at_to is unsupported in follow mode.", 1) + } + if c.offset > 0 { + return cli.Exit("Option offset is unsupported in follow mode.", 1) + } + if c.limit > 0 { + return cli.Exit("Option limit is unsupported in follow mode.", 1) + } + if c.order != "" { + return cli.Exit("Option order is unsupported in follow mode.", 1) + } + + query := url.Values{} + if c.severity != "" { + query.Add("severity", c.severity) + } + if c.showFilter != "" { + query.Add("show", c.showFilter) + } + + return c.runWebSocket(ctx, runWebSocketParams{ + Path: "/" + c.deviceID + "/logs", + Query: query, + RespProcessor: func(r io.Reader) error { + var msg struct { + Timestamp int64 `json:"timestamp"` + ReceivedAt string `json:"received_at"` + Severity string `json:"severity"` + Message string `json:"message"` + } + if err := json.NewDecoder(r).Decode(&msg); err != nil { + return fmt.Errorf("parse payload: %w", err) + } + fmt.Fprintf(c.writer, "%s [%s] %s\n", msg.ReceivedAt, msg.Severity, msg.Message) + return nil + }, + }) +} + +func (c *cmdDeviceLogs) doList(ctx context.Context) error { + query := url.Values{} + if c.from.Value() != nil { + query.Add("received_at_from", c.from.Value().Format(time.RFC3339)) + } + if c.to.Value() != nil { + query.Add("received_at_to", c.to.Value().Format(time.RFC3339)) + } + if c.offset > 0 { + query.Add("offset", strconv.Itoa(c.offset)) + } + if c.limit > 0 { + query.Add("limit", strconv.Itoa(c.limit)) + } + if c.severity != "" { + query.Add("severity", c.severity) + } + if c.order != "" { + query.Add("order", c.order) + } + if c.showFilter != "" { + query.Add("show", c.showFilter) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.deviceID + "/logs", + Query: query, + //nolint:bodyclose //body is closed in doHTTPRequest + RespProcessor: okRespBodyProcessor(func(body io.Reader) error { + var resp struct { + Logs []struct { + Timestamp int64 `json:"timestamp"` + ReceivedAt string `json:"received_at"` + Severity string `json:"severity"` + Message string `json:"message"` + } `json:"logs"` + } + if err := json.NewDecoder(body).Decode(&resp); err != nil { + return fmt.Errorf("parse response body: %w", err) + } + for _, l := range resp.Logs { + fmt.Fprintf(c.writer, "%s [%s] %s\n", l.ReceivedAt, l.Severity, l.Message) + } + return nil + }), + }) +} diff --git a/internal/app/enaptercli/cmd_device_run_terminal.go b/internal/app/enaptercli/cmd_device_run_terminal.go new file mode 100644 index 0000000..4da2af7 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_run_terminal.go @@ -0,0 +1,259 @@ +package enaptercli + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "time" + + "github.com/gorilla/websocket" + "github.com/urfave/cli/v2" + "golang.org/x/term" +) + +type cmdDeviceRunTerminal struct { + cmdDevice + deviceID string + winWidth int + winHeight int +} + +func buildCmdDeviceRunTerminal() *cli.Command { + cmd := &cmdDeviceRunTerminal{} + return &cli.Command{ + Name: "run-terminal", + Usage: "Run new remote terminal session", + Description: "Remote terminal feature should be enabled in gateway settings. " + + "Use Ctrl+] sequence to force connection close.", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceRunTerminal) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "gateway device ID", + Destination: &c.deviceID, + Required: true, + }) +} + +func (c *cmdDeviceRunTerminal) do(ctx context.Context) error { + fin := os.Stdin + fd := int(fin.Fd()) + if !term.IsTerminal(fd) { + return cli.Exit("Standard input should be a terminal.", 1) + } + + var credentials struct { + ChannelID string `json:"channel_id"` + Token string `json:"token"` + WebSocketURL string `json:"websocket_url"` + } + if err := c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/" + c.deviceID + "/run_terminal", + RespProcessor: func(r *http.Response) error { + if r.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(r), 1) + } + return json.NewDecoder(r.Body).Decode(&credentials) + }, + }); err != nil { + return err + } + + url, err := url.Parse(credentials.WebSocketURL) + if err != nil { + return fmt.Errorf("parse url: %w", err) + } + headers := make(http.Header) + headers.Set("Authorization", "Bearer "+credentials.Token) + + conn, err := c.dialWebSocket(ctx, url, headers) + if err != nil { + return fmt.Errorf("dial websocket: %w", err) + } + defer conn.Close() + + oldState, err := term.MakeRaw(fd) + if err != nil { + return fmt.Errorf("make raw terminal: %w", err) + } + defer func() { _ = term.Restore(fd, oldState) }() + + // TODO: wait for pong? + if err := c.writePing(conn, credentials.ChannelID); err != nil { + return fmt.Errorf("ping: %w", err) + } + + fmt.Fprint(c.writer, "Use Ctrl+] to terminate the session.\r\n\r\n") + + errCh := make(chan error) + stdinCh := make(chan byte) + go func() { errCh <- c.runFileReader(ctx, fin, stdinCh) }() + go func() { errCh <- c.runConnReader(ctx, conn) }() + + return c.run(ctx, conn, credentials.ChannelID, stdinCh, errCh, fd) +} + +func (c *cmdDeviceRunTerminal) runFileReader( + ctx context.Context, f *os.File, ch chan<- byte, +) error { + for { + select { + case <-ctx.Done(): + return nil + default: + } + + var buf [1]byte + n, err := f.Read(buf[:]) + if err != nil { + return err + } + + if n > 0 { + select { + case <-ctx.Done(): + return nil + case ch <- buf[0]: + } + } + } +} + +func (c *cmdDeviceRunTerminal) runConnReader( + ctx context.Context, conn *websocket.Conn, +) error { + for { + select { + case <-ctx.Done(): + return nil + default: + } + + var msg struct { + Data string `json:"data"` + } + if err := conn.ReadJSON(&msg); err != nil { + return fmt.Errorf("read: %w", err) + } + + var data []string + if err := json.Unmarshal([]byte(msg.Data), &data); err != nil { + return fmt.Errorf("unmarhal data: %w", err) + } + + if len(data) == 0 { + return cli.Exit("Unexpected payload from server.", 1) + } + if data[0] == "exit" { + return nil + } + if data[0] == "stdin" && len(data) > 1 { + fmt.Fprint(c.writer, data[1]) + } + } +} + +func (c *cmdDeviceRunTerminal) run( + ctx context.Context, conn *websocket.Conn, channelID string, + stdinCh <-chan byte, errCh <-chan error, fd int, +) error { + const keepAliveInterval = 30 * time.Second + const updateSizeInterval = time.Second + + keepAliveTicker := time.NewTicker(keepAliveInterval) + updateSizeTicker := time.NewTicker(updateSizeInterval) + + for { + select { + case <-ctx.Done(): + return nil + case err := <-errCh: + return err + default: + } + + select { + case <-ctx.Done(): + return nil + case err := <-errCh: + return err + case <-keepAliveTicker.C: + if err := c.writeKeepalive(conn, channelID); err != nil { + return err + } + case <-updateSizeTicker.C: + if err := c.writeSetSize(conn, channelID, fd); err != nil { + return err + } + case b := <-stdinCh: + const GS = 29 // ^] + if b == GS { + return cli.Exit("Exiting session.", 0) + } + if err := c.writeStdin(conn, channelID, b); err != nil { + return err + } + } + } +} + +func (c *cmdDeviceRunTerminal) writePing(conn *websocket.Conn, channelID string) error { + return conn.WriteJSON(map[string]any{ + "channel": channelID, + "data": `["ping"]`, + }) +} + +func (c *cmdDeviceRunTerminal) writeStdin(conn *websocket.Conn, channelID string, b byte) error { + data, err := json.Marshal([]string{"stdin", string(b)}) + if err != nil { + return err + } + return conn.WriteJSON(map[string]any{ + "channel": channelID, + "data": string(data), + }) +} + +func (c *cmdDeviceRunTerminal) writeKeepalive(conn *websocket.Conn, channelID string) error { + return conn.WriteJSON(map[string]any{ + "channel": channelID, + "data": `["keepalive_ping"]`, + }) +} + +func (c *cmdDeviceRunTerminal) writeSetSize(conn *websocket.Conn, channelID string, fd int) error { + w, h, err := term.GetSize(fd) + if err != nil { + // FIXME: error on Windows + return nil + } + if c.winWidth == w && c.winHeight == h { + return nil + } + + if err := conn.WriteJSON(map[string]any{ + "channel": channelID, + "data": fmt.Sprintf("[%q, %d, %d]", "set_size", h, w), + }); err != nil { + return err + } + + c.winWidth = w + c.winHeight = h + return nil +} diff --git a/internal/app/enaptercli/cmd_device_telemetry.go b/internal/app/enaptercli/cmd_device_telemetry.go new file mode 100644 index 0000000..d86b52a --- /dev/null +++ b/internal/app/enaptercli/cmd_device_telemetry.go @@ -0,0 +1,84 @@ +package enaptercli + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceTelemetry struct { + cmdDevice + deviceID string + follow bool +} + +func buildCmdDeviceTelemetry() *cli.Command { + cmd := &cmdDeviceTelemetry{} + return &cli.Command{ + Name: "telemetry", + Usage: "Show device telemetry", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceTelemetry) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, &cli.BoolFlag{ + Name: "follow", + Aliases: []string{"f"}, + Usage: "follow the telemetry output", + Destination: &c.follow, + }) +} + +func (c *cmdDeviceTelemetry) do(ctx context.Context) error { + if c.follow { + return c.doFollow(ctx) + } + return c.doList(ctx) +} + +func (c *cmdDeviceTelemetry) doFollow(ctx context.Context) error { + return c.runWebSocket(ctx, runWebSocketParams{ + Path: "/" + c.deviceID + "/telemetry", + RespProcessor: func(r io.Reader) error { + payload, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + fmt.Fprintln(c.writer, strings.TrimSpace(string(payload))) + return nil + }, + }) +} + +func (c *cmdDeviceTelemetry) doList(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.deviceID + "/telemetry", + //nolint:bodyclose //body is closed in doHTTPRequest + RespProcessor: okRespBodyProcessor(func(r io.Reader) error { + payload, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + fmt.Fprintln(c.writer, strings.TrimSpace(string(payload))) + return nil + }), + }) +} diff --git a/internal/app/enaptercli/cmd_device_update.go b/internal/app/enaptercli/cmd_device_update.go new file mode 100644 index 0000000..a4ebb31 --- /dev/null +++ b/internal/app/enaptercli/cmd_device_update.go @@ -0,0 +1,73 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdDeviceUpdate struct { + cmdDevice + deviceID string + name string + slug string +} + +func buildCmdDeviceUpdate() *cli.Command { + cmd := &cmdDeviceUpdate{} + return &cli.Command{ + Name: "update", + Usage: "Update a device", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdDeviceUpdate) Flags() []cli.Flag { + flags := c.cmdDevice.Flags() + return append(flags, + &cli.StringFlag{ + Name: "device-id", + Aliases: []string{"d"}, + Usage: "device ID", + Destination: &c.deviceID, + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Usage: "device name", + Destination: &c.name, + }, + &cli.StringFlag{ + Name: "slug", + Usage: "device slug", + Destination: &c.slug, + }, + ) +} + +func (c *cmdDeviceUpdate) do(ctx context.Context) error { + payload := map[string]string{ + "name": c.name, + "slug": c.slug, + } + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPatch, + Path: "/" + c.deviceID, + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_devices.go b/internal/app/enaptercli/cmd_devices.go deleted file mode 100644 index 7782b4a..0000000 --- a/internal/app/enaptercli/cmd_devices.go +++ /dev/null @@ -1,32 +0,0 @@ -package enaptercli - -import "github.com/urfave/cli/v2" - -type cmdDevices struct { - cmdBase - hardwareID string -} - -func buildCmdDevices() *cli.Command { - return &cli.Command{ - Name: "devices", - Usage: "Device information and management commands.", - Subcommands: []*cli.Command{ - buildCmdDevicesUpload(), - buildCmdDevicesLogs(), - buildCmdDevicesUploadLogs(), - buildCmdDevicesExecute(), - }, - } -} - -func (c *cmdDevices) Flags() []cli.Flag { - flags := c.cmdBase.Flags() - flags = append(flags, &cli.StringFlag{ - Name: "hardware-id", - Usage: "Hardware ID of the device; can be obtained in cloud.enapter.com", - Required: true, - Destination: &c.hardwareID, - }) - return flags -} diff --git a/internal/app/enaptercli/cmd_devices_execute.go b/internal/app/enaptercli/cmd_devices_execute.go deleted file mode 100644 index 6f15714..0000000 --- a/internal/app/enaptercli/cmd_devices_execute.go +++ /dev/null @@ -1,119 +0,0 @@ -package enaptercli - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/publichttp" -) - -type cmdDevicesExecute struct { - cmdDevices - commandName string - arguments string - showProgress bool -} - -func buildCmdDevicesExecute() *cli.Command { - cmd := &cmdDevicesExecute{} - - return &cli.Command{ - Name: "execute", - Usage: "Execute command on device", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.execute(cliCtx.Context) - }, - } -} - -func (c *cmdDevicesExecute) Flags() []cli.Flag { - flags := c.cmdDevices.Flags() - flags = append(flags, - &cli.StringFlag{ - Name: "command", - Usage: "Command name", - Required: true, - Destination: &c.commandName, - }, - &cli.StringFlag{ - Name: "arguments", - Usage: "Command arguments as JSON object", - Destination: &c.arguments, - }, - &cli.BoolFlag{ - Name: "show-progress", - Usage: "Enable in-progress responses streaming", - Destination: &c.showProgress, - }, - ) - return flags -} - -func (c *cmdDevicesExecute) execute(ctx context.Context) error { - transport := publichttp.NewAuthTokenTransport(http.DefaultTransport, c.token) - client, err := publichttp.NewClientWithURL(&http.Client{Transport: transport}, c.apiHost) - if err != nil { - return fmt.Errorf("create http client: %w", err) - } - - var arguments map[string]interface{} - if c.arguments != "" { - if err := json.Unmarshal([]byte(c.arguments), &arguments); err != nil { - return fmt.Errorf("parse arguments: %w", err) - } - } - - query := publichttp.CommandQuery{ - HardwareID: c.hardwareID, - CommandName: c.commandName, - Arguments: arguments, - } - - if c.showProgress { - return c.executeWithProgress(ctx, client, query) - } - - response, err := client.Commands.Execute(ctx, query) - if err != nil { - return err - } - return c.print(response) -} - -func (c *cmdDevicesExecute) executeWithProgress( - ctx context.Context, client *publichttp.Client, - query publichttp.CommandQuery, -) error { - progressCh, err := client.Commands.ExecuteWithProgress(ctx, query) - if err != nil { - return err - } - - for progress := range progressCh { - if progress.Error != nil { - return progress.Error - } - err := c.print(progress.CommandResponse) - if err != nil { - return err - } - } - - return nil -} - -func (c *cmdDevicesExecute) print(r publichttp.CommandResponse) error { - s, err := json.Marshal(r) - if err != nil { - return fmt.Errorf("format response: %w", err) - } - fmt.Fprintln(c.writer, string(s)) - return nil -} diff --git a/internal/app/enaptercli/cmd_devices_execute_test.go b/internal/app/enaptercli/cmd_devices_execute_test.go deleted file mode 100644 index 3b69ecc..0000000 --- a/internal/app/enaptercli/cmd_devices_execute_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package enaptercli_test - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/bxcodec/faker/v3" - "github.com/stretchr/testify/require" -) - -func TestDeviceExecute(t *testing.T) { - t.Run("simple", func(t *testing.T) { - basePath := "testdata/device_execute/simple" - showProgress := false - testDeviceExecute(t, basePath, showProgress, http.StatusOK) - }) - - t.Run("progress", func(t *testing.T) { - basePath := "testdata/device_execute/progress" - showProgress := true - testDeviceExecute(t, basePath, showProgress, http.StatusOK) - }) - - t.Run("error", func(t *testing.T) { - basePath := "testdata/device_execute/error" - showProgress := false - testDeviceExecute(t, basePath, showProgress, http.StatusForbidden) - }) -} - -func testDeviceExecute( - t *testing.T, basePath string, showProgress bool, statusCode int, -) { - resp := readFileLines(t, filepath.Join(basePath, "responses")) - server := startExecuteTestServer(showProgress, statusCode, resp) - defer server.Close() - - args := []string{"enapter", "devices", "execute"} - args = append(args, - "--token", faker.Word(), - "--hardware-id", faker.Word(), - "--command", faker.Word(), - "--api-host", server.URL) - if showProgress { - args = append(args, "--show-progress") - } - - checkExecuteTestAppOutput(t, basePath, args) -} - -func readFileLines(t *testing.T, path string) [][]byte { - f, err := os.ReadFile(path) - require.NoError(t, err) - return bytes.Split(f, []byte{'\n'}) -} - -func startExecuteTestServer( - showProgress bool, statusCode int, responses [][]byte, -) *httptest.Server { - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(statusCode) - - for _, r := range responses { - _, _ = w.Write(append(r, '\n')) - if showProgress { - w.(http.Flusher).Flush() - } - } - } - return httptest.NewServer(http.HandlerFunc(handler)) -} - -func checkExecuteTestAppOutput(t *testing.T, basePath string, args []string) { - app := startTestApp(args...) - defer app.Stop() - - appErr := app.Wait() - - actual, err := io.ReadAll(app.Stdout()) - require.NoError(t, err) - - if appErr != nil { - actual = append(actual, []byte("app exit with error: "+appErr.Error()+"\n")...) - } - - expectedFileName := filepath.Join(basePath, "output") - if update { - err := os.WriteFile(expectedFileName, actual, 0o600) - require.NoError(t, err) - } - - expected, err := os.ReadFile(expectedFileName) - require.NoError(t, err) - - require.Equal(t, string(expected), string(actual)) -} diff --git a/internal/app/enaptercli/cmd_devices_logs.go b/internal/app/enaptercli/cmd_devices_logs.go deleted file mode 100644 index 1436c40..0000000 --- a/internal/app/enaptercli/cmd_devices_logs.go +++ /dev/null @@ -1,42 +0,0 @@ -package enaptercli - -import ( - "context" - "fmt" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/cloudapi" -) - -type cmdDevicesLogs struct { - cmdDevices -} - -func buildCmdDevicesLogs() *cli.Command { - cmd := &cmdDevicesLogs{} - - return &cli.Command{ - Name: "logs", - Usage: "Stream logs from a device", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.run(cliCtx.Context, cliCtx.App.Version) - }, - } -} - -func (c *cmdDevicesLogs) run(ctx context.Context, version string) error { - writer, err := cloudapi.NewDeviceLogsWriter(c.websocketsURL, c.token, - version, c.hardwareID, c.writeLog) - if err != nil { - return fmt.Errorf("create writer: %w", err) - } - return writer.Run(ctx) -} - -func (c *cmdDevicesLogs) writeLog(topic, msg string) { - fmt.Fprintf(c.writer, "[%s] %s\n", topic, msg) -} diff --git a/internal/app/enaptercli/cmd_devices_logs_test.go b/internal/app/enaptercli/cmd_devices_logs_test.go deleted file mode 100644 index 250c353..0000000 --- a/internal/app/enaptercli/cmd_devices_logs_test.go +++ /dev/null @@ -1,41 +0,0 @@ -//nolint:dupl // not a duplicate of `rules logs` command tests -package enaptercli_test - -import ( - "strings" - "testing" -) - -func TestDeviceLogs(t *testing.T) { - t.Run("simple", func(t *testing.T) { - inputFileName := "testdata/device_logs/simple/input" - untilLinePrefix := "[telemetry]" - expectedFileName := "testdata/device_logs/simple/output" - testDeviceLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) - - t.Run("invalid token", func(t *testing.T) { - inputFileName := "testdata/device_logs/disconnect/invalid_token/input" - untilLinePrefix := "[connection]" - expectedFileName := "testdata/device_logs/disconnect/invalid_token/output" - testDeviceLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) - - t.Run("device not found", func(t *testing.T) { - inputFileName := "testdata/device_logs/disconnect/device_not_found/input" - untilLinePrefix := "[connection] disconnected" - expectedFileName := "testdata/device_logs/disconnect/device_not_found/output" - testDeviceLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) -} - -func testDeviceLogs(t *testing.T, inputFileName, untilLinePrefix, expectedFileName string) { - const hardwareID = "SIM-WTM" - - identifier := map[string]string{"channel": "DeviceChannel", "hardware_id": hardwareID} - - command := strings.Split("enapter devices logs", " ") - command = append(command, "--hardware-id", hardwareID) - - testLogsCommand(t, inputFileName, untilLinePrefix, expectedFileName, identifier, command) -} diff --git a/internal/app/enaptercli/cmd_devices_upload.go b/internal/app/enaptercli/cmd_devices_upload.go deleted file mode 100644 index d8c485e..0000000 --- a/internal/app/enaptercli/cmd_devices_upload.go +++ /dev/null @@ -1,156 +0,0 @@ -package enaptercli - -import ( - "bytes" - "context" - "encoding/base64" - "fmt" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/cloudapi" -) - -const deviceUploadDefaultTimeout = 30 * time.Second - -type cmdDevicesUpload struct { - cmdDevices - blueprintDir string - timeout time.Duration -} - -func buildCmdDevicesUpload() *cli.Command { - cmd := &cmdDevicesUpload{} - - return &cli.Command{ - Name: "upload", - Usage: "Upload blueprint to a device", - Description: "Blueprint combines device capabilities declaration and Lua firmware for Enapter UCM. " + - "The command updates device blueprint and uploads the firmware to the UCM. Learn more " + - "about Enapter Blueprints at https://handbook.enapter.com/blueprints.", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.upload(cliCtx.Context, cliCtx.App.Version) - }, - } -} - -func (c *cmdDevicesUpload) Flags() []cli.Flag { - flags := c.cmdDevices.Flags() - flags = append(flags, - &cli.DurationFlag{ - Name: "timeout", - Usage: "Time to wait for blueprint uploading", - Destination: &c.timeout, - Value: deviceUploadDefaultTimeout, - }, - &cli.StringFlag{ - Name: "blueprint-dir", - Usage: "Directory which contains blueprint file", - Required: true, - Destination: &c.blueprintDir, - }, - ) - return flags -} - -func (c *cmdDevicesUpload) upload(ctx context.Context, version string) error { - if c.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.timeout) - defer cancel() - } - - files, err := c.blueprintFilesList() - if err != nil { - return err - } - - fmt.Fprintln(c.writer, "Blueprint files to be uploaded:") - for _, name := range files { - fmt.Fprintln(c.writer, "*", name) - } - - zipBytes, err := c.blueprintZip() - if err != nil { - return err - } - - onceWriter := &onceWriter{w: c.writer} - transport := cloudapi.NewCredentialsTransport(http.DefaultTransport, c.token, version) - transport = cloudapi.NewCLIMessageWriterTransport(transport, onceWriter) - client := cloudapi.NewClientWithURL(&http.Client{Transport: transport}, c.graphqlURL) - - uploadData, uploadErrors, err := client.UploadBlueprint(ctx, c.hardwareID, zipBytes) - if err != nil { - return fmt.Errorf("do update: %w", err) - } - - if len(uploadErrors) != 0 { - for _, e := range uploadErrors { - fmt.Fprintln(c.writer, "[ERROR]", e.Message) - } - return errFinishedWithError - } - - fmt.Fprintln(c.writer, "upload started with operation id", uploadData.OperationID) - - err = client.WriteOperationLogs(ctx, c.hardwareID, uploadData.OperationID, c.writeLog) - if err != nil { - return fmt.Errorf("receive operation logs: %w", err) - } - - fmt.Fprintln(c.writer, "Done!") - return nil -} - -func (c *cmdDevicesUpload) blueprintFilesList() ([]string, error) { - var files []string - - err := filepath.Walk(c.blueprintDir, - func(name string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - files = append(files, name) - } - return nil - }) - if err != nil { - return nil, err - } - - return files, nil -} - -func (c *cmdDevicesUpload) blueprintZip() ([]byte, error) { - bpBytes, err := zipDir(c.blueprintDir) - if err != nil { - return nil, fmt.Errorf("failed to zip blueprint dir %q: %w", c.blueprintDir, err) - } - - zipBuf := &bytes.Buffer{} - zipBuf.WriteString("data:application/gzip;base64,") - enc := base64.NewEncoder(base64.StdEncoding, zipBuf) - _, err = enc.Write(bpBytes) - if err != nil { - return nil, fmt.Errorf("failed to encode blueprint as base64: %w", err) - } - - if err := enc.Close(); err != nil { - return nil, fmt.Errorf("failed to encode blueprint as base64: %w", err) - } - - return zipBuf.Bytes(), nil -} - -func (c *cmdDevicesUpload) writeLog(operationID string, l cloudapi.OperationLog) { - fmt.Fprintf(c.writer, "[#%s] %s [%s] %s\n", operationID, l.CreatedAt, l.Severity, l.Payload) -} diff --git a/internal/app/enaptercli/cmd_devices_upload_logs.go b/internal/app/enaptercli/cmd_devices_upload_logs.go deleted file mode 100644 index 7051808..0000000 --- a/internal/app/enaptercli/cmd_devices_upload_logs.go +++ /dev/null @@ -1,74 +0,0 @@ -package enaptercli - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/cloudapi" -) - -type cmdDevicesUploadLogs struct { - cmdDevices - operationID string - timeout time.Duration -} - -func buildCmdDevicesUploadLogs() *cli.Command { - cmd := &cmdDevicesUploadLogs{} - - return &cli.Command{ - Name: "upload-logs", - Usage: "Show blueprint uploading logs", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.run(cliCtx.Context, cliCtx.App.Version) - }, - } -} - -func (c *cmdDevicesUploadLogs) Flags() []cli.Flag { - flags := c.cmdDevices.Flags() - flags = append(flags, - &cli.DurationFlag{ - Name: "timeout", - Usage: "Time to wait for blueprint uploading", - Destination: &c.timeout, - Value: deviceUploadDefaultTimeout, - }, - &cli.StringFlag{ - Name: "operation-id", - Usage: "Uploading operation ID (optional)", - Destination: &c.operationID, - }, - ) - return flags -} - -func (c *cmdDevicesUploadLogs) run(ctx context.Context, version string) error { - if c.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.timeout) - defer cancel() - } - - transport := cloudapi.NewCredentialsTransport(http.DefaultTransport, c.token, version) - transport = cloudapi.NewCLIMessageWriterTransport(transport, &onceWriter{w: c.writer}) - client := cloudapi.NewClientWithURL(&http.Client{Transport: transport}, c.graphqlURL) - - if c.operationID != "" { - return client.WriteOperationLogs(ctx, c.hardwareID, c.operationID, c.writeLog) - } - - const lastOperationsNumber = 2 - return client.WriteLastOperationsLogs(ctx, c.hardwareID, lastOperationsNumber, c.writeLog) -} - -func (c *cmdDevicesUploadLogs) writeLog(operationID string, l cloudapi.OperationLog) { - fmt.Fprintf(c.writer, "[#%s] %s [%s] %s\n", operationID, l.CreatedAt, l.Severity, l.Payload) -} diff --git a/internal/app/enaptercli/cmd_devices_upload_logs_test.go b/internal/app/enaptercli/cmd_devices_upload_logs_test.go deleted file mode 100644 index d574df4..0000000 --- a/internal/app/enaptercli/cmd_devices_upload_logs_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package enaptercli_test - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/bxcodec/faker/v3" - "github.com/stretchr/testify/require" -) - -func TestDeviceUploadLogs(t *testing.T) { - errorsDir := "testdata/device_upload_logs" - dirs, err := os.ReadDir(errorsDir) - require.NoError(t, err) - - for _, dir := range dirs { - if !dir.IsDir() { - continue - } - - dir := dir - t.Run(dir.Name(), func(t *testing.T) { - testDeviceUploadLogs(t, filepath.Join(errorsDir, dir.Name())) - }) - } -} - -type devicesUploadLogsTestSettings struct { - OperationID string `json:"operation_id"` - HardwareID string `json:"hardware_id"` - CliMessage string `json:"cli_message"` - Token string `json:"-"` -} - -func (s *devicesUploadLogsTestSettings) Fill(t *testing.T, dir string) { - settingsBytes, err := os.ReadFile(filepath.Join(dir, "settings.json")) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(settingsBytes, s)) - s.Token = faker.Word() -} - -func testDeviceUploadLogs(t *testing.T, dir string) { - var settings devicesUploadLogsTestSettings - settings.Fill(t, dir) - - reqs := byteSliceSliceFromFile(t, filepath.Join(dir, "requests")) - resps := byteSliceSliceFromFile(t, filepath.Join(dir, "responses")) - - srv := startTestServer(reqs, resps, settings.CliMessage) - defer srv.Close() - - args := strings.Split("enapter devices upload-logs", " ") - args = append(args, - "--token", settings.Token, - "--hardware-id", settings.HardwareID, - "--gql-api-url", srv.URL) - if settings.OperationID != "" { - args = append(args, "--operation-id", settings.OperationID) - } - - checkTestAppOutput(t, dir, args, reqs) -} diff --git a/internal/app/enaptercli/cmd_devices_upload_test.go b/internal/app/enaptercli/cmd_devices_upload_test.go deleted file mode 100644 index 4cfe889..0000000 --- a/internal/app/enaptercli/cmd_devices_upload_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package enaptercli_test - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/bxcodec/faker/v3" - "github.com/stretchr/testify/require" -) - -const blueprintDir = "testdata/device_upload/simple/blueprint" - -func TestDeviceUpload(t *testing.T) { - testdataDir := "testdata/device_upload" - dirs, err := os.ReadDir(testdataDir) - require.NoError(t, err) - - for _, dir := range dirs { - if !dir.IsDir() { - continue - } - - dir := dir - t.Run(dir.Name(), func(t *testing.T) { - testDeviceUpload(t, filepath.Join(testdataDir, dir.Name()), blueprintDir) - }) - } -} - -func TestDeviceUploadBlueprintDirWithDot(t *testing.T) { - testDeviceUpload(t, "testdata/device_upload/simple", "./"+blueprintDir) -} - -func TestDeviceUploadWrongBlueprintDir(t *testing.T) { - args := strings.Split("enapter devices upload --token token --hardware-id hardwareID "+ - "--gql-api-url apiURL --blueprint-dir wrong", " ") - app := startTestApp(args...) - defer app.Stop() - - appErr := app.Wait() - require.EqualError(t, appErr, `lstat wrong: no such file or directory`) -} - -type devicesUploadTestSettings struct { - HardwareID string `json:"hardware_id"` - CliMessage string `json:"cli_message"` - Token string `json:"-"` -} - -func (s *devicesUploadTestSettings) Fill(t *testing.T, dir string) { - settingsBytes, err := os.ReadFile(filepath.Join(dir, "settings.json")) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(settingsBytes, s)) - s.Token = faker.Word() -} - -func testDeviceUpload(t *testing.T, dir, blueprintDir string) { - var settings devicesUploadTestSettings - settings.Fill(t, dir) - - reqs := byteSliceSliceFromFile(t, filepath.Join(dir, "requests")) - resps := byteSliceSliceFromFile(t, filepath.Join(dir, "responses")) - - srv := startTestServer(reqs, resps, settings.CliMessage) - defer srv.Close() - - args := strings.Split("enapter devices upload", " ") - args = append(args, - "--token", settings.Token, - "--hardware-id", settings.HardwareID, - "--blueprint-dir", blueprintDir, - "--gql-api-url", srv.URL) - - checkTestAppOutput(t, dir, args, reqs) -} diff --git a/internal/app/enaptercli/cmd_logs_test.go b/internal/app/enaptercli/cmd_logs_test.go deleted file mode 100644 index 0db80b7..0000000 --- a/internal/app/enaptercli/cmd_logs_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package enaptercli_test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "reflect" - "strings" - "testing" - "time" - - "github.com/bxcodec/faker/v3" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" -) - -func testLogsCommand( - t *testing.T, inputFileName, untilLinePrefix, expectedFileName string, - identifier map[string]string, args []string, -) { - token := faker.Word() - messagesBytes, err := os.ReadFile(inputFileName) - require.NoError(t, err) - messages := bytes.Split(messagesBytes, []byte{'\n'}) - - handleErrCh := make(chan string) - wsPath, srv := startTestLogsServer(t, token, identifier, messages, handleErrCh) - defer srv.Close() - - args = append(args, "--token", token, "--ws-api-url", wsPath) - app := startTestApp(args...) - defer app.Stop() - - actual := readOutputUntilLineOrError(t, app.Stdout(), untilLinePrefix, handleErrCh) - if update { - err := os.WriteFile(expectedFileName, []byte(actual), 0o600) - require.NoError(t, err) - } - - expected, err := os.ReadFile(expectedFileName) - require.NoError(t, err) - require.Equal(t, string(expected), actual) - - app.Stop() - appErr := app.Wait() - require.NoError(t, appErr) - - restOutput, err := io.ReadAll(app.Stdout()) - require.NoError(t, err) - require.Empty(t, string(restOutput)) -} - -func startTestLogsServer( - t *testing.T, token string, identifier map[string]string, messages [][]byte, - handleErrCh chan<- string, -) (string, *httptest.Server) { - t.Helper() - - handler := buildTestLogsHandler(token, identifier, messages, handleErrCh) - srv := httptest.NewServer(handler) - - u, err := url.Parse(srv.URL) - require.NoError(t, err) - u.Scheme = "ws" - - return u.String(), srv -} - -func readOutputUntilLineOrError( - t *testing.T, r *lineBuffer, prefix string, wsHandleErrCh <-chan string, -) string { - t.Helper() - - readStr, readErr := startBackgroundReadUntilLine(r, prefix) - - timer := time.NewTimer(5 * time.Second) - select { - case <-timer.C: - require.Fail(t, "read output timed out") - case errStr := <-wsHandleErrCh: - require.Failf(t, "ws handler finished with error", errStr) - case err := <-readErr: - require.Failf(t, "read finished with error", err.Error()) - case s := <-readStr: - return s - } - - return "" -} - -func startBackgroundReadUntilLine(r *lineBuffer, prefix string) (<-chan string, <-chan error) { - readStr := make(chan string, 1) - readErr := make(chan error, 1) - - go func() { - buf := strings.Builder{} - - for { - s, err := r.ReadLine() - if err != nil { - readErr <- err - return - } - - buf.WriteString(s) - - if strings.HasPrefix(s, prefix) { - readStr <- buf.String() - return - } - } - }() - - return readStr, readErr -} - -//nolint:funlen // because contains a lot of simple logged checks. -func buildTestLogsHandler( - token string, identifier map[string]string, messages [][]byte, handleErrCh chan<- string, -) http.Handler { - f := func(w http.ResponseWriter, r *http.Request) { - reqToken := r.URL.Query().Get("token") - if reqToken != token { - w.WriteHeader(http.StatusBadRequest) - handleErrCh <- fmt.Sprintf("unexpected token %q, should be %q", reqToken, token) - return - } - - upgrader := websocket.Upgrader{} - c, err := upgrader.Upgrade(w, r, nil) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - handleErrCh <- fmt.Sprintf("failed to upgrade: %s", err) - return - } - defer c.Close() - - msgType, msgBytes, err := c.ReadMessage() - if err != nil { - handleErrCh <- fmt.Sprintf("failed to read subscribe message: %s", err) - return - } - - if msgType != websocket.TextMessage { - handleErrCh <- fmt.Sprintf("subscribe message should be text type [%d], but [%d]", - websocket.TextMessage, msgType) - return - } - - subMsg := struct { - Command string `json:"command"` - Identifier string `json:"identifier"` - }{} - if err := json.Unmarshal(msgBytes, &subMsg); err != nil { - handleErrCh <- fmt.Sprintf("failed to unmarshall subsribe message %q: %s", string(msgBytes), err.Error()) - return - } - - if subMsg.Command != "subscribe" { - handleErrCh <- fmt.Sprintf("this is not subscribe message, but %q", subMsg.Command) - return - } - - var reqIdentifier map[string]string - if err := json.Unmarshal([]byte(subMsg.Identifier), &reqIdentifier); err != nil { - handleErrCh <- fmt.Sprintf("failed to unmarshall subsribe message identifier %q: %s", - subMsg.Identifier, err.Error()) - return - } - - if !reflect.DeepEqual(identifier, reqIdentifier) { - handleErrCh <- fmt.Sprintf("subsribe message identifier shoud be equal to %q, but %q", - identifier, reqIdentifier) - return - } - - for _, m := range messages { - if len(m) == 0 { - continue - } - if err := c.WriteMessage(websocket.TextMessage, m); err != nil { - handleErrCh <- fmt.Sprintf("failed to write message: %s", err.Error()) - return - } - } - - <-r.Context().Done() - } - - return http.HandlerFunc(f) -} diff --git a/internal/app/enaptercli/cmd_rule_engine.go b/internal/app/enaptercli/cmd_rule_engine.go new file mode 100644 index 0000000..16c0a56 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine.go @@ -0,0 +1,53 @@ +package enaptercli + +import ( + "context" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngine struct { + cmdBase + siteID string +} + +func buildCmdRuleEngine() *cli.Command { + cmd := &cmdRuleEngine{} + return &cli.Command{ + Name: "rule-engine", + Usage: "Manage the rule engine", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdRuleEngineGet(), + buildCmdRuleEngineSuspend(), + buildCmdRuleEngineResume(), + buildCmdRuleEngineRule(), + }, + } +} + +func (c *cmdRuleEngine) Flags() []cli.Flag { + flags := c.cmdBase.Flags() + return append(flags, &cli.StringFlag{ + Name: "site-id", + Usage: "site ID", + Destination: &c.siteID, + }) +} + +func (c *cmdRuleEngine) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + siteID, err := c.chooseSiteID(c.siteID) + if err != nil { + return err + } + + path, err := url.JoinPath("/sites/", siteID, "/rule_engine", p.Path) + if err != nil { + return fmt.Errorf("join path: %w", err) + } + + p.Path = path + return c.cmdBase.doHTTPRequest(ctx, p) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_get.go b/internal/app/enaptercli/cmd_rule_engine_get.go new file mode 100644 index 0000000..69a9bf4 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_get.go @@ -0,0 +1,33 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineGet struct { + cmdRuleEngine +} + +func buildCmdRuleEngineGet() *cli.Command { + cmd := &cmdRuleEngineGet{} + return &cli.Command{ + Name: "get", + Usage: "Retrieve the rule engine", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineGet) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "", + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_resume.go b/internal/app/enaptercli/cmd_rule_engine_resume.go new file mode 100644 index 0000000..b40b0eb --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_resume.go @@ -0,0 +1,33 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineResume struct { + cmdRuleEngine +} + +func buildCmdRuleEngineResume() *cli.Command { + cmd := &cmdRuleEngineResume{} + return &cli.Command{ + Name: "resume", + Usage: "Resume execution of rules", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineResume) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/resume", + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule.go b/internal/app/enaptercli/cmd_rule_engine_rule.go new file mode 100644 index 0000000..7fdfbf0 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule.go @@ -0,0 +1,52 @@ +package enaptercli + +import ( + "context" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +const ( + ruleRuntimeV1 = "V1" + ruleRuntimeV3 = "V3" +) + +type cmdRuleEngineRule struct { + cmdRuleEngine +} + +func buildCmdRuleEngineRule() *cli.Command { + cmd := &cmdRuleEngineRule{} + return &cli.Command{ + Name: "rule", + Usage: "Manage rules", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdRuleEngineRuleCreate(), + buildCmdRuleEngineRuleDelete(), + buildCmdRuleEngineRuleDisable(), + buildCmdRuleEngineRuleEnable(), + buildCmdRuleEngineRuleGet(), + buildCmdRuleEngineRuleList(), + buildCmdRuleEngineRuleUpdate(), + buildCmdRuleEngineRuleUpdateScript(), + buildCmdRuleEngineRuleLogs(), + }, + } +} + +func (c *cmdRuleEngineRule) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + path, err := url.JoinPath("/rules", p.Path) + if err != nil { + return fmt.Errorf("join path: %w", err) + } + p.Path = path + return c.cmdRuleEngine.doHTTPRequest(ctx, p) +} + +func (c *cmdRuleEngineRule) validateRuntimeVersion(value string) error { + supportedVersions := []string{ruleRuntimeV1, ruleRuntimeV3} + return validateFlag("runtime-version", value, supportedVersions) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_create.go b/internal/app/enaptercli/cmd_rule_engine_rule_create.go new file mode 100644 index 0000000..d865b06 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_create.go @@ -0,0 +1,108 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/cliflags" +) + +type cmdRuleEngineRuleCreate struct { + cmdRuleEngineRule + slug string + scriptPath string + runtimeVersion string + execInterval time.Duration + disable bool +} + +func buildCmdRuleEngineRuleCreate() *cli.Command { + cmd := &cmdRuleEngineRuleCreate{} + return &cli.Command{ + Name: "create", + Usage: "Create a new rule", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleCreate) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "slug", + Usage: "Slug for the new rule", + Destination: &c.slug, + Required: true, + }, + &cli.StringFlag{ + Name: "script", + Usage: "Path to the file containing the script code", + Destination: &c.scriptPath, + Required: true, + }, + &cli.StringFlag{ + Name: "runtime-version", + Usage: "Version of the runtime to use for the script execution", + Destination: &c.runtimeVersion, + Value: ruleRuntimeV3, + Action: func(_ *cli.Context, v string) error { + return c.validateRuntimeVersion(v) + }, + }, + &cliflags.Duration{ + DurationFlag: cli.DurationFlag{ + Name: "exec-interval", + Usage: "How frequently to execute the script " + + "(compatible only with runtime version 1) in duration format (e.g., 5s, 2m)", + Destination: &c.execInterval, + }, + }, + &cli.BoolFlag{ + Name: "disable", + Usage: "Disable the rule upon creation", + Destination: &c.disable, + }, + ) +} + +func (c *cmdRuleEngineRuleCreate) do(ctx context.Context) error { + if c.scriptPath == "-" { + c.scriptPath = "/dev/stdin" + } + scriptBytes, err := os.ReadFile(c.scriptPath) + if err != nil { + return fmt.Errorf("read script code file: %w", err) + } + + body, err := json.Marshal(map[string]any{ + "slug": c.slug, + "script": map[string]any{ + "code": base64.StdEncoding.EncodeToString(scriptBytes), + "runtime_version": c.runtimeVersion, + "exec_interval": c.execInterval.String(), + }, + "disable": c.disable, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_delete.go b/internal/app/enaptercli/cmd_rule_engine_rule_delete.go new file mode 100644 index 0000000..ef82b7d --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_delete.go @@ -0,0 +1,46 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleDelete struct { + cmdRuleEngineRule + ruleID string +} + +func buildCmdRuleEngineRuleDelete() *cli.Command { + cmd := &cmdRuleEngineRuleDelete{} + return &cli.Command{ + Name: "delete", + Usage: "Delete a rule", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleDelete) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "rule-id", + Usage: "Rule ID or slug", + Required: true, + Destination: &c.ruleID, + }, + ) +} + +func (c *cmdRuleEngineRuleDelete) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodDelete, + Path: "/" + c.ruleID, + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_disable.go b/internal/app/enaptercli/cmd_rule_engine_rule_disable.go new file mode 100644 index 0000000..a5c76fc --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_disable.go @@ -0,0 +1,58 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleDisable struct { + cmdRuleEngineRule + ruleIDs []string +} + +func buildCmdRuleEngineRuleDisable() *cli.Command { + cmd := &cmdRuleEngineRuleDisable{} + return &cli.Command{ + Name: "disable", + Usage: "Disable one or more rules", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleDisable) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.MultiStringFlag{ + Target: &cli.StringSliceFlag{ + Name: "rule-id", + Usage: "Rule IDs or slugs", + Required: true, + }, + Destination: &c.ruleIDs, + }, + ) +} + +func (c *cmdRuleEngineRuleDisable) do(ctx context.Context) error { + body, err := json.Marshal(map[string]any{ + "rule_ids": c.ruleIDs, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/batch_disable", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_enable.go b/internal/app/enaptercli/cmd_rule_engine_rule_enable.go new file mode 100644 index 0000000..44e9580 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_enable.go @@ -0,0 +1,58 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleEnable struct { + cmdRuleEngineRule + ruleIDs []string +} + +func buildCmdRuleEngineRuleEnable() *cli.Command { + cmd := &cmdRuleEngineRuleEnable{} + return &cli.Command{ + Name: "enable", + Usage: "Enable one or more rules", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleEnable) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.MultiStringFlag{ + Target: &cli.StringSliceFlag{ + Name: "rule-id", + Usage: "Rule IDs or slugs", + Required: true, + }, + Destination: &c.ruleIDs, + }, + ) +} + +func (c *cmdRuleEngineRuleEnable) do(ctx context.Context) error { + body, err := json.Marshal(map[string]any{ + "rule_ids": c.ruleIDs, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/batch_enable", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_get.go b/internal/app/enaptercli/cmd_rule_engine_rule_get.go new file mode 100644 index 0000000..abf31f1 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_get.go @@ -0,0 +1,45 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleGet struct { + cmdRuleEngineRule + ruleID string +} + +func buildCmdRuleEngineRuleGet() *cli.Command { + cmd := &cmdRuleEngineRuleGet{} + return &cli.Command{ + Name: "get", + Usage: "Retrieve a rule", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleGet) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "rule-id", + Usage: "Rule ID or slug", + Destination: &c.ruleID, + Required: true, + }, + ) +} + +func (c *cmdRuleEngineRuleGet) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.ruleID, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_list.go b/internal/app/enaptercli/cmd_rule_engine_rule_list.go new file mode 100644 index 0000000..cb1de8e --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_list.go @@ -0,0 +1,33 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleList struct { + cmdRuleEngineRule +} + +func buildCmdRuleEngineRuleList() *cli.Command { + cmd := &cmdRuleEngineRuleList{} + return &cli.Command{ + Name: "list", + Usage: "List rules", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleList) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "", + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_logs.go b/internal/app/enaptercli/cmd_rule_engine_rule_logs.go new file mode 100644 index 0000000..6e328ee --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_logs.go @@ -0,0 +1,72 @@ +package enaptercli + +import ( + "encoding/json" + "fmt" + "io" + "time" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleLogs struct { + cmdRuleEngineRule + ruleID string + follow bool +} + +func buildCmdRuleEngineRuleLogs() *cli.Command { + cmd := &cmdRuleEngineRuleLogs{} + return &cli.Command{ + Name: "logs", + Usage: "Show rule logs", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx) + }, + } +} + +func (c *cmdRuleEngineRuleLogs) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "rule-id", + Usage: "rule ID", + Destination: &c.ruleID, + Required: true, + }, + &cli.BoolFlag{ + Name: "follow", + Aliases: []string{"f"}, + Usage: "follow the log output", + Destination: &c.follow, + }, + ) +} + +func (c *cmdRuleEngineRuleLogs) do(cliCtx *cli.Context) error { + if !c.follow { + return cli.Exit("Currently, only follow mode (--follow) is supported.", 1) + } + + path := fmt.Sprintf("/site/rule_engine/rules/%s/logs", c.ruleID) + + return c.runWebSocket(cliCtx.Context, runWebSocketParams{ + Path: path, + RespProcessor: func(r io.Reader) error { + var msg struct { + Timestamp int64 `json:"timestamp"` + Severity string `json:"severity"` + Message string `json:"message"` + } + if err := json.NewDecoder(r).Decode(&msg); err != nil { + return fmt.Errorf("parse payload: %w", err) + } + ts := time.Unix(msg.Timestamp, 0).Format(time.RFC3339) + fmt.Fprintf(c.writer, "%s [%s] %s\n", ts, msg.Severity, msg.Message) + return nil + }, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_update.go b/internal/app/enaptercli/cmd_rule_engine_rule_update.go new file mode 100644 index 0000000..9009ec6 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_update.go @@ -0,0 +1,66 @@ +package enaptercli + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineRuleUpdate struct { + cmdRuleEngineRule + ruleID string + slug string +} + +func buildCmdRuleEngineRuleUpdate() *cli.Command { + cmd := &cmdRuleEngineRuleUpdate{} + return &cli.Command{ + Name: "update", + Usage: "Update a rule", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx) + }, + } +} + +func (c *cmdRuleEngineRuleUpdate) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "rule-id", + Usage: "Rule ID or slug to update", + Destination: &c.ruleID, + Required: true, + }, + &cli.StringFlag{ + Name: "slug", + Usage: "A new rule slug", + Destination: &c.slug, + }, + ) +} + +func (c *cmdRuleEngineRuleUpdate) do(cliCtx *cli.Context) error { + payload := make(map[string]any) + + if cliCtx.IsSet("slug") { + payload["slug"] = c.slug + } + + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(cliCtx.Context, doHTTPRequestParams{ + Method: http.MethodPatch, + Path: "/" + c.ruleID, + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_update_script.go b/internal/app/enaptercli/cmd_rule_engine_rule_update_script.go new file mode 100644 index 0000000..9e1ee24 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_rule_update_script.go @@ -0,0 +1,100 @@ +package enaptercli + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "github.com/urfave/cli/v2" + + "github.com/enapter/enapter-cli/internal/app/cliflags" +) + +type cmdRuleEngineRuleUpdateScript struct { + cmdRuleEngineRule + ruleID string + scriptPath string + runtimeVersion string + execInterval time.Duration +} + +func buildCmdRuleEngineRuleUpdateScript() *cli.Command { + cmd := &cmdRuleEngineRuleUpdateScript{} + return &cli.Command{ + Name: "update-script", + Usage: "Update the script of a rule", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineRuleUpdateScript) Flags() []cli.Flag { + return append(c.cmdRuleEngineRule.Flags(), + &cli.StringFlag{ + Name: "rule-id", + Usage: "Rule ID or slug to update", + Destination: &c.ruleID, + Required: true, + }, + &cli.StringFlag{ + Name: "script", + Usage: "Path to a file containing the script code", + Destination: &c.scriptPath, + Required: true, + }, + &cli.StringFlag{ + Name: "runtime-version", + Usage: "Version of the runtime to use for the script execution", + Destination: &c.runtimeVersion, + Value: ruleRuntimeV3, + Action: func(_ *cli.Context, v string) error { + return c.validateRuntimeVersion(v) + }, + }, + &cliflags.Duration{ + DurationFlag: cli.DurationFlag{ + Name: "exec-interval", + Usage: "How frequently to execute the script " + + "(compatible only with runtime version 1) in duration format (e.g., 5s, 2m)", + Destination: &c.execInterval, + }, + }, + ) +} + +func (c *cmdRuleEngineRuleUpdateScript) do(ctx context.Context) error { + if c.scriptPath == "-" { + c.scriptPath = "/dev/stdin" + } + scriptBytes, err := os.ReadFile(c.scriptPath) + if err != nil { + return fmt.Errorf("read script file: %w", err) + } + + body, err := json.Marshal(map[string]any{ + "script": map[string]any{ + "code": base64.StdEncoding.EncodeToString(scriptBytes), + "runtime_version": c.runtimeVersion, + "exec_interval": c.execInterval.String(), + }, + }) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/" + c.ruleID + "/update_script", + Body: bytes.NewReader(body), + ContentType: contentTypeJSON, + }) +} diff --git a/internal/app/enaptercli/cmd_rule_engine_suspend.go b/internal/app/enaptercli/cmd_rule_engine_suspend.go new file mode 100644 index 0000000..5637e48 --- /dev/null +++ b/internal/app/enaptercli/cmd_rule_engine_suspend.go @@ -0,0 +1,33 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdRuleEngineSuspend struct { + cmdRuleEngine +} + +func buildCmdRuleEngineSuspend() *cli.Command { + cmd := &cmdRuleEngineSuspend{} + return &cli.Command{ + Name: "suspend", + Usage: "Suspend execution of rules", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdRuleEngineSuspend) do(ctx context.Context) error { + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodPost, + Path: "/suspend", + }) +} diff --git a/internal/app/enaptercli/cmd_rules.go b/internal/app/enaptercli/cmd_rules.go deleted file mode 100644 index c6a465e..0000000 --- a/internal/app/enaptercli/cmd_rules.go +++ /dev/null @@ -1,30 +0,0 @@ -package enaptercli - -import "github.com/urfave/cli/v2" - -type cmdRules struct { - cmdBase - ruleID string -} - -func buildCmdRules() *cli.Command { - return &cli.Command{ - Name: "rules", - Usage: "Rules information and management commands.", - Subcommands: []*cli.Command{ - buildCmdRulesUpdate(), - buildCmdRulesLogs(), - }, - } -} - -func (c *cmdRules) Flags() []cli.Flag { - flags := c.cmdBase.Flags() - flags = append(flags, &cli.StringFlag{ - Name: "rule-id", - Usage: "Rule ID; can be obtained in cloud.enapter.com", - Required: true, - Destination: &c.ruleID, - }) - return flags -} diff --git a/internal/app/enaptercli/cmd_rules_logs.go b/internal/app/enaptercli/cmd_rules_logs.go deleted file mode 100644 index a02059e..0000000 --- a/internal/app/enaptercli/cmd_rules_logs.go +++ /dev/null @@ -1,43 +0,0 @@ -package enaptercli - -import ( - "context" - "fmt" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/cloudapi" -) - -type cmdRulesLogs struct { - cmdRules -} - -func buildCmdRulesLogs() *cli.Command { - cmd := &cmdRulesLogs{} - - return &cli.Command{ - Name: "logs", - Usage: "Stream logs from a rule", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.run(cliCtx.Context, cliCtx.App.Version) - }, - } -} - -func (c *cmdRulesLogs) run(ctx context.Context, version string) error { - writer := func(topic, msg string) { - fmt.Fprintf(c.writer, "[%s] %s\n", topic, msg) - } - - streamer, err := cloudapi.NewRuleLogsWriter(c.websocketsURL, c.token, - version, c.ruleID, writer) - if err != nil { - return fmt.Errorf("create streamer: %w", err) - } - - return streamer.Run(ctx) -} diff --git a/internal/app/enaptercli/cmd_rules_logs_test.go b/internal/app/enaptercli/cmd_rules_logs_test.go deleted file mode 100644 index 41e9339..0000000 --- a/internal/app/enaptercli/cmd_rules_logs_test.go +++ /dev/null @@ -1,41 +0,0 @@ -//nolint:dupl // not a duplicate of `devices logs` command tests -package enaptercli_test - -import ( - "strings" - "testing" -) - -func TestRuleLogs(t *testing.T) { - t.Run("simple", func(t *testing.T) { - inputFileName := "testdata/rules_logs/simple/input" - untilLinePrefix := "[info]" - expectedFileName := "testdata/rules_logs/simple/output" - testRuleLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) - - t.Run("invalid token", func(t *testing.T) { - inputFileName := "testdata/rules_logs/disconnect/invalid_token/input" - untilLinePrefix := "[connection]" - expectedFileName := "testdata/rules_logs/disconnect/invalid_token/output" - testRuleLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) - - t.Run("rule not found", func(t *testing.T) { - inputFileName := "testdata/rules_logs/disconnect/rule_not_found/input" - untilLinePrefix := "[connection] disconnected" - expectedFileName := "testdata/rules_logs/disconnect/rule_not_found/output" - testRuleLogs(t, inputFileName, untilLinePrefix, expectedFileName) - }) -} - -func testRuleLogs(t *testing.T, inputFileName, untilLinePrefix, expectedFileName string) { - const hardwareID = "SIM-RULE" - - identifier := map[string]string{"channel": "RuleChannel", "rule_id": hardwareID} - - command := strings.Split("enapter rules logs", " ") - command = append(command, "--rule-id", hardwareID) - - testLogsCommand(t, inputFileName, untilLinePrefix, expectedFileName, identifier, command) -} diff --git a/internal/app/enaptercli/cmd_rules_update.go b/internal/app/enaptercli/cmd_rules_update.go deleted file mode 100644 index b72a705..0000000 --- a/internal/app/enaptercli/cmd_rules_update.go +++ /dev/null @@ -1,107 +0,0 @@ -package enaptercli - -import ( - "context" - "fmt" - "net/http" - "os" - "time" - - "github.com/urfave/cli/v2" - - "github.com/enapter/enapter-cli/internal/cloudapi" -) - -const ruleUpdateDefaultTimeout = 30 * time.Second - -type cmdRulesUpdate struct { - cmdRules - path string - executionInterval int - stdlibVersion string - timeout time.Duration -} - -func buildCmdRulesUpdate() *cli.Command { - cmd := &cmdRulesUpdate{} - - return &cli.Command{ - Name: "update", - Usage: "Update rule.", - CustomHelpTemplate: cmd.HelpTemplate(), - Flags: cmd.Flags(), - Before: cmd.Before, - Action: func(cliCtx *cli.Context) error { - return cmd.run(cliCtx.Context, cliCtx.App.Version) - }, - } -} - -func (c *cmdRulesUpdate) Flags() []cli.Flag { - flags := c.cmdRules.Flags() - flags = append(flags, - &cli.StringFlag{ - Name: "rule-path", - Usage: "Path to file with rule Lua code", - Destination: &c.path, - }, - &cli.IntFlag{ - Name: "execution-interval", - Usage: "Rule execution interval in milliseconds", - DefaultText: "chosen by the server", - Destination: &c.executionInterval, - }, - &cli.StringFlag{ - Name: "stdlib-version", - Usage: "Version of standard library used by the rule", - DefaultText: "chosen by the server", - Destination: &c.stdlibVersion, - }, - &cli.DurationFlag{ - Name: "timeout", - Usage: "Time to wait for rule update", - Destination: &c.timeout, - Value: ruleUpdateDefaultTimeout, - }, - ) - return flags -} - -func (c *cmdRulesUpdate) run(ctx context.Context, version string) error { - if c.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.timeout) - defer cancel() - } - - luaCode, err := os.ReadFile(c.path) - if err != nil { - return fmt.Errorf("read rule file: %w", err) - } - - transport := cloudapi.NewCredentialsTransport(http.DefaultTransport, c.token, version) - transport = cloudapi.NewCLIMessageWriterTransport(transport, &onceWriter{w: c.writer}) - client := cloudapi.NewClientWithURL(&http.Client{Transport: transport}, c.graphqlURL) - - input := cloudapi.UpdateRuleInput{ - RuleID: c.ruleID, - LuaCode: string(luaCode), - StdlibVersion: c.stdlibVersion, - ExecutionInterval: c.executionInterval, - } - - updateData, updateErrors, err := client.UpdateRule(ctx, input) - if err != nil { - return fmt.Errorf("do update: %w", err) - } - - if len(updateErrors) != 0 { - for _, e := range updateErrors { - fmt.Fprintln(c.writer, "[ERROR]", e.Message) - } - return errFinishedWithError - } - - fmt.Fprintln(c.writer, updateData.Message) - return nil -} diff --git a/internal/app/enaptercli/cmd_rules_update_test.go b/internal/app/enaptercli/cmd_rules_update_test.go deleted file mode 100644 index 0a7ee9c..0000000 --- a/internal/app/enaptercli/cmd_rules_update_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package enaptercli_test - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/bxcodec/faker/v3" - "github.com/stretchr/testify/require" -) - -func TestRulesUpdate(t *testing.T) { - testdataDir := "testdata/rules_update" - dirs, err := os.ReadDir(testdataDir) - require.NoError(t, err) - - for _, dir := range dirs { - if !dir.IsDir() { - continue - } - - dir := dir - t.Run(dir.Name(), func(t *testing.T) { - testRulesUpdate(t, filepath.Join(testdataDir, dir.Name())) - }) - } -} - -func TestRulesUpdateWrongFilePath(t *testing.T) { - args := strings.Split("enapter rules update --token token --rule-id ruleID "+ - "--gql-api-url apiURL --rule-path wrong", " ") - app := startTestApp(args...) - defer app.Stop() - - appErr := app.Wait() - require.EqualError(t, appErr, "read rule file: open wrong: no such file or directory") -} - -type rulesUpdateTestSettings struct { - RuleID string `json:"rule_id"` - RulePath string `json:"rule_path"` - Token string `json:"-"` -} - -func (s *rulesUpdateTestSettings) Fill(t *testing.T, dir string) { - settingsBytes, err := os.ReadFile(filepath.Join(dir, "settings.json")) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(settingsBytes, s)) - - s.RulePath = filepath.Join(dir, s.RulePath) - s.Token = faker.Word() -} - -func testRulesUpdate(t *testing.T, dir string) { - var settings rulesUpdateTestSettings - settings.Fill(t, dir) - - reqs := byteSliceSliceFromFile(t, filepath.Join(dir, "requests")) - resps := byteSliceSliceFromFile(t, filepath.Join(dir, "responses")) - - srv := startTestServer(reqs, resps, "") - defer srv.Close() - - args := strings.Split("enapter rules update", " ") - args = append(args, - "--token", settings.Token, - "--rule-id", settings.RuleID, - "--rule-path", settings.RulePath, - "--gql-api-url", srv.URL) - - checkTestAppOutput(t, dir, args, reqs) -} diff --git a/internal/app/enaptercli/cmd_site.go b/internal/app/enaptercli/cmd_site.go new file mode 100644 index 0000000..75fc336 --- /dev/null +++ b/internal/app/enaptercli/cmd_site.go @@ -0,0 +1,35 @@ +package enaptercli + +import ( + "context" + "fmt" + "net/url" + + "github.com/urfave/cli/v2" +) + +type cmdSite struct { + cmdBase +} + +func buildCmdSite() *cli.Command { + cmd := &cmdSite{} + return &cli.Command{ + Name: "site", + Usage: "Manage sites", + CustomHelpTemplate: cmd.SubcommandHelpTemplate(), + Subcommands: []*cli.Command{ + buildCmdSiteList(), + buildCmdSiteGet(), + }, + } +} + +func (c *cmdSite) doHTTPRequest(ctx context.Context, p doHTTPRequestParams) error { + path, err := url.JoinPath("/sites", p.Path) + if err != nil { + return fmt.Errorf("join path: %w", err) + } + p.Path = path + return c.cmdBase.doHTTPRequest(ctx, p) +} diff --git a/internal/app/enaptercli/cmd_site_get.go b/internal/app/enaptercli/cmd_site_get.go new file mode 100644 index 0000000..929904c --- /dev/null +++ b/internal/app/enaptercli/cmd_site_get.go @@ -0,0 +1,47 @@ +package enaptercli + +import ( + "context" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdSiteGet struct { + cmdSite + siteID string +} + +func buildCmdSiteGet() *cli.Command { + cmd := &cmdSiteGet{} + return &cli.Command{ + Name: "get", + Usage: "Get a site", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdSiteGet) Flags() []cli.Flag { + flags := c.cmdSite.Flags() + return append(flags, &cli.StringFlag{ + Name: "site-id", + Usage: "site ID", + Destination: &c.siteID, + }) +} + +func (c *cmdSiteGet) do(ctx context.Context) error { + siteID, err := c.chooseSiteID(c.siteID) + if err != nil { + return err + } + return c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + siteID, + }) +} diff --git a/internal/app/enaptercli/cmd_site_list.go b/internal/app/enaptercli/cmd_site_list.go new file mode 100644 index 0000000..c43854f --- /dev/null +++ b/internal/app/enaptercli/cmd_site_list.go @@ -0,0 +1,88 @@ +package enaptercli + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/urfave/cli/v2" +) + +type cmdSiteList struct { + cmdSite + mySites bool + limit int +} + +func buildCmdSiteList() *cli.Command { + cmd := &cmdSiteList{} + return &cli.Command{ + Name: "list", + Usage: "List user sites", + CustomHelpTemplate: cmd.CommandHelpTemplate(), + Flags: cmd.Flags(), + Before: cmd.Before, + Action: func(cliCtx *cli.Context) error { + return cmd.do(cliCtx.Context) + }, + } +} + +func (c *cmdSiteList) Flags() []cli.Flag { + flags := c.cmdSite.Flags() + return append(flags, &cli.BoolFlag{ + Name: "my-sites", + Usage: "returns only sites where user is owner or installer", + Destination: &c.mySites, + }, &cli.IntFlag{ + Name: "limit", + Usage: "maximum number of sites to retrieve", + Destination: &c.limit, + DefaultText: "retrieves all", + }) +} + +func (c *cmdSiteList) do(ctx context.Context) error { + if siteID, _ := c.chooseSiteID(""); siteID != "" { + fmt.Fprintln(c.errWriter, "WARNING: trying to get sites list when site ID "+ + "is set for current connection, result will contain only one site.") + + var resp struct { + Site json.RawMessage `json:"site"` + } + if err := c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "/" + c.siteID, + RespProcessor: func(r *http.Response) error { + return json.NewDecoder(r.Body).Decode(&resp) + }, + }); err != nil { + return err + } + + return json.NewEncoder(c.writer).Encode(struct { + Sites []json.RawMessage `json:"sites"` + TotalCount int `json:"total_count"` + }{ + Sites: []json.RawMessage{resp.Site}, + TotalCount: 1, + }) + } + + doPaginateRequestParams := paginateHTTPRequestParams{ + ObjectName: "sites", + Limit: c.limit, + DoFn: c.doHTTPRequest, + BaseParams: doHTTPRequestParams{ + Method: http.MethodGet, + Path: "", + }, + } + + if c.mySites { + doPaginateRequestParams.BaseParams.Path = "/users/me/sites" + } + + return c.doPaginateRequest(ctx, doPaginateRequestParams) +} diff --git a/internal/app/enaptercli/cmd_test.go b/internal/app/enaptercli/cmd_test.go deleted file mode 100644 index fde7025..0000000 --- a/internal/app/enaptercli/cmd_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package enaptercli_test - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -type byteSliceSlice struct { - lines [][]byte -} - -func byteSliceSliceFromFile(t *testing.T, path string) *byteSliceSlice { - f, err := os.ReadFile(path) - require.NoError(t, err) - lines := bytes.Split(f, []byte{'\n'}) - - n := 0 - for _, line := range lines { - if len(line) > 0 { - lines[n] = line - n++ - } - } - - return &byteSliceSlice{lines: lines[:n]} -} - -func (b *byteSliceSlice) Next() []byte { - for i, s := range b.lines { - b.lines = b.lines[i+1:] - return s - } - return nil -} - -func (b *byteSliceSlice) Append(d []byte) { - b.lines = append(b.lines, d) -} - -func (b *byteSliceSlice) Buffer() [][]byte { - return b.lines -} - -func (b *byteSliceSlice) Clear() { - b.lines = nil -} - -func startTestServer(reqs, resps *byteSliceSlice, cliMessage string) *httptest.Server { - if update { - reqs.Clear() - } - - handler := func(w http.ResponseWriter, r *http.Request) { - resp := resps.Next() - if resp == nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("to much requests for test (not enough responses)")) - return - } - - var req []byte - if !update { - req = reqs.Next() - if len(req) == 0 { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("to much requests for test (not enough requests)")) - return - } - } - - if cliMessage != "" { - w.Header().Set("X-ENAPTER-CLI-MESSAGE", cliMessage) - } - - reqBody, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("failed to read request")) - return - } - - if update { - reqs.Append(reqBody) - } else { - reqBody := bytes.TrimRight(reqBody, "\n") - if !bytes.Equal(reqBody, req) { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("unexpected request\nActual\n")) - _, _ = w.Write(reqBody) - _, _ = w.Write([]byte("\nExpected\n")) - _, _ = w.Write(req) - return - } - } - - _, _ = w.Write(resp) - } - - return httptest.NewServer(http.HandlerFunc(handler)) -} - -func checkTestAppOutput(t *testing.T, basePath string, args []string, requests *byteSliceSlice) { - app := startTestApp(args...) - defer app.Stop() - - appErr := app.Wait() - - actual, err := io.ReadAll(app.Stdout()) - require.NoError(t, err) - - if appErr != nil { - actual = append(actual, []byte("app exit with error: "+appErr.Error()+"\n")...) - } - - expectedFileName := filepath.Join(basePath, "output") - if update { - err := os.WriteFile(expectedFileName, actual, 0o600) - require.NoError(t, err) - - requestsFileName := filepath.Join(basePath, "requests") - requestsBytes := bytes.Join(requests.Buffer(), []byte{'\n'}) - err = os.WriteFile(requestsFileName, requestsBytes, 0o600) - require.NoError(t, err) - } - - expected, err := os.ReadFile(expectedFileName) - require.NoError(t, err) - - require.Equal(t, string(expected), string(actual)) -} diff --git a/internal/app/enaptercli/content_types.go b/internal/app/enaptercli/content_types.go new file mode 100644 index 0000000..5431e71 --- /dev/null +++ b/internal/app/enaptercli/content_types.go @@ -0,0 +1,5 @@ +package enaptercli + +const ( + contentTypeJSON = "application/json" +) diff --git a/internal/app/enaptercli/errors.go b/internal/app/enaptercli/errors.go index e2d3fb3..44d17b2 100644 --- a/internal/app/enaptercli/errors.go +++ b/internal/app/enaptercli/errors.go @@ -3,7 +3,9 @@ package enaptercli import "errors" var ( - errFinishedWithError = errors.New("request execution failed") - errAPITokenMissed = errors.New("API token missing. Set it up using environment " + - "variable ENAPTER_API_TOKEN") + errSiteIDMismatch = errors.New("passed site-ID must match the site-ID of the current connection") + errSiteIDMissing = errors.New("site ID is required") + errUnsupportedFlagValue = errors.New("unsupported flag value") + errOnlyOneBlueprinFlag = errors.New("only one of --blueprint-id or --blueprint-path can be specified") + errMissedBlueprintFlag = errors.New("one of --blueprint-id or --blueprint-path must be specified") ) diff --git a/internal/app/enaptercli/execute.go b/internal/app/enaptercli/execute.go index 09df97a..8010ce4 100644 --- a/internal/app/enaptercli/execute.go +++ b/internal/app/enaptercli/execute.go @@ -3,10 +3,10 @@ package enaptercli import ( "archive/zip" "bytes" + "fmt" "io" + "io/fs" "os" - "path/filepath" - "strings" "github.com/urfave/cli/v2" ) @@ -15,55 +15,59 @@ import ( func NewApp() *cli.App { app := cli.NewApp() - app.Usage = "Command line interface for Enapter services." - app.Description = "Enapter CLI requires access token for authentication. " + - "The token can be obtained in your Enapter Cloud account settings.\n\n" + - "Configure API token using ENAPTER_API_TOKEN environment variable or using --token global option." + app.Usage = "Command Line Interface (CLI) for Enapter services." + app.Description = "The Enapter CLI requires an access token for authentication. " + + "You can obtain the token in your Enapter Cloud account settings." + app.CustomAppHelpTemplate = cli.AppHelpTemplate + enapterAPIEnvVarsHelp app.Commands = []*cli.Command{ - buildCmdDevices(), - buildCmdRules(), + buildCmdSite(), + buildCmdDevice(), + buildCmdBlueprint(), + buildCmdRuleEngine(), + buildCmdConnection(), } return app } func zipDir(path string) ([]byte, error) { - buf := &bytes.Buffer{} - myZip := zip.NewWriter(buf) + fsys := os.DirFS(path) - path = strings.TrimPrefix(path, "./") + buf := &bytes.Buffer{} + zw := zip.NewWriter(buf) - err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error { + err := fs.WalkDir(fsys, ".", func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } - if info.IsDir() { + if entry.IsDir() { return nil } - relPath := strings.TrimPrefix(filePath, path) - relPath = strings.TrimPrefix(relPath, "/") - zipFile, err := myZip.Create(relPath) + + f, err := fsys.Open(path) if err != nil { - return err + return fmt.Errorf("open: %w", err) } - fsFile, err := os.Open(filePath) + defer f.Close() + + zf, err := zw.Create(path) if err != nil { - return err + return fmt.Errorf("create: %w", err) } - _, err = io.Copy(zipFile, fsFile) - if err != nil { - return err + + if _, err = io.Copy(zf, f); err != nil { + return fmt.Errorf("copy: %w", err) } return nil }) if err != nil { - return nil, err + return nil, fmt.Errorf("walk dir: %w", err) } - if err := myZip.Close(); err != nil { - return nil, err + if err := zw.Close(); err != nil { + return nil, fmt.Errorf("close zip: %w", err) } - return buf.Bytes(), err + return buf.Bytes(), nil } diff --git a/internal/app/enaptercli/execute_test.go b/internal/app/enaptercli/execute_test.go index 0feda1e..9227850 100644 --- a/internal/app/enaptercli/execute_test.go +++ b/internal/app/enaptercli/execute_test.go @@ -1,28 +1,35 @@ package enaptercli_test import ( + "bytes" + "encoding/json" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" + "strconv" "strings" "testing" + "text/template" "github.com/stretchr/testify/require" ) +const testToken = "enapter_api_test_token" + func TestHelpMessages(t *testing.T) { files, err := os.ReadDir("testdata/helps") require.NoError(t, err) for _, fi := range files { - fi := fi t.Run(fi.Name(), func(t *testing.T) { args := strings.Split(fi.Name(), " ") args = append(args, "-h") app := startTestApp(args...) appErr := app.Wait() - actual, err := io.ReadAll(app.Stdout()) + actual, err := io.ReadAll(app.Output()) require.NoError(t, err) if appErr != nil { @@ -33,12 +40,148 @@ func TestHelpMessages(t *testing.T) { if update { err := os.WriteFile(exepctedFileName, actual, 0o600) require.NoError(t, err) + } else { + require.Equal(t, readFileToString(t, exepctedFileName), string(actual)) } + }) + } +} - expected, err := os.ReadFile(exepctedFileName) - require.NoError(t, err) +func TestHTTPReqResp(t *testing.T) { + const testdataPath = "testdata/http_req_resp" + tests, err := os.ReadDir(testdataPath) + require.NoError(t, err) - require.Equal(t, string(expected), string(actual)) + for _, tc := range tests { + t.Run(tc.Name(), func(t *testing.T) { + path := filepath.Join(testdataPath, tc.Name()) + testExecute(t, path) }) } } + +func testExecute(t *testing.T, path string) { + srv := newTestServer(t, path) + + cmd := executeTmpl(t, filepath.Join(path, "cmd.tmpl"), struct { + Token string + URL string + }{ + Token: testToken, + URL: srv.URL, + }) + + t.Setenv("ENAPTER3_CONFIG", t.TempDir()) + output := executeCommands(t, cmd) + + exepctedOutFileName := filepath.Join(path, "out") + if update { + err := os.WriteFile(exepctedOutFileName, output, 0o600) + require.NoError(t, err) + } else { + expected := readFileToString(t, exepctedOutFileName) + require.Equal(t, expected, string(output)) + } +} + +func newTestServer(t *testing.T, path string) *httptest.Server { + t.Helper() + + reqCount := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqPath := filepath.Join(path, "req_"+strconv.Itoa(reqCount)) + respPath := filepath.Join(path, "resp_"+strconv.Itoa(reqCount)) + + info := struct { + Method string + URL string + Header http.Header + Body string + }{ + Method: r.Method, + URL: r.URL.String(), + Header: r.Header, + Body: readBodyAsString(t, r.Body), + } + if update { + err := os.WriteFile(reqPath, shouldMarshalIndent(t, info), 0o600) + require.NoError(t, err) + } else { + expected := readFileToString(t, reqPath) + actual := string(shouldMarshalIndent(t, info)) + require.Equal(t, expected, actual) + } + + resp := shouldReadFile(t, respPath) + _, _ = w.Write(resp) + + reqCount++ + })) + t.Cleanup(func() { srv.Close() }) + + return srv +} + +func executeCommands(t *testing.T, cmd string) []byte { + t.Helper() + + var output []byte + for cmd := range strings.Lines(cmd) { + cmd := strings.Trim(cmd, "\n") + args := strings.Split(cmd, " ") + + app := startTestApp(args...) + appErr := app.Wait() + + out, err := io.ReadAll(app.Output()) + require.NoError(t, err) + + output = append(output, out...) + if appErr != nil { + output = append(output, []byte("app exit with error: "+appErr.Error()+"\n")...) + break + } + } + return output +} + +func executeTmpl(t *testing.T, tmplFilePath string, tmplParams interface{}) string { + t.Helper() + tmplData := readFileToString(t, tmplFilePath) + tmplData = strings.TrimRight(tmplData, " \n\t") + + tmpl := template.New(tmplFilePath) + tmpl, err := tmpl.Parse(tmplData) + require.NoError(t, err) + + out := &bytes.Buffer{} + require.NoError(t, tmpl.Execute(out, tmplParams)) + + return out.String() +} + +func readFileToString(t *testing.T, path string) string { + t.Helper() + return string(shouldReadFile(t, path)) +} + +func shouldReadFile(t *testing.T, path string) []byte { + t.Helper() + d, err := os.ReadFile(path) + require.NoError(t, err) + return d +} + +func readBodyAsString(t *testing.T, r io.Reader) string { + t.Helper() + d, err := io.ReadAll(r) + require.NoError(t, err) + return string(d) +} + +func shouldMarshalIndent(t *testing.T, v interface{}) []byte { + t.Helper() + d, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + return d +} diff --git a/internal/app/enaptercli/once_writer.go b/internal/app/enaptercli/once_writer.go deleted file mode 100644 index 4ae3a3e..0000000 --- a/internal/app/enaptercli/once_writer.go +++ /dev/null @@ -1,20 +0,0 @@ -package enaptercli - -import ( - "io" - "sync" -) - -type onceWriter struct { - once sync.Once - w io.Writer -} - -func (w *onceWriter) Write(p []byte) (int, error) { - n := len(p) - var err error - w.once.Do(func() { - n, err = w.w.Write(p) - }) - return n, err -} diff --git a/internal/app/enaptercli/testdata/blueprints/bp.zip b/internal/app/enaptercli/testdata/blueprints/bp.zip new file mode 100644 index 0000000..5150cb7 --- /dev/null +++ b/internal/app/enaptercli/testdata/blueprints/bp.zip @@ -0,0 +1 @@ +blueprint.zip diff --git a/internal/app/enaptercli/testdata/blueprints/simple/firmware.lua b/internal/app/enaptercli/testdata/blueprints/simple/firmware.lua new file mode 100644 index 0000000..aa6fd65 --- /dev/null +++ b/internal/app/enaptercli/testdata/blueprints/simple/firmware.lua @@ -0,0 +1 @@ +enapter.log("Hello from firmware.lua") diff --git a/internal/app/enaptercli/testdata/blueprints/simple/manifest.yml b/internal/app/enaptercli/testdata/blueprints/simple/manifest.yml new file mode 100644 index 0000000..fc8f08a --- /dev/null +++ b/internal/app/enaptercli/testdata/blueprints/simple/manifest.yml @@ -0,0 +1,7 @@ +blueprint_spec: device/3.0 +display_name: Simple Lua + +runtime: + type: lua + options: + file: firmware.lua diff --git a/internal/app/enaptercli/testdata/device_execute/error/output b/internal/app/enaptercli/testdata/device_execute/error/output deleted file mode 100644 index a0772af..0000000 --- a/internal/app/enaptercli/testdata/device_execute/error/output +++ /dev/null @@ -1 +0,0 @@ -app exit with error: forbidden: Access denied. diff --git a/internal/app/enaptercli/testdata/device_execute/error/responses b/internal/app/enaptercli/testdata/device_execute/error/responses deleted file mode 100644 index c1bed99..0000000 --- a/internal/app/enaptercli/testdata/device_execute/error/responses +++ /dev/null @@ -1 +0,0 @@ -{"errors":[{"code":"forbidden","message":"Access denied."}]} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/device_execute/progress/output b/internal/app/enaptercli/testdata/device_execute/progress/output deleted file mode 100644 index a267617..0000000 --- a/internal/app/enaptercli/testdata/device_execute/progress/output +++ /dev/null @@ -1,3 +0,0 @@ -{"state":"started"} -{"state":"device_in_progress","payload":{"progress":50}} -{"state":"succeeded"} diff --git a/internal/app/enaptercli/testdata/device_execute/progress/responses b/internal/app/enaptercli/testdata/device_execute/progress/responses deleted file mode 100644 index dbb2c43..0000000 --- a/internal/app/enaptercli/testdata/device_execute/progress/responses +++ /dev/null @@ -1,3 +0,0 @@ -{"state":"started"} -{"state":"device_in_progress","payload":{"progress":50}} -{"state":"succeeded"} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/device_execute/simple/output b/internal/app/enaptercli/testdata/device_execute/simple/output deleted file mode 100644 index 7d416b3..0000000 --- a/internal/app/enaptercli/testdata/device_execute/simple/output +++ /dev/null @@ -1 +0,0 @@ -{"state":"started"} diff --git a/internal/app/enaptercli/testdata/device_execute/simple/responses b/internal/app/enaptercli/testdata/device_execute/simple/responses deleted file mode 100644 index 78d59d2..0000000 --- a/internal/app/enaptercli/testdata/device_execute/simple/responses +++ /dev/null @@ -1,2 +0,0 @@ -{"state":"started"} -{"state":"succeeded"} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/input b/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/input deleted file mode 100644 index 20dbe46..0000000 --- a/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/input +++ /dev/null @@ -1,5 +0,0 @@ -{"type":"welcome"} - -{"identifier":"{\"channel\":\"DeviceChannel\",\"hardware_id\":\"SIM-WTM\"}","type":"message","message":{"topic":"error","payload":"Device not found"}} - -{"identifier":"{\"channel\":\"DeviceChannel\",\"hardware_id\":\"SIM-WTM\"}","type":"reject_subscription"} diff --git a/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/output b/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/output deleted file mode 100644 index cae285f..0000000 --- a/internal/app/enaptercli/testdata/device_logs/disconnect/device_not_found/output +++ /dev/null @@ -1,3 +0,0 @@ -[connection] welcome -[error] Device not found -[connection] disconnected diff --git a/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/input b/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/input deleted file mode 100644 index 37f6889..0000000 --- a/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/input +++ /dev/null @@ -1 +0,0 @@ -{"type":"disconnect","reason":"unauthorized","reconnect":false} diff --git a/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/output b/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/output deleted file mode 100644 index 1c44604..0000000 --- a/internal/app/enaptercli/testdata/device_logs/disconnect/invalid_token/output +++ /dev/null @@ -1 +0,0 @@ -[connection] disconnected with reason: unauthorized diff --git a/internal/app/enaptercli/testdata/device_logs/simple/input b/internal/app/enaptercli/testdata/device_logs/simple/input deleted file mode 100644 index 63189d3..0000000 --- a/internal/app/enaptercli/testdata/device_logs/simple/input +++ /dev/null @@ -1,16 +0,0 @@ -{"type":"welcome"} -{"identifier":"{\"channel\":\"DeviceChannel\",\"hardware_id\":\"SIM-WTM\"}","type":"confirm_subscription"} - -{"type":"message","identifier":"{\"channel\":\"DeviceChannel\",\"hardware_id\":\"SIM-WTM\"}","message":{"topic":"register","payload":"{\"timestamp\":1606485742,\"fw_ver\":\"1.0.0\",\"efuse\":\"0x1C04\",\"product_revision\":\"WTM21 REV1\",\"vendor\":\"Enapter\",\"model\":\"WTM\"}"}} - -{"type":"ping","message":"1606485743"} - -{"type":"message","identifier":"{\"channel\":\"DeviceChannel\",\"hardware_id\":\"SIM-FRANK\"}","message":{"topic":"telemetry","payload":"{\"timestamp\":1606485747,\"status\":\"ok\",\"PUMP_out_power\":false,\"SV01_out_open\":false,\"LSH_in\":false,\"LSL_in\":false,\"WPS01_in\":false,\"BUTTON_in\":false,\"TT01_in_c\":2.0,\"TT02_in_c\":1.0,\"CS01_in_v\":4.0,\"CS01_in_a\":3.0,\"CT01_in_v\":6.0,\"CT01_in_uscm\":5.0,\"CT01_in_uscm_comp\":9.0,\"LT01_in_v\":8.0,\"LT01_in_l\":7.0,\"last_calibration\":1611756147}"}} - -{"type":"ping","message":"1606485746"} - -{"type":"message","identifier":"{\"channel\":\"OtherChannel\",\"hardware_id\":\"SIM-WTM\"}","message":{"topic":"telemetry","payload":"{\"timestamp\":1606485747,\"status\":\"ok\",\"PUMP_out_power\":false,\"SV01_out_open\":false,\"LSH_in\":false,\"LSL_in\":false,\"WPS01_in\":false,\"BUTTON_in\":false,\"TT01_in_c\":2.0,\"TT02_in_c\":1.0,\"CS01_in_v\":4.0,\"CS01_in_a\":3.0,\"CT01_in_v\":6.0,\"CT01_in_uscm\":5.0,\"CT01_in_uscm_comp\":9.0,\"LT01_in_v\":8.0,\"LT01_in_l\":7.0,\"last_calibration\":1611756147}"}} - -{"type":"ping","message":"1606485749"} - -{"type":"message","identifier":"{\"hardware_id\":\"SIM-WTM\",\"channel\":\"DeviceChannel\"}","message":{"topic":"telemetry","payload":"{\"timestamp\":1606486092,\"status\":\"ok\",\"PUMP_out_power\":false,\"SV01_out_open\":false,\"LSH_in\":false,\"LSL_in\":false,\"WPS01_in\":false,\"BUTTON_in\":false,\"TT01_in_c\":1.0,\"TT02_in_c\":2.0,\"CS01_in_v\":3.0,\"CS01_in_a\":4.0,\"CT01_in_v\":5.0,\"CT01_in_uscm\":6.0,\"CT01_in_uscm_comp\":10.0,\"LT01_in_v\":7.0,\"LT01_in_l\":8.0,\"last_calibration\":1611756492}"}} diff --git a/internal/app/enaptercli/testdata/device_logs/simple/output b/internal/app/enaptercli/testdata/device_logs/simple/output deleted file mode 100644 index 3277fb5..0000000 --- a/internal/app/enaptercli/testdata/device_logs/simple/output +++ /dev/null @@ -1,6 +0,0 @@ -[connection] welcome -[connection] confirm_subscription -[register] {"timestamp":1606485742,"fw_ver":"1.0.0","efuse":"0x1C04","product_revision":"WTM21 REV1","vendor":"Enapter","model":"WTM"} -[read_error] skip message with unknown identifier map[channel:DeviceChannel hardware_id:SIM-FRANK] -[read_error] skip message with unknown identifier map[channel:OtherChannel hardware_id:SIM-WTM] -[telemetry] {"timestamp":1606486092,"status":"ok","PUMP_out_power":false,"SV01_out_open":false,"LSH_in":false,"LSL_in":false,"WPS01_in":false,"BUTTON_in":false,"TT01_in_c":1.0,"TT02_in_c":2.0,"CS01_in_v":3.0,"CS01_in_a":4.0,"CT01_in_v":5.0,"CT01_in_uscm":6.0,"CT01_in_uscm_comp":10.0,"LT01_in_v":7.0,"LT01_in_l":8.0,"last_calibration":1611756492} diff --git a/internal/app/enaptercli/testdata/device_upload/cli_message/output b/internal/app/enaptercli/testdata/device_upload/cli_message/output deleted file mode 100644 index 1b92681..0000000 --- a/internal/app/enaptercli/testdata/device_upload/cli_message/output +++ /dev/null @@ -1,9 +0,0 @@ -Blueprint files to be uploaded: -* testdata/device_upload/simple/blueprint/manifest.yml -VERSION IS OUTDATED -upload started with operation id 25 -[#25] 2020-12-09T14:02:07Z [INFO] Started uploading blueprint[id=d428e77c-3081-4873-b343-2f8f96d9cadc] on device[hardware_id=SIM-WTM] -[#25] 2020-12-09T14:02:07Z [INFO] Generating configuration for uploading -[#25] 2020-12-09T14:02:07Z [INFO] Updating configuration on the platform -[#25] 2020-12-09T14:02:07Z [INFO] Uploading blueprint finished successfully -Done! diff --git a/internal/app/enaptercli/testdata/device_upload/cli_message/requests b/internal/app/enaptercli/testdata/device_upload/cli_message/requests deleted file mode 100644 index 8efcf61..0000000 --- a/internal/app/enaptercli/testdata/device_upload/cli_message/requests +++ /dev/null @@ -1,7 +0,0 @@ -{"query":"mutation($input:UploadBlueprintInput!){device{uploadBlueprint(input: $input){data{code,message,title,operationId},errors{code,message,path,title}}}}","variables":{"input":{"blueprint":"data:application/gzip;base64,UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAAMAAAAbWFuaWZlc3QueW1sAQAA//9QSwcIAAAAAAUAAAAAAAAAUEsBAhQAFAAIAAgAAAAAAAAAAAAFAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAG1hbmlmZXN0LnltbFBLBQYAAAAAAQABADoAAAA/AAAAAAA=","hardwareId":"SIM-WTM"}}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"25"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"MQ","hardware_id":"SIM-WTM","operation_id":"25"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"25"}} diff --git a/internal/app/enaptercli/testdata/device_upload/cli_message/responses b/internal/app/enaptercli/testdata/device_upload/cli_message/responses deleted file mode 100644 index cefce05..0000000 --- a/internal/app/enaptercli/testdata/device_upload/cli_message/responses +++ /dev/null @@ -1,4 +0,0 @@ -{"data":{"device":{"uploadBlueprint":{"data":{"code":"started","message":"Uploading blueprint successfully started.","title":"Started","operationId":"25"},"errors":null}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"IN_PROGRESS","logs":{"edges":[{"cursor":"MQ","node":{"payload":"Started uploading blueprint[id=d428e77c-3081-4873-b343-2f8f96d9cadc] on device[hardware_id=SIM-WTM]","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"Mg","node":{"payload":"Generating configuration for uploading","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}},{"cursor":"Mw","node":{"payload":"Updating configuration on the platform","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}},{"cursor":"NA","node":{"payload":"Uploading blueprint finished successfully","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} diff --git a/internal/app/enaptercli/testdata/device_upload/cli_message/settings.json b/internal/app/enaptercli/testdata/device_upload/cli_message/settings.json deleted file mode 100644 index 18d48a5..0000000 --- a/internal/app/enaptercli/testdata/device_upload/cli_message/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "hardware_id": "SIM-WTM", - "cli_message": "VERSION IS OUTDATED" -} diff --git a/internal/app/enaptercli/testdata/device_upload/simple/blueprint/manifest.yml b/internal/app/enaptercli/testdata/device_upload/simple/blueprint/manifest.yml deleted file mode 100644 index e69de29..0000000 diff --git a/internal/app/enaptercli/testdata/device_upload/simple/output b/internal/app/enaptercli/testdata/device_upload/simple/output deleted file mode 100644 index 40f2098..0000000 --- a/internal/app/enaptercli/testdata/device_upload/simple/output +++ /dev/null @@ -1,8 +0,0 @@ -Blueprint files to be uploaded: -* testdata/device_upload/simple/blueprint/manifest.yml -upload started with operation id 25 -[#25] 2020-12-09T14:02:07Z [INFO] Started uploading blueprint[id=d428e77c-3081-4873-b343-2f8f96d9cadc] on device[hardware_id=SIM-WTM] -[#25] 2020-12-09T14:02:07Z [INFO] Generating configuration for uploading -[#25] 2020-12-09T14:02:07Z [INFO] Updating configuration on the platform -[#25] 2020-12-09T14:02:07Z [INFO] Uploading blueprint finished successfully -Done! diff --git a/internal/app/enaptercli/testdata/device_upload/simple/requests b/internal/app/enaptercli/testdata/device_upload/simple/requests deleted file mode 100644 index 8efcf61..0000000 --- a/internal/app/enaptercli/testdata/device_upload/simple/requests +++ /dev/null @@ -1,7 +0,0 @@ -{"query":"mutation($input:UploadBlueprintInput!){device{uploadBlueprint(input: $input){data{code,message,title,operationId},errors{code,message,path,title}}}}","variables":{"input":{"blueprint":"data:application/gzip;base64,UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAAMAAAAbWFuaWZlc3QueW1sAQAA//9QSwcIAAAAAAUAAAAAAAAAUEsBAhQAFAAIAAgAAAAAAAAAAAAFAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAG1hbmlmZXN0LnltbFBLBQYAAAAAAQABADoAAAA/AAAAAAA=","hardwareId":"SIM-WTM"}}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"25"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"MQ","hardware_id":"SIM-WTM","operation_id":"25"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"25"}} diff --git a/internal/app/enaptercli/testdata/device_upload/simple/responses b/internal/app/enaptercli/testdata/device_upload/simple/responses deleted file mode 100644 index cefce05..0000000 --- a/internal/app/enaptercli/testdata/device_upload/simple/responses +++ /dev/null @@ -1,4 +0,0 @@ -{"data":{"device":{"uploadBlueprint":{"data":{"code":"started","message":"Uploading blueprint successfully started.","title":"Started","operationId":"25"},"errors":null}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"IN_PROGRESS","logs":{"edges":[{"cursor":"MQ","node":{"payload":"Started uploading blueprint[id=d428e77c-3081-4873-b343-2f8f96d9cadc] on device[hardware_id=SIM-WTM]","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"Mg","node":{"payload":"Generating configuration for uploading","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}},{"cursor":"Mw","node":{"payload":"Updating configuration on the platform","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}},{"cursor":"NA","node":{"payload":"Uploading blueprint finished successfully","createdAt":"2020-12-09T14:02:07Z","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} diff --git a/internal/app/enaptercli/testdata/device_upload/simple/settings.json b/internal/app/enaptercli/testdata/device_upload/simple/settings.json deleted file mode 100644 index e2bb1f5..0000000 --- a/internal/app/enaptercli/testdata/device_upload/simple/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hardware_id": "SIM-WTM" -} diff --git a/internal/app/enaptercli/testdata/device_upload/upload_errors/output b/internal/app/enaptercli/testdata/device_upload/upload_errors/output deleted file mode 100644 index 8ba1c83..0000000 --- a/internal/app/enaptercli/testdata/device_upload/upload_errors/output +++ /dev/null @@ -1,5 +0,0 @@ -Blueprint files to be uploaded: -* testdata/device_upload/simple/blueprint/manifest.yml -[ERROR] hmm... wait a minute -[ERROR] oops! -app exit with error: request execution failed diff --git a/internal/app/enaptercli/testdata/device_upload/upload_errors/requests b/internal/app/enaptercli/testdata/device_upload/upload_errors/requests deleted file mode 100644 index e74f0f1..0000000 --- a/internal/app/enaptercli/testdata/device_upload/upload_errors/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"mutation($input:UploadBlueprintInput!){device{uploadBlueprint(input: $input){data{code,message,title,operationId},errors{code,message,path,title}}}}","variables":{"input":{"blueprint":"data:application/gzip;base64,UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAAMAAAAbWFuaWZlc3QueW1sAQAA//9QSwcIAAAAAAUAAAAAAAAAUEsBAhQAFAAIAAgAAAAAAAAAAAAFAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAG1hbmlmZXN0LnltbFBLBQYAAAAAAQABADoAAAA/AAAAAAA=","hardwareId":"SIM-WTM"}}} diff --git a/internal/app/enaptercli/testdata/device_upload/upload_errors/responses b/internal/app/enaptercli/testdata/device_upload/upload_errors/responses deleted file mode 100644 index 31dfdfd..0000000 --- a/internal/app/enaptercli/testdata/device_upload/upload_errors/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"device":{"uploadBlueprint":{"data":null,"errors":[{"code":"warning","message":"hmm... wait a minute","title":"Started"},{"code":"fatal","message":"oops!","title":"Started"}]}}}} diff --git a/internal/app/enaptercli/testdata/device_upload/upload_errors/settings.json b/internal/app/enaptercli/testdata/device_upload/upload_errors/settings.json deleted file mode 100644 index e2bb1f5..0000000 --- a/internal/app/enaptercli/testdata/device_upload/upload_errors/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hardware_id": "SIM-WTM" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/output b/internal/app/enaptercli/testdata/device_upload_logs/cli_message/output deleted file mode 100644 index a4cd9d1..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/output +++ /dev/null @@ -1,5 +0,0 @@ -VERSION IS OUTDATED -[#5] 2020-12-17T13:32:57Z [INFO] Started uploading blueprint[id=42cc8af1-cc60-4eeb-972f-0c0bfa6e3df5] on device[hardware_id=SIM-WTM] -[#5] 2020-12-17T13:32:57Z [INFO] Generating configuration for uploading -[#5] 2020-12-17T13:32:57Z [INFO] Updating configuration on the platform -[#5] 2020-12-17T13:32:57Z [INFO] Uploading blueprint finished successfully diff --git a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/requests b/internal/app/enaptercli/testdata/device_upload_logs/cli_message/requests deleted file mode 100644 index d2f87b8..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/requests +++ /dev/null @@ -1,3 +0,0 @@ -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"5"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"5"}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/responses b/internal/app/enaptercli/testdata/device_upload_logs/cli_message/responses deleted file mode 100644 index 5588761..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/responses +++ /dev/null @@ -1,2 +0,0 @@ -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"MQ","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Started uploading blueprint[id=42cc8af1-cc60-4eeb-972f-0c0bfa6e3df5] on device[hardware_id=SIM-WTM]","severity":"INFO"}},{"cursor":"Mg","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Generating configuration for uploading","severity":"INFO"}},{"cursor":"Mw","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Updating configuration on the platform","severity":"INFO"}},{"cursor":"NA","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Uploading blueprint finished successfully","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/cli_message/settings.json deleted file mode 100644 index 20c3a13..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/cli_message/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "hardware_id": "SIM-WTM", - "operation_id": "5", - "cli_message": "VERSION IS OUTDATED" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/simple/output b/internal/app/enaptercli/testdata/device_upload_logs/simple/output deleted file mode 100644 index 39a229e..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/simple/output +++ /dev/null @@ -1,4 +0,0 @@ -[#5] 2020-12-17T13:32:57Z [INFO] Started uploading blueprint[id=42cc8af1-cc60-4eeb-972f-0c0bfa6e3df5] on device[hardware_id=SIM-WTM] -[#5] 2020-12-17T13:32:57Z [INFO] Generating configuration for uploading -[#5] 2020-12-17T13:32:57Z [INFO] Updating configuration on the platform -[#5] 2020-12-17T13:32:57Z [INFO] Uploading blueprint finished successfully diff --git a/internal/app/enaptercli/testdata/device_upload_logs/simple/requests b/internal/app/enaptercli/testdata/device_upload_logs/simple/requests deleted file mode 100644 index d2f87b8..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/simple/requests +++ /dev/null @@ -1,3 +0,0 @@ -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"5"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"5"}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/simple/responses b/internal/app/enaptercli/testdata/device_upload_logs/simple/responses deleted file mode 100644 index 5588761..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/simple/responses +++ /dev/null @@ -1,2 +0,0 @@ -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"MQ","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Started uploading blueprint[id=42cc8af1-cc60-4eeb-972f-0c0bfa6e3df5] on device[hardware_id=SIM-WTM]","severity":"INFO"}},{"cursor":"Mg","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Generating configuration for uploading","severity":"INFO"}},{"cursor":"Mw","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Updating configuration on the platform","severity":"INFO"}},{"cursor":"NA","node":{"createdAt":"2020-12-17T13:32:57Z","payload":"Uploading blueprint finished successfully","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/simple/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/simple/settings.json deleted file mode 100644 index af8682b..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/simple/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "hardware_id": "SIM-WTM", - "operation_id": "5" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/output b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/output deleted file mode 100644 index 7367de8..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/output +++ /dev/null @@ -1 +0,0 @@ -app exit with error: request execution failed: device not found diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/requests b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/requests deleted file mode 100644 index 450f634..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"54"}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/responses b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/responses deleted file mode 100644 index e1c8cf9..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"device": null}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/settings.json deleted file mode 100644 index f5b1cbf..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "hardware_id": "SIM-WTM", - "operation_id": "54" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/output b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/output deleted file mode 100644 index 7367de8..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/output +++ /dev/null @@ -1 +0,0 @@ -app exit with error: request execution failed: device not found diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/requests b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/requests deleted file mode 100644 index b8be040..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"query($hardware_id:ID!$last_int:Int!){device(hardwareId: $hardware_id){blueprintUpdateOperations(last: $last_int){nodes{id}}}}","variables":{"hardware_id":"SIM-WTM","last_int":2}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/responses b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/responses deleted file mode 100644 index e1c8cf9..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"device": null}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/settings.json deleted file mode 100644 index e2bb1f5..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_hardware_id_without_operation_id/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hardware_id": "SIM-WTM" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/output b/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/output deleted file mode 100644 index 8f6a001..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/output +++ /dev/null @@ -1 +0,0 @@ -app exit with error: request execution failed: operation not found diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/requests b/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/requests deleted file mode 100644 index 450f634..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"54"}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/responses b/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/responses deleted file mode 100644 index 692d3f2..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"device": {"blueprintUpdateOperation":null}}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/settings.json deleted file mode 100644 index f5b1cbf..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/unknown_operation_id/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "hardware_id": "SIM-WTM", - "operation_id": "54" -} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/output b/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/output deleted file mode 100644 index 2502ef5..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/output +++ /dev/null @@ -1,8 +0,0 @@ -[#17] 2020-12-17T16:07:37Z [INFO] Started uploading blueprint[id=84606e67-d377-4f11-b3e4-421f08265ec7] on device[hardware_id=SIM-WTM] -[#17] 2020-12-17T16:07:37Z [INFO] Generating configuration for uploading -[#17] 2020-12-17T16:07:37Z [INFO] Updating configuration on the platform -[#17] 2020-12-17T16:07:37Z [INFO] Uploading blueprint finished successfully -[#20] 2020-12-21T11:53:14Z [INFO] Started uploading blueprint[id=a7be3c13-e138-4c43-a7c0-db50ef775613] on device[hardware_id=SIM-WTM] -[#20] 2020-12-21T11:53:14Z [INFO] Generating configuration for uploading -[#20] 2020-12-21T11:53:14Z [INFO] Updating configuration on the platform -[#20] 2020-12-21T11:53:14Z [INFO] Uploading blueprint finished successfully diff --git a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/requests b/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/requests deleted file mode 100644 index 52f4479..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/requests +++ /dev/null @@ -1,9 +0,0 @@ -{"query":"query($hardware_id:ID!$last_int:Int!){device(hardwareId: $hardware_id){blueprintUpdateOperations(last: $last_int){nodes{id}}}}","variables":{"hardware_id":"SIM-WTM","last_int":2}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"17"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"17"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"","hardware_id":"SIM-WTM","operation_id":"20"}} - -{"query":"query($after_cursor:String!$hardware_id:ID!$operation_id:ID!){device(hardwareId: $hardware_id){blueprintUpdateOperation(id: $operation_id){status,logs(after: $after_cursor){edges{cursor,node{payload,createdAt,severity}}}}}}","variables":{"after_cursor":"NA","hardware_id":"SIM-WTM","operation_id":"20"}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/responses b/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/responses deleted file mode 100644 index 8127b73..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/responses +++ /dev/null @@ -1,5 +0,0 @@ -{"data":{"device":{"blueprintUpdateOperations":{"nodes":[{"id":"17"},{"id":"20"}]}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"MQ","node":{"createdAt":"2020-12-17T16:07:37Z","payload":"Started uploading blueprint[id=84606e67-d377-4f11-b3e4-421f08265ec7] on device[hardware_id=SIM-WTM]","severity":"INFO"}},{"cursor":"Mg","node":{"createdAt":"2020-12-17T16:07:37Z","payload":"Generating configuration for uploading","severity":"INFO"}},{"cursor":"Mw","node":{"createdAt":"2020-12-17T16:07:37Z","payload":"Updating configuration on the platform","severity":"INFO"}},{"cursor":"NA","node":{"createdAt":"2020-12-17T16:07:37Z","payload":"Uploading blueprint finished successfully","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[{"cursor":"MQ","node":{"createdAt":"2020-12-21T11:53:14Z","payload":"Started uploading blueprint[id=a7be3c13-e138-4c43-a7c0-db50ef775613] on device[hardware_id=SIM-WTM]","severity":"INFO"}},{"cursor":"Mg","node":{"createdAt":"2020-12-21T11:53:14Z","payload":"Generating configuration for uploading","severity":"INFO"}},{"cursor":"Mw","node":{"createdAt":"2020-12-21T11:53:14Z","payload":"Updating configuration on the platform","severity":"INFO"}},{"cursor":"NA","node":{"createdAt":"2020-12-21T11:53:14Z","payload":"Uploading blueprint finished successfully","severity":"INFO"}}]}}}}} -{"data":{"device":{"blueprintUpdateOperation":{"status":"SUCCEEDED","logs":{"edges":[]}}}}} diff --git a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/settings.json b/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/settings.json deleted file mode 100644 index e2bb1f5..0000000 --- a/internal/app/enaptercli/testdata/device_upload_logs/without_operation_id/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hardware_id": "SIM-WTM" -} diff --git a/internal/app/enaptercli/testdata/helps/enapter b/internal/app/enaptercli/testdata/helps/enapter index b916872..159cf84 100644 --- a/internal/app/enaptercli/testdata/helps/enapter +++ b/internal/app/enaptercli/testdata/helps/enapter @@ -1,18 +1,25 @@ NAME: - enaptercli.test - Command line interface for Enapter services. + enaptercli.test - Command Line Interface (CLI) for Enapter services. USAGE: - enaptercli.test [global options] command [command options] [arguments...] + enaptercli.test [global options] command [command options] DESCRIPTION: - Enapter CLI requires access token for authentication. The token can be obtained in your Enapter Cloud account settings. - - Configure API token using ENAPTER_API_TOKEN environment variable or using --token global option. + The Enapter CLI requires an access token for authentication. You can obtain the token in your Enapter Cloud account settings. COMMANDS: - devices Device information and management commands. - rules Rules information and management commands. - help, h Shows a list of commands or help for one command + site Manage sites + device Manage devices + blueprint Manage blueprints + rule-engine Manage the rule engine + connection Manage connections to Enapter Cloud and Gateways + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --help, -h show help (default: false) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint b/internal/app/enaptercli/testdata/helps/enapter blueprint new file mode 100644 index 0000000..4c25e20 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint @@ -0,0 +1,21 @@ +NAME: + enaptercli.test blueprint - Manage blueprints + +USAGE: + enaptercli.test blueprint command [command options] + +COMMANDS: + profiles Manage blueprint profiles + upload Upload the blueprint to the Platform + download Download the blueprint zip from the Platform + get Retrieve blueprint metadata + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint download b/internal/app/enaptercli/testdata/helps/enapter blueprint download new file mode 100644 index 0000000..7081ba2 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint download @@ -0,0 +1,19 @@ +NAME: + enaptercli.test blueprint download - Download the blueprint zip from the Platform + +USAGE: + enaptercli.test blueprint download [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --blueprint-id value, -b value blueprint name or ID to download + --output value, -o value blueprint file name to save the blueprint + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint get b/internal/app/enaptercli/testdata/helps/enapter blueprint get new file mode 100644 index 0000000..b104785 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint get @@ -0,0 +1,18 @@ +NAME: + enaptercli.test blueprint get - Retrieve blueprint metadata + +USAGE: + enaptercli.test blueprint get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --blueprint-id value, -b value blueprint name or ID to retrieve + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint profiles b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles new file mode 100644 index 0000000..e8d0dde --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles @@ -0,0 +1,19 @@ +NAME: + enaptercli.test blueprint profiles - Manage blueprint profiles + +USAGE: + enaptercli.test blueprint profiles command [command options] + +COMMANDS: + download Download profiles zip from the Platform + upload Upload profiles to the Platform + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint profiles download b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles download new file mode 100644 index 0000000..88a0bfe --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles download @@ -0,0 +1,18 @@ +NAME: + enaptercli.test blueprint profiles download - Download profiles zip from the Platform + +USAGE: + enaptercli.test blueprint profiles download [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --output value, -o value file name to save the downloaded profiles + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint profiles upload b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles upload new file mode 100644 index 0000000..1d5da02 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint profiles upload @@ -0,0 +1,18 @@ +NAME: + enaptercli.test blueprint profiles upload - Upload profiles to the Platform + +USAGE: + enaptercli.test blueprint profiles upload [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --path value, -p value profiles zip file path + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter blueprint upload b/internal/app/enaptercli/testdata/helps/enapter blueprint upload new file mode 100644 index 0000000..7932fb2 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter blueprint upload @@ -0,0 +1,18 @@ +NAME: + enaptercli.test blueprint upload - Upload the blueprint to the Platform + +USAGE: + enaptercli.test blueprint upload [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --path value, -p value blueprint path (zip file or directory) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter connection b/internal/app/enaptercli/testdata/helps/enapter connection new file mode 100644 index 0000000..8438469 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter connection @@ -0,0 +1,15 @@ +NAME: + enaptercli.test connection - Manage connections to Enapter Cloud and Gateways + +USAGE: + enaptercli.test connection command [command options] + +COMMANDS: + add Add a new connection + remove Remove a connection + list List all connections + set-default Set default connection + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help diff --git a/internal/app/enaptercli/testdata/helps/enapter connection add b/internal/app/enaptercli/testdata/helps/enapter connection add new file mode 100644 index 0000000..9ebed3e --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter connection add @@ -0,0 +1,14 @@ +NAME: + enaptercli.test connection add - Add a new connection + +USAGE: + enaptercli.test connection add [command options] + +OPTIONS: + --name value connection name + --gateway indicates that the connection is to a Gateway (default: false) + --url value Enapter API base URL (default: "https://api.enapter.com") + --token value Enapter API access token + --site-id value if specified, the connection will be limited to this site (available only for Cloud connections) + --allow-insecure allow insecure connections to the Enapter API (default: false) + --help, -h show help diff --git a/internal/app/enaptercli/testdata/helps/enapter connection list b/internal/app/enaptercli/testdata/helps/enapter connection list new file mode 100644 index 0000000..84afb87 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter connection list @@ -0,0 +1,8 @@ +NAME: + enaptercli.test connection list - List all connections + +USAGE: + enaptercli.test connection list [command options] + +OPTIONS: + --help, -h show help diff --git a/internal/app/enaptercli/testdata/helps/enapter connection remove b/internal/app/enaptercli/testdata/helps/enapter connection remove new file mode 100644 index 0000000..207770c --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter connection remove @@ -0,0 +1,9 @@ +NAME: + enaptercli.test connection remove - Remove a connection + +USAGE: + enaptercli.test connection remove [command options] + +OPTIONS: + --name value connection name + --help, -h show help diff --git a/internal/app/enaptercli/testdata/helps/enapter connection set-default b/internal/app/enaptercli/testdata/helps/enapter connection set-default new file mode 100644 index 0000000..eb243ae --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter connection set-default @@ -0,0 +1,9 @@ +NAME: + enaptercli.test connection set-default - Set default connection + +USAGE: + enaptercli.test connection set-default [command options] + +OPTIONS: + --name value connection name + --help, -h show help diff --git a/internal/app/enaptercli/testdata/helps/enapter device b/internal/app/enaptercli/testdata/helps/enapter device new file mode 100644 index 0000000..131a4c7 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device @@ -0,0 +1,28 @@ +NAME: + enaptercli.test device - Manage devices + +USAGE: + enaptercli.test device command [command options] + +COMMANDS: + create Create devices of different types + list List user devices ordered by device ID + get Retrieve device information + change-blueprint Change device blueprint + logs Show device logs + update Update a device + delete Delete a device + command Manage device commands + telemetry Show device telemetry + communication-config Manage device communication config + run-terminal Run new remote terminal session + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device change-blueprint b/internal/app/enaptercli/testdata/helps/enapter device change-blueprint new file mode 100644 index 0000000..c5d106a --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device change-blueprint @@ -0,0 +1,21 @@ +NAME: + enaptercli.test device change-blueprint - Change device blueprint + +USAGE: + enaptercli.test device change-blueprint [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --blueprint-id value, -b value blueprint ID to use as new device blueprint + --blueprint-path value blueprint path (zip file or directory) to use as new device blueprint + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device command b/internal/app/enaptercli/testdata/helps/enapter device command new file mode 100644 index 0000000..a29328a --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device command @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device command - Manage device commands + +USAGE: + enaptercli.test device command command [command options] + +COMMANDS: + execute Execute a device command + list List device command executions + get Retrieve a device command execution + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device command execute b/internal/app/enaptercli/testdata/helps/enapter device command execute new file mode 100644 index 0000000..b041eda --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device command execute @@ -0,0 +1,21 @@ +NAME: + enaptercli.test device command execute - Execute a device command + +USAGE: + enaptercli.test device command execute [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --name value command name + --arguments value command arguments (should be a JSON string) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device command get b/internal/app/enaptercli/testdata/helps/enapter device command get new file mode 100644 index 0000000..45b0868 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device command get @@ -0,0 +1,21 @@ +NAME: + enaptercli.test device command get - Retrieve a device command execution + +USAGE: + enaptercli.test device command get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --execution-id value execution ID + --expand value [ --expand value ] coma-separated list of expanded options (supported values: log) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device command list b/internal/app/enaptercli/testdata/helps/enapter device command list new file mode 100644 index 0000000..a189c0b --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device command list @@ -0,0 +1,19 @@ +NAME: + enaptercli.test device command list - List device command executions + +USAGE: + enaptercli.test device command list [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device communication-config b/internal/app/enaptercli/testdata/helps/enapter device communication-config new file mode 100644 index 0000000..9ca4f7f --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device communication-config @@ -0,0 +1,18 @@ +NAME: + enaptercli.test device communication-config - Manage device communication config + +USAGE: + enaptercli.test device communication-config command [command options] + +COMMANDS: + generate Generate a new communication config for device + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device communication-config generate b/internal/app/enaptercli/testdata/helps/enapter device communication-config generate new file mode 100644 index 0000000..ffc8338 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device communication-config generate @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device communication-config generate - Generate a new communication config for device + +USAGE: + enaptercli.test device communication-config generate [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --protocol value connection protocol (supported values: MQTT, MQTTS) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device create b/internal/app/enaptercli/testdata/helps/enapter device create new file mode 100644 index 0000000..fe5efa7 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device create @@ -0,0 +1,19 @@ +NAME: + enaptercli.test device create - Create devices of different types + +USAGE: + enaptercli.test device create command [command options] + +COMMANDS: + standalone Create a new standalone device + lua-device Create a new Lua device + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device create lua-device b/internal/app/enaptercli/testdata/helps/enapter device create lua-device new file mode 100644 index 0000000..fe4ea93 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device create lua-device @@ -0,0 +1,23 @@ +NAME: + enaptercli.test device create lua-device - Create a new Lua device + +USAGE: + enaptercli.test device create lua-device [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --runtime-id value, -r value UCM device ID where the new Lua device will run + --device-name value, -n value name for the new Lua device + --device-slug value slug for the new Lua device + --blueprint-id value, -b value blueprint ID to use for the new Lua device + --blueprint-path value blueprint path (zip file or directory) to use for the new Lua device + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device create standalone b/internal/app/enaptercli/testdata/helps/enapter device create standalone new file mode 100644 index 0000000..f98d0f1 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device create standalone @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device create standalone - Create a new standalone device + +USAGE: + enaptercli.test device create standalone [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value, -s value site ID where the device will be created + --device-name value, -n value name for the new device + --device-slug value slug for the new standalone device + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device delete b/internal/app/enaptercli/testdata/helps/enapter device delete new file mode 100644 index 0000000..44e27cd --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device delete @@ -0,0 +1,19 @@ +NAME: + enaptercli.test device delete - Delete a device + +USAGE: + enaptercli.test device delete [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device get b/internal/app/enaptercli/testdata/helps/enapter device get new file mode 100644 index 0000000..84246aa --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device get @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device get - Retrieve device information + +USAGE: + enaptercli.test device get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --expand value [ --expand value ] coma-separated list of expanded device information (supported values: connectivity, manifest, properties, communication, site) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device list b/internal/app/enaptercli/testdata/helps/enapter device list new file mode 100644 index 0000000..87711a7 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device list @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device list - List user devices ordered by device ID + +USAGE: + enaptercli.test device list [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --expand value [ --expand value ] coma-separated list of expanded device information (supported values: connectivity, manifest, properties, communication, site) + --limit value maximum number of devices to retrieve (default: retrieves all) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device logs b/internal/app/enaptercli/testdata/helps/enapter device logs new file mode 100644 index 0000000..cef3a68 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device logs @@ -0,0 +1,27 @@ +NAME: + enaptercli.test device logs - Show device logs + +USAGE: + enaptercli.test device logs [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --follow, -f follow the log output (default: false) + --from value from timestamp in RFC 3339 format (e.g. 2006-01-02T15:04:05Z) + --to value to timestamp in RFC 3339 format (e.g. 2006-01-02T15:04:05Z) + --limit value, -l value maximum number of logs to retrieve (default: 0) + --offset value, -o value number of logs to skip when retrieving (default: 0) + --severity value, -s value filter logs by severity + --order value order logs by criteria (RECEIVED_AT_ASC[default], RECEIVED_AT_DESC) + --show value filter logs by criteria (ALL[default], PERSISTED_ONLY, TEMPORARY_ONLY) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device run-terminal b/internal/app/enaptercli/testdata/helps/enapter device run-terminal new file mode 100644 index 0000000..53ba0f3 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device run-terminal @@ -0,0 +1,22 @@ +NAME: + enaptercli.test device run-terminal - Run new remote terminal session + +USAGE: + enaptercli.test device run-terminal [command options] + +DESCRIPTION: + Remote terminal feature should be enabled in gateway settings. Use Ctrl+] sequence to force connection close. + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value gateway device ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device telemetry b/internal/app/enaptercli/testdata/helps/enapter device telemetry new file mode 100644 index 0000000..9b759a3 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device telemetry @@ -0,0 +1,20 @@ +NAME: + enaptercli.test device telemetry - Show device telemetry + +USAGE: + enaptercli.test device telemetry [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --follow, -f follow the telemetry output (default: false) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter device update b/internal/app/enaptercli/testdata/helps/enapter device update new file mode 100644 index 0000000..1e42322 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter device update @@ -0,0 +1,21 @@ +NAME: + enaptercli.test device update - Update a device + +USAGE: + enaptercli.test device update [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --device-id value, -d value device ID + --name value device name + --slug value device slug + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter devices b/internal/app/enaptercli/testdata/helps/enapter devices deleted file mode 100644 index dddf861..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter devices +++ /dev/null @@ -1,16 +0,0 @@ -NAME: - enaptercli.test devices - Device information and management commands. - -USAGE: - enaptercli.test devices command [command options] [arguments...] - -COMMANDS: - upload Upload blueprint to a device - logs Stream logs from a device - upload-logs Show blueprint uploading logs - execute Execute command on device - help, h Shows a list of commands or help for one command - -OPTIONS: - --help, -h show help (default: false) - diff --git a/internal/app/enaptercli/testdata/helps/enapter devices execute b/internal/app/enaptercli/testdata/helps/enapter devices execute deleted file mode 100644 index 7df8a65..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter devices execute +++ /dev/null @@ -1,16 +0,0 @@ -NAME: - enaptercli.test devices execute - Execute command on device - -USAGE: - enaptercli.test devices execute [command options] [arguments...] - -OPTIONS: - --hardware-id value Hardware ID of the device; can be obtained in cloud.enapter.com - --command value Command name - --arguments value Command arguments as JSON object - --show-progress Enable in-progress responses streaming (default: false) - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter devices logs b/internal/app/enaptercli/testdata/helps/enapter devices logs deleted file mode 100644 index b68811b..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter devices logs +++ /dev/null @@ -1,13 +0,0 @@ -NAME: - enaptercli.test devices logs - Stream logs from a device - -USAGE: - enaptercli.test devices logs [command options] [arguments...] - -OPTIONS: - --hardware-id value Hardware ID of the device; can be obtained in cloud.enapter.com - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter devices upload b/internal/app/enaptercli/testdata/helps/enapter devices upload deleted file mode 100644 index 89cfd31..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter devices upload +++ /dev/null @@ -1,18 +0,0 @@ -NAME: - enaptercli.test devices upload - Upload blueprint to a device - -USAGE: - enaptercli.test devices upload [command options] [arguments...] - -DESCRIPTION: - Blueprint combines device capabilities declaration and Lua firmware for Enapter UCM. The command updates device blueprint and uploads the firmware to the UCM. Learn more about Enapter Blueprints at https://handbook.enapter.com/blueprints. - -OPTIONS: - --hardware-id value Hardware ID of the device; can be obtained in cloud.enapter.com - --timeout value Time to wait for blueprint uploading (default: 30s) - --blueprint-dir value Directory which contains blueprint file - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter devices upload-logs b/internal/app/enaptercli/testdata/helps/enapter devices upload-logs deleted file mode 100644 index fdd775d..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter devices upload-logs +++ /dev/null @@ -1,15 +0,0 @@ -NAME: - enaptercli.test devices upload-logs - Show blueprint uploading logs - -USAGE: - enaptercli.test devices upload-logs [command options] [arguments...] - -OPTIONS: - --hardware-id value Hardware ID of the device; can be obtained in cloud.enapter.com - --timeout value Time to wait for blueprint uploading (default: 30s) - --operation-id value Uploading operation ID (optional) - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine b/internal/app/enaptercli/testdata/helps/enapter rule-engine new file mode 100644 index 0000000..597bb25 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine @@ -0,0 +1,21 @@ +NAME: + enaptercli.test rule-engine - Manage the rule engine + +USAGE: + enaptercli.test rule-engine command [command options] + +COMMANDS: + get Retrieve the rule engine + suspend Suspend execution of rules + resume Resume execution of rules + rule Manage rules + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine get b/internal/app/enaptercli/testdata/helps/enapter rule-engine get new file mode 100644 index 0000000..e87cfe9 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine get @@ -0,0 +1,18 @@ +NAME: + enaptercli.test rule-engine get - Retrieve the rule engine + +USAGE: + enaptercli.test rule-engine get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine resume b/internal/app/enaptercli/testdata/helps/enapter rule-engine resume new file mode 100644 index 0000000..a25a105 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine resume @@ -0,0 +1,18 @@ +NAME: + enaptercli.test rule-engine resume - Resume execution of rules + +USAGE: + enaptercli.test rule-engine resume [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule new file mode 100644 index 0000000..6996d20 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule @@ -0,0 +1,26 @@ +NAME: + enaptercli.test rule-engine rule - Manage rules + +USAGE: + enaptercli.test rule-engine rule command [command options] + +COMMANDS: + create Create a new rule + delete Delete a rule + disable Disable one or more rules + enable Enable one or more rules + get Retrieve a rule + list List rules + update Update a rule + update-script Update the script of a rule + logs Show rule logs + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule create b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule create new file mode 100644 index 0000000..24645d9 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule create @@ -0,0 +1,23 @@ +NAME: + enaptercli.test rule-engine rule create - Create a new rule + +USAGE: + enaptercli.test rule-engine rule create [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --slug value Slug for the new rule + --script value Path to the file containing the script code + --runtime-version value Version of the runtime to use for the script execution (default: "V3") + --exec-interval value How frequently to execute the script (compatible only with runtime version 1) in duration format (e.g., 5s, 2m) + --disable Disable the rule upon creation (default: false) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule delete b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule delete new file mode 100644 index 0000000..66a260b --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule delete @@ -0,0 +1,19 @@ +NAME: + enaptercli.test rule-engine rule delete - Delete a rule + +USAGE: + enaptercli.test rule-engine rule delete [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value Rule ID or slug + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule disable b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule disable new file mode 100644 index 0000000..a3e6d94 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule disable @@ -0,0 +1,19 @@ +NAME: + enaptercli.test rule-engine rule disable - Disable one or more rules + +USAGE: + enaptercli.test rule-engine rule disable [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value [ --rule-id value ] Rule IDs or slugs + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule enable b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule enable new file mode 100644 index 0000000..b58bad4 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule enable @@ -0,0 +1,19 @@ +NAME: + enaptercli.test rule-engine rule enable - Enable one or more rules + +USAGE: + enaptercli.test rule-engine rule enable [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value [ --rule-id value ] Rule IDs or slugs + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule get b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule get new file mode 100644 index 0000000..e794ae6 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule get @@ -0,0 +1,19 @@ +NAME: + enaptercli.test rule-engine rule get - Retrieve a rule + +USAGE: + enaptercli.test rule-engine rule get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value Rule ID or slug + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule list b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule list new file mode 100644 index 0000000..b400c2a --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule list @@ -0,0 +1,18 @@ +NAME: + enaptercli.test rule-engine rule list - List rules + +USAGE: + enaptercli.test rule-engine rule list [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule logs b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule logs new file mode 100644 index 0000000..2d46abc --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule logs @@ -0,0 +1,20 @@ +NAME: + enaptercli.test rule-engine rule logs - Show rule logs + +USAGE: + enaptercli.test rule-engine rule logs [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value rule ID + --follow, -f follow the log output (default: false) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update new file mode 100644 index 0000000..9b3f278 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update @@ -0,0 +1,20 @@ +NAME: + enaptercli.test rule-engine rule update - Update a rule + +USAGE: + enaptercli.test rule-engine rule update [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value Rule ID or slug to update + --slug value A new rule slug + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update-script b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update-script new file mode 100644 index 0000000..e64f5a8 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine rule update-script @@ -0,0 +1,22 @@ +NAME: + enaptercli.test rule-engine rule update-script - Update the script of a rule + +USAGE: + enaptercli.test rule-engine rule update-script [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --rule-id value Rule ID or slug to update + --script value Path to a file containing the script code + --runtime-version value Version of the runtime to use for the script execution (default: "V3") + --exec-interval value How frequently to execute the script (compatible only with runtime version 1) in duration format (e.g., 5s, 2m) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rule-engine suspend b/internal/app/enaptercli/testdata/helps/enapter rule-engine suspend new file mode 100644 index 0000000..e632a47 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter rule-engine suspend @@ -0,0 +1,18 @@ +NAME: + enaptercli.test rule-engine suspend - Suspend execution of rules + +USAGE: + enaptercli.test rule-engine suspend [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter rules logs b/internal/app/enaptercli/testdata/helps/enapter rules logs deleted file mode 100644 index c831068..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter rules logs +++ /dev/null @@ -1,13 +0,0 @@ -NAME: - enaptercli.test rules logs - Stream logs from a rule - -USAGE: - enaptercli.test rules logs [command options] [arguments...] - -OPTIONS: - --rule-id value Rule ID; can be obtained in cloud.enapter.com - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter rules update b/internal/app/enaptercli/testdata/helps/enapter rules update deleted file mode 100644 index f92a7af..0000000 --- a/internal/app/enaptercli/testdata/helps/enapter rules update +++ /dev/null @@ -1,17 +0,0 @@ -NAME: - enaptercli.test rules update - Update rule. - -USAGE: - enaptercli.test rules update [command options] [arguments...] - -OPTIONS: - --rule-id value Rule ID; can be obtained in cloud.enapter.com - --rule-path value Path to file with rule Lua code - --execution-interval value Rule execution interval in milliseconds (default: chosen by the server) - --stdlib-version value Version of standard library used by the rule (default: chosen by the server) - --timeout value Time to wait for rule update (default: 30s) - --help, -h show help (default: false) - -ENVIRONMENT VARIABLES: - ENAPTER_API_TOKEN Enapter API access token - diff --git a/internal/app/enaptercli/testdata/helps/enapter site b/internal/app/enaptercli/testdata/helps/enapter site new file mode 100644 index 0000000..e0cc024 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter site @@ -0,0 +1,19 @@ +NAME: + enaptercli.test site - Manage sites + +USAGE: + enaptercli.test site command [command options] + +COMMANDS: + list List user sites + get Get a site + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter site get b/internal/app/enaptercli/testdata/helps/enapter site get new file mode 100644 index 0000000..0d75156 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter site get @@ -0,0 +1,18 @@ +NAME: + enaptercli.test site get - Get a site + +USAGE: + enaptercli.test site get [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --site-id value site ID + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/helps/enapter site list b/internal/app/enaptercli/testdata/helps/enapter site list new file mode 100644 index 0000000..0ce7752 --- /dev/null +++ b/internal/app/enaptercli/testdata/helps/enapter site list @@ -0,0 +1,19 @@ +NAME: + enaptercli.test site list - List user sites + +USAGE: + enaptercli.test site list [command options] + +OPTIONS: + --connection value, -c value name of the connection to use + --api-allow-insecure allow insecure connections to the Enapter API (default: false) [$ENAPTER3_API_ALLOW_INSECURE] + --verbose log extra details about the operation (default: false) + --my-sites returns only sites where user is owner or installer (default: false) + --limit value maximum number of sites to retrieve (default: retrieves all) + --help, -h show help + +ENVIRONMENT VARIABLES: + ENAPTER3_API_TOKEN Enapter API access token + ENAPTER3_API_URL Enapter API base URL (default: https://api.enapter.com) + ENAPTER3_API_ALLOW_INSECURE Allow insecure connections to the Enapter API (default: false) + diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/cmd.tmpl new file mode 100644 index 0000000..0d61ed5 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 blueprint get --connection my-conn --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/out b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/out new file mode 100644 index 0000000..fca5dda --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/out @@ -0,0 +1 @@ +{"created_at": TODAY!} diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/req_0 new file mode 100644 index 0000000..65ad234 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/blueprints/cdd82438-dda8-4f69-aad1-0be9adeab964", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/resp_0 new file mode 100644 index 0000000..fca5dda --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_id/resp_0 @@ -0,0 +1 @@ +{"created_at": TODAY!} diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/cmd.tmpl new file mode 100644 index 0000000..f980f03 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 blueprint get --connection my-conn --blueprint-id test_blueprint_name diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/out b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/out new file mode 100644 index 0000000..fca5dda --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/out @@ -0,0 +1 @@ +{"created_at": TODAY!} diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/req_0 b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/req_0 new file mode 100644 index 0000000..72fc047 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/blueprints/enapter/test_blueprint_name/latest", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/resp_0 new file mode 100644 index 0000000..fca5dda --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/blueprint_get_by_name/resp_0 @@ -0,0 +1 @@ +{"created_at": TODAY!} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/cmd.tmpl new file mode 100644 index 0000000..713bef9 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device get --connection my-conn --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/req_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/req_0 new file mode 100644 index 0000000..e16fe8e --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_connection_name/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/cmd.tmpl new file mode 100644 index 0000000..f0ac177 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/cmd.tmpl @@ -0,0 +1,3 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 connection set-default --name my-conn +enapter3 device get --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/req_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/req_0 new file mode 100644 index 0000000..e16fe8e --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_default_connection/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/cmd.tmpl new file mode 100644 index 0000000..cf34301 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/cmd.tmpl @@ -0,0 +1 @@ +enapter3 device get --token 123 --api-url {{.URL}} --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/req_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/req_0 new file mode 100644 index 0000000..8945df1 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "123" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_by_flags/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/cmd.tmpl new file mode 100644 index 0000000..eb1adbb --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/cmd.tmpl @@ -0,0 +1 @@ +enapter3 device get --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/out new file mode 100644 index 0000000..2acda60 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_missing/out @@ -0,0 +1,10 @@ +app exit with error: No connection configured. + +Please, specify connection using --connection flag. + +To list available connections: +$ enapter3 connection list + +To add a new connection: +$ enapter3 connection add + diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/cmd.tmpl new file mode 100644 index 0000000..3b85c13 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device get --connection my-conn --token {{.Token}} --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/out new file mode 100644 index 0000000..9d7e60d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/out @@ -0,0 +1,2 @@ +WARNING: credentials set via environment variables or flags are ignored. +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/req_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/req_0 new file mode 100644 index 0000000..e16fe8e --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_mixed/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/cmd.tmpl new file mode 100644 index 0000000..7250861 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} --site-id my-site +enapter3 device get --connection my-conn --site-id other-site --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/out new file mode 100644 index 0000000..ea896f0 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_mismatch/out @@ -0,0 +1 @@ +app exit with error: passed site-ID must match the site-ID of the current connection diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/cmd.tmpl new file mode 100644 index 0000000..9e9d408 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} --site-id my-site +enapter3 device get --connection my-conn --site-id my-site --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/out b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/req_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/req_0 new file mode 100644 index 0000000..99cb7dd --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/credentials_site_id_redundancy/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/cmd.tmpl new file mode 100644 index 0000000..dc26dc1 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device change-blueprint --connection my-conn --device-id 427ec09e-ec1e-4760-acc1-50106533b875 --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/out new file mode 100644 index 0000000..d832236 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/out @@ -0,0 +1 @@ +{"blueprint": "assigned"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/req_0 new file mode 100644 index 0000000..0a7f3c4 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/req_0 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/devices/427ec09e-ec1e-4760-acc1-50106533b875/assign_blueprint", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "55" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "{\"blueprint_id\":\"cdd82438-dda8-4f69-aad1-0be9adeab964\"}" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/resp_0 new file mode 100644 index 0000000..d832236 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id/resp_0 @@ -0,0 +1 @@ +{"blueprint": "assigned"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/cmd.tmpl new file mode 100644 index 0000000..17c9b3a --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device change-blueprint --connection my-conn --device-id 427ec09e-ec1e-4760-acc1-50106533b875 --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 --blueprint-path ./testdata/blueprints/bp.zip diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/out b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/out new file mode 100644 index 0000000..dc69131 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_id_and_path/out @@ -0,0 +1 @@ +app exit with error: only one of --blueprint-id or --blueprint-path can be specified diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/cmd.tmpl new file mode 100644 index 0000000..1769b48 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device change-blueprint --connection my-conn --device-id 3b0a0626-2dc4-44a3-ac5a-34d58b7b2a26 --blueprint-path ./testdata/blueprints/simple diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/out b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/out new file mode 100644 index 0000000..f134b3a --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/out @@ -0,0 +1 @@ +{"blueprint": "assigned by path"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_0 new file mode 100644 index 0000000..0a841e2 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_0 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/blueprints/upload", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "400" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "PK\u0003\u0004\u0014\u0000\b\u0000\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\f\u0000\u0000\u0000firmware.luaJ\ufffdK,(I-\ufffd\ufffd\ufffdO\ufffdP\ufffdH\ufffd\ufffd\ufffdWH+\ufffd\ufffdUH\ufffd,\ufffd-O,J\ufffd\ufffd)MT\ufffd\ufffd\u0002\u0004\u0000\u0000\ufffd\ufffdPK\u0007\b\ufffd\ufffdv*-\u0000\u0000\u0000'\u0000\u0000\u0000PK\u0003\u0004\u0014\u0000\b\u0000\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\f\u0000\u0000\u0000manifest.yml\u0014\ufffd˩\ufffd@\f\u0005н\ufffd\ufffd\u0015\ufffd=\ufffdN5d\ufffd\u0002\ufffdb_\ufffd@3\u0016\ufffdIp\ufffd\ufffd\ufffd\ufffdy\ufffdd6\ufffdc\ufffd\ufffdM\ufffd\ufffd\ufffd\u001b\ufffd\u001e˿\ufffd\ufffd3\ufffdZ\ufffd\u0015*^^2\ufffd\ufffd4\ufffd6\ufffd\ufffdB\u0015`\\IEL\u0013\ufffd\ufffd\ufffdg\ufffd7\u0003\ufffd\u0007\u0015\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\u001e\ufffd\u0000\u0000\u0000\ufffd\ufffdPK\u0007\b*\u0011\u0019Ae\u0000\u0000\u0000l\u0000\u0000\u0000PK\u0001\u0002\u0014\u0000\u0014\u0000\b\u0000\b\u0000\u0000\u0000\u0000\u0000\ufffd\ufffdv*-\u0000\u0000\u0000'\u0000\u0000\u0000\f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000firmware.luaPK\u0001\u0002\u0014\u0000\u0014\u0000\b\u0000\b\u0000\u0000\u0000\u0000\u0000*\u0011\u0019Ae\u0000\u0000\u0000l\u0000\u0000\u0000\f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000g\u0000\u0000\u0000manifest.ymlPK\u0005\u0006\u0000\u0000\u0000\u0000\u0002\u0000\u0002\u0000t\u0000\u0000\u0000\u0006\u0001\u0000\u0000\u0000\u0000" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_1 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_1 new file mode 100644 index 0000000..26711ab --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/req_1 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/devices/3b0a0626-2dc4-44a3-ac5a-34d58b7b2a26/assign_blueprint", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "35" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "{\"blueprint_id\":\"new_blueprint_id\"}" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_0 new file mode 100644 index 0000000..deb78ad --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_0 @@ -0,0 +1 @@ +{ "blueprint": {"id": "new_blueprint_id"} } diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_1 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_1 new file mode 100644 index 0000000..f134b3a --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_path/resp_1 @@ -0,0 +1 @@ +{"blueprint": "assigned by path"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/cmd.tmpl new file mode 100644 index 0000000..0097e3a --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device change-blueprint --connection my-conn --device-id 3b0a0626-2dc4-44a3-ac5a-34d58b7b2a26 --blueprint-path ./testdata/blueprints/bp.zip diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/out b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/out new file mode 100644 index 0000000..c815626 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/out @@ -0,0 +1 @@ +{"blueprint": "assigned by zip"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_0 new file mode 100644 index 0000000..7175cac --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_0 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/blueprints/upload", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "14" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "blueprint.zip\n" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_1 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_1 new file mode 100644 index 0000000..8e5669b --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/req_1 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/devices/3b0a0626-2dc4-44a3-ac5a-34d58b7b2a26/assign_blueprint", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "40" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "{\"blueprint_id\":\"blueprint_id_from_zip\"}" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_0 new file mode 100644 index 0000000..34acd0a --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_0 @@ -0,0 +1 @@ +{ "blueprint": {"id": "blueprint_id_from_zip"} } diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_1 b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_1 new file mode 100644 index 0000000..c815626 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_by_zip/resp_1 @@ -0,0 +1 @@ +{"blueprint": "assigned by zip"} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/cmd.tmpl new file mode 100644 index 0000000..18c0433 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device change-blueprint --connection my-conn --device-id 427ec09e-ec1e-4760-acc1-50106533b875 diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/out b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/out new file mode 100644 index 0000000..8ccadaf --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_change_blueprint_without_blueprint/out @@ -0,0 +1 @@ +app exit with error: one of --blueprint-id or --blueprint-path must be specified diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/cmd.tmpl new file mode 100644 index 0000000..555e0c4 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device create lua-device --connection my-conn --runtime-id my-runtime --device-name my-device --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 --blueprint-path ./testdata/blueprints/bp.zip diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/out b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/out new file mode 100644 index 0000000..dc69131 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_blueprint_id_and_path/out @@ -0,0 +1 @@ +app exit with error: only one of --blueprint-id or --blueprint-path can be specified diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/cmd.tmpl new file mode 100644 index 0000000..d291224 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} --site-id my-site +enapter3 device create lua-device --connection my-conn --runtime-id my-runtime --device-name my-device --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/out new file mode 100644 index 0000000..dc63955 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/out @@ -0,0 +1 @@ +{"device":{"id":"1b6adca2-3a9f-4bfe-95b4-92b28a581055"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_0 new file mode 100644 index 0000000..34f782b --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/devices/my-runtime", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_1 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_1 new file mode 100644 index 0000000..9a91017 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/req_1 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/provisioning/lua_device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "136" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "{\"blueprint_id\":\"cdd82438-dda8-4f69-aad1-0be9adeab964\",\"name\":\"my-device\",\"runtime_id\":\"427ec09e-ec1e-4760-acc1-50106533b875\",\"slug\":\"\"}" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_1 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_1 new file mode 100644 index 0000000..dc63955 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_with_site_id/resp_1 @@ -0,0 +1 @@ +{"device":{"id":"1b6adca2-3a9f-4bfe-95b4-92b28a581055"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/cmd.tmpl new file mode 100644 index 0000000..fa50e46 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device create lua-device --connection my-conn --runtime-id my-runtime --device-name my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/out b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/out new file mode 100644 index 0000000..8ccadaf --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_blueprint/out @@ -0,0 +1 @@ +app exit with error: one of --blueprint-id or --blueprint-path must be specified diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/cmd.tmpl new file mode 100644 index 0000000..a8bd9e8 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device create lua-device --connection my-conn --runtime-id my-runtime --device-name my-device --blueprint-id cdd82438-dda8-4f69-aad1-0be9adeab964 diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/out new file mode 100644 index 0000000..dc63955 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/out @@ -0,0 +1 @@ +{"device":{"id":"1b6adca2-3a9f-4bfe-95b4-92b28a581055"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/req_0 new file mode 100644 index 0000000..569c3ba --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/req_0 @@ -0,0 +1,22 @@ +{ + "Method": "POST", + "URL": "/v3/provisioning/lua_device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Length": [ + "110" + ], + "Content-Type": [ + "application/json" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "{\"blueprint_id\":\"cdd82438-dda8-4f69-aad1-0be9adeab964\",\"name\":\"my-device\",\"runtime_id\":\"my-runtime\",\"slug\":\"\"}" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/resp_0 new file mode 100644 index 0000000..dc63955 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_lua_without_site_id/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"1b6adca2-3a9f-4bfe-95b4-92b28a581055"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/cmd.tmpl new file mode 100644 index 0000000..88631d2 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device create standalone --connection my-conn --device-name my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/out new file mode 100644 index 0000000..9789fa3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_create_standalone_without_site_id/out @@ -0,0 +1 @@ +app exit with error: site ID is required diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/cmd.tmpl new file mode 100644 index 0000000..ac61885 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device get --connection my-conn --site-id my-site --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/req_0 new file mode 100644 index 0000000..99cb7dd --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_with_site_id/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/cmd.tmpl new file mode 100644 index 0000000..713bef9 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 device get --connection my-conn --device-id my-device diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/out new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/out @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/req_0 new file mode 100644 index 0000000..e16fe8e --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/devices/my-device", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/resp_0 new file mode 100644 index 0000000..9e7cb4d --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/device_get_without_site_id/resp_0 @@ -0,0 +1 @@ +{"device":{"id":"427ec09e-ec1e-4760-acc1-50106533b875"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/cmd.tmpl new file mode 100644 index 0000000..0988d20 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 rule-engine get --connection my-conn --site-id my-site diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/out new file mode 100644 index 0000000..3fbd0b3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/out @@ -0,0 +1 @@ +{"engine":{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/req_0 new file mode 100644 index 0000000..b6b1edd --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/rule_engine", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/resp_0 new file mode 100644 index 0000000..3fbd0b3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_with_site_id/resp_0 @@ -0,0 +1 @@ +{"engine":{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/cmd.tmpl new file mode 100644 index 0000000..7e61b91 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 rule-engine get --connection my-conn diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/out new file mode 100644 index 0000000..9789fa3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_get_without_site_id/out @@ -0,0 +1 @@ +app exit with error: site ID is required diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/cmd.tmpl new file mode 100644 index 0000000..fff957c --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 site get --connection my-conn --site-id my-site diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/out new file mode 100644 index 0000000..86a96ed --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/out @@ -0,0 +1 @@ +{"site":{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/req_0 new file mode 100644 index 0000000..619b9c4 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/resp_0 new file mode 100644 index 0000000..86a96ed --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_with_site_id/resp_0 @@ -0,0 +1 @@ +{"site":{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/cmd.tmpl new file mode 100644 index 0000000..7ff9adf --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 site get --connection my-conn diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/out new file mode 100644 index 0000000..9789fa3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_get_without_site_id/out @@ -0,0 +1 @@ +app exit with error: site ID is required diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/cmd.tmpl new file mode 100644 index 0000000..b0fdd24 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} --site-id my-site +enapter3 site list --connection my-conn diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/out new file mode 100644 index 0000000..87fda06 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/out @@ -0,0 +1,2 @@ +WARNING: trying to get sites list when site ID is set for current connection, result will contain only one site. +{"sites":[{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}],"total_count":1} diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/req_0 new file mode 100644 index 0000000..619b9c4 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/resp_0 new file mode 100644 index 0000000..86a96ed --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_with_site_id/resp_0 @@ -0,0 +1 @@ +{"site":{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}} diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/cmd.tmpl new file mode 100644 index 0000000..62a108c --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} +enapter3 site list --connection my-conn diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/out b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/out new file mode 100644 index 0000000..402bf28 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/out @@ -0,0 +1 @@ +{"sites":[{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}],"total_count":1} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_0 b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_0 new file mode 100644 index 0000000..07fca04 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites?limit=50\u0026offset=0", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_1 b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_1 new file mode 100644 index 0000000..f10ffb3 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/req_1 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites?limit=50\u0026offset=1", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_0 new file mode 100644 index 0000000..5b3b589 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_0 @@ -0,0 +1 @@ +{"sites":[{"id":"7cbdf086-4555-428d-9264-29c3052d71dd"}],"total_count":1} diff --git a/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_1 b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_1 new file mode 100644 index 0000000..d27230f --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/site_list_without_site_id/resp_1 @@ -0,0 +1 @@ +{"sites":[],"total_count":1} diff --git a/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/input b/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/input deleted file mode 100644 index 37f6889..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/input +++ /dev/null @@ -1 +0,0 @@ -{"type":"disconnect","reason":"unauthorized","reconnect":false} diff --git a/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/output b/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/output deleted file mode 100644 index 1c44604..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/disconnect/invalid_token/output +++ /dev/null @@ -1 +0,0 @@ -[connection] disconnected with reason: unauthorized diff --git a/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/input b/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/input deleted file mode 100644 index 3ba1f47..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/input +++ /dev/null @@ -1,5 +0,0 @@ -{"type":"welcome"} - -{"identifier":"{\"channel\":\"RuleChannel\",\"rule_id\":\"SIM-RULE\"}","type":"message","message":{"topic":"error","payload":"Rule not found"}} - -{"identifier":"{\"channel\":\"RuleChannel\",\"rule_id\":\"SIM-RULE\"}","type":"reject_subscription"} diff --git a/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/output b/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/output deleted file mode 100644 index 41e8300..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/disconnect/rule_not_found/output +++ /dev/null @@ -1,3 +0,0 @@ -[connection] welcome -[error] Rule not found -[connection] disconnected diff --git a/internal/app/enaptercli/testdata/rules_logs/simple/input b/internal/app/enaptercli/testdata/rules_logs/simple/input deleted file mode 100644 index b2317d9..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/simple/input +++ /dev/null @@ -1,14 +0,0 @@ -{"type":"welcome"} -{"identifier":"{\"channel\":\"RuleChannel\",\"rule_id\":\"SIM-RULE\"}","type":"confirm_subscription"} - -{"type":"ping","message":"1606485743"} - -{"type":"message","identifier":"{\"channel\":\"RuleChannel\",\"rule_id\":\"SIM-OTHER\"}","message":{"topic":"info","payload":"{\"timestamp\":1606485747,\"status\":\"ok\"}"}} - -{"type":"ping","message":"1606485746"} - -{"type":"message","identifier":"{\"channel\":\"OtherChannel\",\"rule_id\":\"SIM-RULE\"}","message":{"topic":"info","payload":"{\"timestamp\":1606485747,\"status\":\"ok\"}"}} - -{"type":"ping","message":"1606485749"} - -{"type":"message","identifier":"{\"rule_id\":\"SIM-RULE\",\"channel\":\"RuleChannel\"}","message":{"topic":"info","payload":"{\"timestamp\":1606486092,\"status\":\"ok\"}"}} diff --git a/internal/app/enaptercli/testdata/rules_logs/simple/output b/internal/app/enaptercli/testdata/rules_logs/simple/output deleted file mode 100644 index d03a3b3..0000000 --- a/internal/app/enaptercli/testdata/rules_logs/simple/output +++ /dev/null @@ -1,5 +0,0 @@ -[connection] welcome -[connection] confirm_subscription -[read_error] skip message with unknown identifier map[channel:RuleChannel rule_id:SIM-OTHER] -[read_error] skip message with unknown identifier map[channel:OtherChannel rule_id:SIM-RULE] -[info] {"timestamp":1606486092,"status":"ok"} diff --git a/internal/app/enaptercli/testdata/rules_update/errors/output b/internal/app/enaptercli/testdata/rules_update/errors/output deleted file mode 100644 index e64ea7a..0000000 --- a/internal/app/enaptercli/testdata/rules_update/errors/output +++ /dev/null @@ -1,3 +0,0 @@ -[ERROR] hmm... wait a minute -[ERROR] oops! -app exit with error: request execution failed diff --git a/internal/app/enaptercli/testdata/rules_update/errors/requests b/internal/app/enaptercli/testdata/rules_update/errors/requests deleted file mode 100644 index 462e2c2..0000000 --- a/internal/app/enaptercli/testdata/rules_update/errors/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"mutation($input:UpdateInput!){rule{update(input: $input){data{code,message,title},errors{code,message,path,title}}}}","variables":{"input":{"ruleId":"SIM-RULE","luaCode":"-- Rule\n"}}} diff --git a/internal/app/enaptercli/testdata/rules_update/errors/responses b/internal/app/enaptercli/testdata/rules_update/errors/responses deleted file mode 100644 index 2167716..0000000 --- a/internal/app/enaptercli/testdata/rules_update/errors/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"rule":{"update":{"data":null,"errors":[{"code":"warning","message":"hmm... wait a minute","title":"Started"},{"code":"fatal","message":"oops!","title":"Started"}]}}}} diff --git a/internal/app/enaptercli/testdata/rules_update/errors/rule.lua b/internal/app/enaptercli/testdata/rules_update/errors/rule.lua deleted file mode 100644 index 1be38ac..0000000 --- a/internal/app/enaptercli/testdata/rules_update/errors/rule.lua +++ /dev/null @@ -1 +0,0 @@ --- Rule diff --git a/internal/app/enaptercli/testdata/rules_update/errors/settings.json b/internal/app/enaptercli/testdata/rules_update/errors/settings.json deleted file mode 100644 index f429bfc..0000000 --- a/internal/app/enaptercli/testdata/rules_update/errors/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rule_id": "SIM-RULE", - "rule_path": "rule.lua" -} diff --git a/internal/app/enaptercli/testdata/rules_update/simple/output b/internal/app/enaptercli/testdata/rules_update/simple/output deleted file mode 100644 index cdc1f76..0000000 --- a/internal/app/enaptercli/testdata/rules_update/simple/output +++ /dev/null @@ -1 +0,0 @@ -Rule successfully updated diff --git a/internal/app/enaptercli/testdata/rules_update/simple/requests b/internal/app/enaptercli/testdata/rules_update/simple/requests deleted file mode 100644 index 462e2c2..0000000 --- a/internal/app/enaptercli/testdata/rules_update/simple/requests +++ /dev/null @@ -1 +0,0 @@ -{"query":"mutation($input:UpdateInput!){rule{update(input: $input){data{code,message,title},errors{code,message,path,title}}}}","variables":{"input":{"ruleId":"SIM-RULE","luaCode":"-- Rule\n"}}} diff --git a/internal/app/enaptercli/testdata/rules_update/simple/responses b/internal/app/enaptercli/testdata/rules_update/simple/responses deleted file mode 100644 index 48b742f..0000000 --- a/internal/app/enaptercli/testdata/rules_update/simple/responses +++ /dev/null @@ -1 +0,0 @@ -{"data":{"rule":{"update":{"data":{"message":"Rule successfully updated"}}}}} diff --git a/internal/app/enaptercli/testdata/rules_update/simple/rule.lua b/internal/app/enaptercli/testdata/rules_update/simple/rule.lua deleted file mode 100644 index 1be38ac..0000000 --- a/internal/app/enaptercli/testdata/rules_update/simple/rule.lua +++ /dev/null @@ -1 +0,0 @@ --- Rule diff --git a/internal/app/enaptercli/testdata/rules_update/simple/settings.json b/internal/app/enaptercli/testdata/rules_update/simple/settings.json deleted file mode 100644 index f429bfc..0000000 --- a/internal/app/enaptercli/testdata/rules_update/simple/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rule_id": "SIM-RULE", - "rule_path": "rule.lua" -} diff --git a/internal/cloudapi/client.go b/internal/cloudapi/client.go deleted file mode 100644 index b1c8a22..0000000 --- a/internal/cloudapi/client.go +++ /dev/null @@ -1,76 +0,0 @@ -package cloudapi - -import ( - "fmt" - "io" - "net/http" - - "github.com/shurcooL/graphql" -) - -type Client struct { - client *graphql.Client -} - -func NewClientWithURL(httpClient *http.Client, host string) *Client { - if httpClient == nil { - httpClient = http.DefaultClient - } - return &Client{ - client: graphql.NewClient(host, httpClient), - } -} - -type CredentialsTransport struct { - tripper http.RoundTripper - token string - version string -} - -func NewCredentialsTransport(t http.RoundTripper, token, version string) http.RoundTripper { - return CredentialsTransport{ - tripper: t, - token: token, - version: version, - } -} - -func (t CredentialsTransport) RoundTrip(r *http.Request) (*http.Response, error) { - newReq := new(http.Request) - *newReq = *r - - newReq.Header = make(http.Header, len(r.Header)) - for k, s := range r.Header { - newReq.Header[k] = s - } - - newReq.Header.Set("Authorization", "Bearer "+t.token) - newReq.Header.Set("X-ENAPTER-CLI-VERSION", t.version) - - return t.tripper.RoundTrip(newReq) -} - -type CLIMessageWriterTransport struct { - tripper http.RoundTripper - writer io.Writer -} - -func NewCLIMessageWriterTransport(t http.RoundTripper, w io.Writer) http.RoundTripper { - return CLIMessageWriterTransport{ - tripper: t, - writer: w, - } -} - -func (t CLIMessageWriterTransport) RoundTrip(r *http.Request) (*http.Response, error) { - resp, err := t.tripper.RoundTrip(r) - if err != nil { - return nil, err - } - - if msg := resp.Header.Get("X-ENAPTER-CLI-MESSAGE"); msg != "" { - fmt.Fprintln(t.writer, msg) - } - - return resp, nil -} diff --git a/internal/cloudapi/devices.go b/internal/cloudapi/devices.go deleted file mode 100644 index 0df313d..0000000 --- a/internal/cloudapi/devices.go +++ /dev/null @@ -1,172 +0,0 @@ -package cloudapi - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/shurcooL/graphql" -) - -type UploadBlueprintData struct { - Code string - Message string - Title string - OperationID string -} - -type UploadBlueprintError struct { - Code string - Message string - Path []string - Title string -} - -type uploadBlueprintMutation struct { - Device struct { - UploadBlueprint struct { - Data UploadBlueprintData - Errors []UploadBlueprintError - } `graphql:"uploadBlueprint(input: $input)"` - } -} - -func (c *Client) UploadBlueprint( - ctx context.Context, hardwareID string, blueprint []byte, -) (UploadBlueprintData, []UploadBlueprintError, error) { - type UploadBlueprintInput struct { - Blueprint graphql.String `json:"blueprint"` - HardwareID graphql.ID `json:"hardwareId"` - } - - variables := map[string]interface{}{ - "input": UploadBlueprintInput{ - Blueprint: graphql.String(blueprint), - HardwareID: graphql.String(hardwareID), - }, - } - - var mutation uploadBlueprintMutation - if err := c.client.Mutate(ctx, &mutation, variables); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = ErrRequestTimedOut - } - return UploadBlueprintData{}, nil, fmt.Errorf("mutate: %w", err) - } - - uploadInfo := mutation.Device.UploadBlueprint - return uploadInfo.Data, uploadInfo.Errors, nil -} - -type OperationLog struct { - Payload string - CreatedAt string - Severity string -} - -type blueprintUpdateOperationQuery struct { - Device *struct { - BlueprintUpdateOperation *struct { - Status string - Logs struct { - Edges []struct { - Cursor graphql.String - Node OperationLog - } - } `graphql:"logs(after: $after_cursor)"` - } `graphql:"blueprintUpdateOperation(id: $operation_id)"` - } `graphql:"device(hardwareId: $hardware_id)"` -} - -func (c *Client) WriteOperationLogs( - ctx context.Context, hardwareID, operationID string, - writeLog func(operationID string, log OperationLog), -) error { - v := map[string]interface{}{ - "after_cursor": graphql.String(""), - "hardware_id": hardwareID, - "operation_id": operationID, - } - - for { - var q blueprintUpdateOperationQuery - if err := c.client.Query(ctx, &q, v); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = ErrRequestTimedOut - } - return fmt.Errorf("failed to send request: %w", err) - } - - if q.Device == nil { - return fmt.Errorf("%w: device not found", ErrFinishedWithError) - } - - if q.Device.BlueprintUpdateOperation == nil { - return fmt.Errorf("%w: operation not found", ErrFinishedWithError) - } - - status := q.Device.BlueprintUpdateOperation.Status - logs := q.Device.BlueprintUpdateOperation.Logs - - for _, e := range logs.Edges { - v["after_cursor"] = e.Cursor - writeLog(operationID, e.Node) - } - - if len(logs.Edges) == 0 { - switch status { - case "SUCCEEDED": - return nil - case "ERROR": - return ErrLogStatusError - } - } - - const logRequestPeriod = 100 * time.Millisecond - time.Sleep(logRequestPeriod) - } -} - -type blueprintUpdateOperationsQuery struct { - Device *struct { - BlueprintUpdateOperations struct { - Nodes []struct { - ID string - } - } `graphql:"blueprintUpdateOperations(last: $last_int)"` - } `graphql:"device(hardwareId: $hardware_id)"` -} - -func (c *Client) WriteLastOperationsLogs( - ctx context.Context, hardwareID string, lastOperationsNumber int, - writeLog func(operationID string, log OperationLog), -) error { - v := map[string]interface{}{ - "hardware_id": hardwareID, - "last_int": graphql.Int(lastOperationsNumber), - } - - var q blueprintUpdateOperationsQuery - if err := c.client.Query(ctx, &q, v); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = ErrRequestTimedOut - } - return fmt.Errorf("failed to send request: %w", err) - } - - if q.Device == nil { - return fmt.Errorf("%w: device not found", ErrFinishedWithError) - } - - for _, op := range q.Device.BlueprintUpdateOperations.Nodes { - if err := c.WriteOperationLogs(ctx, hardwareID, op.ID, writeLog); err != nil { - if errors.Is(err, ErrLogStatusError) { - continue - } - return err - } - } - - return nil -} diff --git a/internal/cloudapi/errors.go b/internal/cloudapi/errors.go deleted file mode 100644 index 559c7a6..0000000 --- a/internal/cloudapi/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package cloudapi - -import "errors" - -var ( - ErrFinishedWithError = errors.New("request execution failed") - ErrLogStatusError = errors.New("error during request execution") - ErrRequestTimedOut = errors.New("request timed out") - ErrFinished = errors.New("finished") -) diff --git a/internal/cloudapi/logs_writer.go b/internal/cloudapi/logs_writer.go deleted file mode 100644 index 4ae69ee..0000000 --- a/internal/cloudapi/logs_writer.go +++ /dev/null @@ -1,272 +0,0 @@ -package cloudapi - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/url" - "sync" - - "github.com/gorilla/websocket" -) - -const ( - fieldChannel = "channel" - fieldHardwareID = "hardware_id" - fieldRuleID = "rule_id" -) - -type LogsWriter struct { - url string - identifier map[string]string - writeLog func(topic, message string) - wsConnMu sync.Mutex - wsConn *websocket.Conn -} - -func NewDeviceLogsWriter( - host, token, apiVersion, hardwareID string, - writer func(topic, message string), -) (*LogsWriter, error) { - identifier := map[string]string{ - fieldChannel: "DeviceChannel", - fieldHardwareID: hardwareID, - } - return newLogsWriter(host, token, apiVersion, identifier, writer) -} - -func NewRuleLogsWriter( - host, token, apiVersion, ruleID string, - writer func(topic, message string), -) (*LogsWriter, error) { - identifier := map[string]string{ - fieldChannel: "RuleChannel", - fieldRuleID: ruleID, - } - return newLogsWriter(host, token, apiVersion, identifier, writer) -} - -func newLogsWriter( - host, token, apiVersion string, identifier map[string]string, - writer func(topic, message string), -) (*LogsWriter, error) { - u, err := url.Parse(host) - if err != nil { - return nil, fmt.Errorf("parse url: %w", err) - } - - q := u.Query() - q.Set("token", token) - q.Set("enapter_api_version", apiVersion) - u.RawQuery = q.Encode() - - return &LogsWriter{ - url: u.String(), - identifier: identifier, - writeLog: writer, - }, nil -} - -func (l *LogsWriter) Run(ctx context.Context) error { - defer l.close() - - go func() { - <-ctx.Done() - l.close() - }() - - for { - select { - case <-ctx.Done(): - return nil - default: - } - - if err := l.connect(ctx); err != nil { - l.writeLog("connection", fmt.Sprintf("failed to connect: %s", err.Error())) - continue - } - - if err := l.subscribe(); err != nil { - l.writeLog("connection", fmt.Sprintf("failed to subscribe: %s", err.Error())) - continue - } - - if err := l.readAndWriteLogs(ctx); err != nil { - if errors.Is(err, ErrFinished) { - return nil - } - l.writeLog("read_error", fmt.Sprintf("failed to read msg: %s", err.Error())) - return err - } - } -} - -func (l *LogsWriter) connect(ctx context.Context) error { - wsConn, resp, err := websocket.DefaultDialer.DialContext(ctx, l.url, nil) - if err != nil { - return fmt.Errorf("websockets dial: %w", err) - } - defer resp.Body.Close() - - l.wsConnMu.Lock() - defer l.wsConnMu.Unlock() - l.wsConn = wsConn - - return nil -} - -func (l *LogsWriter) close() { - l.wsConnMu.Lock() - defer l.wsConnMu.Unlock() - - if l.wsConn != nil { - l.wsConn.Close() - } -} - -func (l *LogsWriter) subscribe() error { - identifierBytes, err := json.Marshal(l.identifier) - if err != nil { - return fmt.Errorf("failed to marshal sm: %w", err) - } - - msg := struct { - Command string `json:"command"` - Identifier string `json:"identifier"` - }{ - Command: "subscribe", - Identifier: string(identifierBytes), - } - - msgBytes, err := json.Marshal(&msg) - if err != nil { - return fmt.Errorf("failed to marshal m: %w", err) - } - - err = l.wsConn.WriteMessage(websocket.TextMessage, msgBytes) - if err != nil { - return fmt.Errorf("failed to subscribe: %w", err) - } - - return nil -} - -func (l *LogsWriter) readAndWriteLogs(ctx context.Context) error { - for { - msgType, msgBytes, err := l.wsConn.ReadMessage() - select { - case <-ctx.Done(): - return nil - default: - } - - if err != nil { - return err - } - - if msgType != websocket.TextMessage { - l.writeLog("read_error", - fmt.Sprintf("skip unsupported message type [%d] (only text type [%d] supported)", - msgType, websocket.TextMessage)) - continue - } - - if err := l.process(msgBytes); err != nil { - return err - } - } -} - -type logsBaseMessage struct { - Type string `json:"type"` - Identifier string `json:"identifier"` - Message json.RawMessage `json:"message"` -} - -type logsMessage struct { - Topic string `json:"topic"` - Payload string `json:"payload"` -} - -type disconnectMessage struct { - Type string `json:"type"` - Reason string `json:"reason"` - Reconnect bool `json:"reconnect"` -} - -func (l *LogsWriter) process(msgBytes []byte) error { - baseMsg := logsBaseMessage{} - if err := json.Unmarshal(msgBytes, &baseMsg); err != nil { - errMsg := fmt.Sprintf("skip invalid message %s: %s", string(msgBytes), err.Error()) - l.writeLog("read_error", errMsg) - return err - } - - switch baseMsg.Type { - case "ping": - case "welcome", "confirm_subscription": - l.writeLog("connection", baseMsg.Type) - case "reject_subscription": - l.writeLog("connection", "disconnected") - return ErrFinished - case "disconnect": - return l.processDisconnect(msgBytes) - case "message": - l.processMessage(baseMsg) - default: - l.writeLog("unknown", string(msgBytes)) - } - - return nil -} - -func (l *LogsWriter) processDisconnect(msgBytes []byte) error { - var msg disconnectMessage - if err := json.Unmarshal(msgBytes, &msg); err != nil { - return nil - } - if msg.Reconnect { - l.writeLog("connection", - fmt.Sprintf("disconnected with reason: %s. Reconnecting...", msg.Reason)) - return nil - } - l.writeLog("connection", - fmt.Sprintf("disconnected with reason: %s", msg.Reason)) - return ErrFinished -} - -func (l *LogsWriter) processMessage(baseMsg logsBaseMessage) { - var identifier map[string]string - if err := json.Unmarshal([]byte(baseMsg.Identifier), &identifier); err != nil { - l.writeLog("read_error", - fmt.Sprintf("skip message with invalid identifier %s: %s", baseMsg.Identifier, err.Error())) - return - } - if !mapsEqual(l.identifier, identifier) { - l.writeLog("read_error", - fmt.Sprintf("skip message with unknown identifier %+v", identifier)) - return - } - - var msg logsMessage - if err := json.Unmarshal(baseMsg.Message, &msg); err != nil { - l.writeLog("read_error", - fmt.Sprintf("skip invalid log message %s: %s", string(baseMsg.Message), err.Error())) - return - } - l.writeLog(msg.Topic, msg.Payload) -} - -func mapsEqual(m1, m2 map[string]string) bool { - if len(m1) != len(m2) { - return false - } - for k, v1 := range m1 { - if v2, ok := m2[k]; !ok || v1 != v2 { - return false - } - } - return true -} diff --git a/internal/cloudapi/rules.go b/internal/cloudapi/rules.go deleted file mode 100644 index bf1b42e..0000000 --- a/internal/cloudapi/rules.go +++ /dev/null @@ -1,67 +0,0 @@ -package cloudapi - -import ( - "context" - "errors" - "fmt" - - "github.com/shurcooL/graphql" -) - -type UpdateRuleInput struct { - RuleID string - LuaCode string - StdlibVersion string - ExecutionInterval int -} - -type UpdateRuleData struct { - Code string - Message string - Title string -} - -type UpdateRuleError struct { - Code string - Message string - Path []string - Title string -} - -func (c *Client) UpdateRule( - ctx context.Context, input UpdateRuleInput, -) (UpdateRuleData, []UpdateRuleError, error) { - var mutation struct { - Rule struct { - Update struct { - Data UpdateRuleData - Errors []UpdateRuleError - } `graphql:"update(input: $input)"` - } - } - - type UpdateInput struct { - RuleID graphql.String `json:"ruleId"` - LuaCode graphql.String `json:"luaCode"` - StdlibVersion graphql.String `json:"stdlibVersion,omitempty"` - ExecutionInterval graphql.Int `json:"executionInterval,omitempty"` - } - - variables := map[string]interface{}{ - "input": UpdateInput{ - RuleID: graphql.String(input.RuleID), - LuaCode: graphql.String(input.LuaCode), - StdlibVersion: graphql.String(input.StdlibVersion), - ExecutionInterval: graphql.Int(input.ExecutionInterval), - }, - } - - if err := c.client.Mutate(ctx, &mutation, variables); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = ErrRequestTimedOut - } - return UpdateRuleData{}, nil, fmt.Errorf("mutate: %w", err) - } - - return mutation.Rule.Update.Data, mutation.Rule.Update.Errors, nil -} diff --git a/internal/publichttp/client.go b/internal/publichttp/client.go deleted file mode 100644 index effa227..0000000 --- a/internal/publichttp/client.go +++ /dev/null @@ -1,105 +0,0 @@ -package publichttp - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" -) - -const defaultBaseURL = "https://api.enapter.com" - -type Client struct { - baseURL *url.URL - client *http.Client - - // Services used for talking to different parts of the Enapter API. - Commands CommandsAPI -} - -func NewClient(httpClient *http.Client) *Client { - c, err := NewClientWithURL(httpClient, defaultBaseURL) - if err != nil { - panic(err) - } - return c -} - -func NewClientWithURL(httpClient *http.Client, baseURL string) (*Client, error) { - if httpClient == nil { - httpClient = http.DefaultClient - } - - u, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - c := &Client{baseURL: u, client: httpClient} - c.Commands = CommandsAPI{client: c} - return c, nil -} - -func (c *Client) NewRequest(method, path string, body io.Reader) (*http.Request, error) { - return c.NewRequestWithContext(context.Background(), method, path, body) -} - -func (c *Client) NewRequestWithContext( - ctx context.Context, method, path string, body io.Reader, -) (*http.Request, error) { - u, err := c.baseURL.Parse(path) - if err != nil { - return nil, fmt.Errorf("parse url: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, method, u.String(), body) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - return req, err -} - -func (c *Client) Do(req *http.Request) (*http.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode >= http.StatusMultipleChoices { - defer resp.Body.Close() - - responseError, err := c.parseResponseError(resp) - if err != nil { - return nil, err - } - return nil, responseError - } - - return resp, nil -} - -func (c *Client) parseResponseError(r *http.Response) (ResponseError, error) { - var errors ResponseError - - if r.Body != http.NoBody { - if err := json.NewDecoder(r.Body).Decode(&errors); err != nil { - return ResponseError{}, fmt.Errorf("unmarshal body: %w", err) - } - } - - errors.StatusCode = r.StatusCode - if retryAfter := r.Header.Get("Retry-After"); retryAfter != "" { - duration, err := strconv.Atoi(retryAfter) - if err != nil { - return ResponseError{}, fmt.Errorf("parse Retry-After: %w", err) - } - errors.RetryAfter = time.Duration(duration) * time.Second - } - - return errors, nil -} diff --git a/internal/publichttp/commands.go b/internal/publichttp/commands.go deleted file mode 100644 index 24eae76..0000000 --- a/internal/publichttp/commands.go +++ /dev/null @@ -1,114 +0,0 @@ -package publichttp - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" -) - -type CommandsAPI struct { - client *Client -} - -type CommandQuery struct { - HardwareID string `json:"hardware_id"` - CommandName string `json:"command_name"` - Arguments map[string]interface{} `json:"arguments"` -} - -type CommandResponse struct { - State CommandState `json:"state"` - Payload map[string]interface{} `json:"payload,omitempty"` -} - -type CommandState string - -const ( - CommandSucceeded CommandState = "succeeded" - CommandError CommandState = "error" - CommandPlatformError CommandState = "platform_error" - CommandStarted CommandState = "started" - CommandInProgress CommandState = "device_in_progress" -) - -func (c *CommandsAPI) Execute( - ctx context.Context, query CommandQuery, -) (CommandResponse, error) { - resp, err := c.execute(ctx, query, false) - if err != nil { - return CommandResponse{}, err - } - defer resp.Body.Close() - - var cmdResp CommandResponse - if err := json.NewDecoder(resp.Body).Decode(&cmdResp); err != nil { - return CommandResponse{}, fmt.Errorf("unmarshal response: %w", err) - } - - return cmdResp, nil -} - -type CommandProgress struct { - CommandResponse - Error error -} - -func (c *CommandsAPI) ExecuteWithProgress( - ctx context.Context, query CommandQuery, -) (<-chan CommandProgress, error) { - //nolint:bodyclose // closed in the reading goroutine - resp, err := c.execute(ctx, query, true) - if err != nil { - return nil, err - } - - progressCh := make(chan CommandProgress) - go func() { - defer resp.Body.Close() - defer close(progressCh) - - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - var p CommandProgress - p.Error = json.Unmarshal(scanner.Bytes(), &p.CommandResponse) - - select { - case <-ctx.Done(): - return - case progressCh <- p: - } - } - }() - - return progressCh, nil -} - -func (c *CommandsAPI) execute( - ctx context.Context, query CommandQuery, showProgress bool, -) (*http.Response, error) { - queryBody := new(bytes.Buffer) - if err := json.NewEncoder(queryBody).Encode(query); err != nil { - return nil, fmt.Errorf("marshal body: %w", err) - } - - const path = "/commands/v1/execute" - req, err := c.client.NewRequestWithContext(ctx, http.MethodPost, path, queryBody) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - values := req.URL.Query() - values.Set("show_progress", strconv.FormatBool(showProgress)) - req.URL.RawQuery = values.Encode() - - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - return resp, nil -} diff --git a/internal/publichttp/errors.go b/internal/publichttp/errors.go deleted file mode 100644 index 297992b..0000000 --- a/internal/publichttp/errors.go +++ /dev/null @@ -1,30 +0,0 @@ -package publichttp - -import ( - "fmt" - "strings" - "time" -) - -type ResponseError struct { - Errors []Error `json:"errors"` - StatusCode int `json:"-"` - RetryAfter time.Duration `json:"-"` -} - -func (r ResponseError) Error() string { - var builder strings.Builder - for i, e := range r.Errors { - if i > 0 { - builder.WriteByte('\n') - } - builder.WriteString(fmt.Sprintf("%v: %v", e.Code, e.Message)) - } - return builder.String() -} - -type Error struct { - Code string `json:"code"` - Message string `json:"message"` - Details map[string]interface{} `json:"details,omitempty"` -} diff --git a/internal/publichttp/transport.go b/internal/publichttp/transport.go deleted file mode 100644 index d5892fd..0000000 --- a/internal/publichttp/transport.go +++ /dev/null @@ -1,28 +0,0 @@ -package publichttp - -import "net/http" - -type AuthTokenTransport struct { - token string - next http.RoundTripper -} - -func NewAuthTokenTransport(t http.RoundTripper, token string) http.RoundTripper { - return &AuthTokenTransport{token: token, next: t} -} - -func (t *AuthTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { - const header = "X-Enapter-Auth-Token" - s := cloneRequest(req) - s.Header.Set(header, t.token) - return t.next.RoundTrip(s) -} - -func cloneRequest(req *http.Request) *http.Request { - shallow := new(http.Request) - *shallow = *req - for k, s := range req.Header { - shallow.Header[k] = s - } - return shallow -}