NOISSUE - Add Magistrala CLI (#40)

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Felix Gateru
2025-01-14 16:08:19 +03:00
committed by GitHub
parent 96158cdc8e
commit 0bcb846e86
17 changed files with 2484 additions and 5 deletions
+10
View File
@@ -68,6 +68,11 @@ jobs:
- "pkg/sdk/**"
- "pkg/events/**"
cli:
- "cli/**"
- "cmd/cli/**"
- "pkg/sdk/**"
consumers:
- "consumers/**"
- "cmd/postgres-writer/**"
@@ -109,6 +114,11 @@ jobs:
run: |
go test --race -v -count=1 -coverprofile=coverage/bootstrap.out ./bootstrap/...
- name: Run cli tests
if: steps.changes.outputs.cli == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/cli.out ./cli/...
- name: Run consumers tests
if: steps.changes.outputs.consumers == 'true' || steps.changes.outputs.workflow == 'true'
run: |
+1 -1
View File
@@ -3,7 +3,7 @@
MG_DOCKER_IMAGE_NAME_PREFIX ?= ghcr.io/absmach/magistrala
BUILD_DIR = build
SERVICES = bootstrap provision re postgres-writer postgres-reader timescale-writer timescale-reader
SERVICES = bootstrap provision re postgres-writer postgres-reader timescale-writer timescale-reader cli
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
+216
View File
@@ -0,0 +1,216 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdBootstrap = []cobra.Command{
{
Use: "create <JSON_config> <domain_id> <user_auth_token>",
Short: "Create config",
Long: `Create new Client Bootstrap Config to the user identified by the provided key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
id, err := sdk.AddBootstrap(cfg, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logCreatedCmd(*cmd, id)
},
},
{
Use: "get [all | <client_id>] <domain_id> <user_auth_token>",
Short: "Get config",
Long: `Get Client Config with given ID belonging to the user identified by the given key.
all - lists all config
<client_id> - view config of <client_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
State: State,
Name: Name,
}
if args[0] == "all" {
l, err := sdk.Bootstraps(pageMetadata, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, l)
return
}
c, err := sdk.ViewBootstrap(args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "update [config <JSON_config> | connection <id> <channel_ids> | certs <id> <client_cert> <client_key> <ca> ] <domain_id> <user_auth_token>",
Short: "Update config",
Long: `Updates editable fields of the provided Config.
config <JSON_config> - Updates editable fields of the provided Config.
connection <id> <channel_ids> - Updates connections performs update of the channel list corresponding Client is connected to.
channel_ids - '["channel_id1", ...]'
certs <id> <client_cert> <client_key> <ca> - Update bootstrap config certificates.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "config" {
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[1]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrap(cfg, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "connection" {
var ids []string
if err := json.Unmarshal([]byte(args[2]), &ids); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrapConnection(args[1], ids, args[3], args[4]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "certs" {
cfg, err := sdk.UpdateBootstrapCerts(args[0], args[1], args[2], args[3], args[4], args[5])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cfg)
return
}
logUsageCmd(*cmd, cmd.Use)
},
},
{
Use: "remove <client_id> <domain_id> <user_auth_token>",
Short: "Remove config",
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.RemoveBootstrap(args[0], args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "bootstrap [<external_id> <external_key> | secure <external_id> <external_key> <crypto_key> ]",
Short: "Bootstrap config",
Long: `Returns Config to the Client with provided external ID using external key.
secure - Retrieves a configuration with given external ID and encrypted external key.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "secure" {
c, err := sdk.BootstrapSecure(args[1], args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
return
}
c, err := sdk.Bootstrap(args[0], args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "whitelist <JSON_config> <domain_id> <user_auth_token>",
Short: "Whitelist config",
Long: `Whitelist updates client state config with given id from the authenticated user`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.Whitelist(cfg.ClientID, cfg.State, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
}
// NewBootstrapCmd returns bootstrap command.
func NewBootstrapCmd() *cobra.Command {
cmd := cobra.Command{
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist]",
Short: "Bootstrap management",
Long: `Bootstrap management: create, get, update, delete or whitelist Bootstrap config`,
}
for i := range cmdBootstrap {
cmd.AddCommand(&cmdBootstrap[i])
}
return &cmd
}
+633
View File
@@ -0,0 +1,633 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/absmach/magistrala/cli"
"github.com/absmach/magistrala/internal/testsutil"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
clientID = testsutil.GenerateUUID(&testing.T{})
channelID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
bootConfig = mgsdk.BootstrapConfig{
ClientID: clientID,
Channels: []string{channelID},
Name: "Test Bootstrap",
ExternalID: "09:6:0:sb:sa",
ExternalKey: "key",
}
validToken = "validToken"
invalidToken = "invalidToken"
extraArg = "extra-arg"
invalidID = "invalidID"
all = "all"
)
func TestCreateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]}", clientID, "Test Bootstrap", channelID)
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]", clientID, "Test Bootdtrap", channelID)
cases := []struct {
desc string
args []string
logType outputLog
response string
sdkErr errors.SDKError
errLogMessage string
id string
}{
{
desc: "create bootstrap config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: createLog,
id: clientID,
response: fmt.Sprintf("\ncreated: %s\n\n", clientID),
},
{
desc: "create bootstrap config with invald args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "create bootstrap config with invald json",
args: []string{
invalidJson,
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "create bootstrap config with invald token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("AddBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.id, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
switch tc.logType {
case createLog:
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestGetBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
var page mgsdk.BootstrapPage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
page mgsdk.BootstrapPage
boot mgsdk.BootstrapConfig
logType outputLog
errLogMessage string
}{
{
desc: "get all bootstrap config successfully",
args: []string{
all,
domainID,
validToken,
},
page: mgsdk.BootstrapPage{
PageRes: mgsdk.PageRes{
Total: 1,
Offset: 0,
Limit: 10,
},
Configs: []mgsdk.BootstrapConfig{bootConfig},
},
logType: entityLog,
},
{
desc: "get bootstrap config with id",
args: []string{
channelID,
domainID,
validToken,
},
logType: entityLog,
boot: bootConfig,
},
{
desc: "get bootstrap config with invalid args",
args: []string{
all,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "get all bootstrap config with invalid token",
args: []string{
all,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "get bootstrap config with invalid id",
args: []string{
invalidID,
domainID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstraps", mock.Anything, tc.args[1], tc.args[2]).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[0] == all {
err := json.Unmarshal([]byte(out), &page)
assert.Nil(t, err)
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
} else {
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.boot, boot))
}
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
func TestRemoveBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "remove bootstrap config successfully",
args: []string{
clientID,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "remove bootstrap config with invalid args",
args: []string{
clientID,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "remove bootstrap config with invalid client id",
args: []string{
invalidID,
domainID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
{
desc: "remove bootstrap config with invalid token",
args: []string{
clientID,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("RemoveBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestUpdateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
config := "config"
connection := "connection"
newConfigJson := "{\"name\" : \"New Bootstrap\"}"
chanIDsJson := fmt.Sprintf("[\"%s\"]", channelID)
cases := []struct {
desc string
args []string
boot mgsdk.BootstrapConfig
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "update bootstrap config successfully",
args: []string{
config,
newConfigJson,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "update bootstrap config with invalid token",
args: []string{
config,
newConfigJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap connections successfully",
args: []string{
connection,
clientID,
chanIDsJson,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "update bootstrap connections with invalid json",
args: []string{
connection,
clientID,
fmt.Sprintf("[\"%s\"", clientID),
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap connections with invalid token",
args: []string{
connection,
clientID,
chanIDsJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap certs successfully",
args: []string{
"certs",
clientID,
"client cert",
"client key",
"ca",
domainID,
validToken,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "update bootstrap certs with invalid token",
args: []string{
"certs",
clientID,
"client cert",
"client key",
"ca",
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap config with invalid args",
args: []string{
newConfigJson,
domainID,
validToken,
},
logType: usageLog,
},
{
desc: "update bootstrap config with invalid json",
args: []string{
config,
"{\"name\" : \"New Bootstrap\"",
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap with invalid args",
args: []string{
extraArg,
extraArg,
extraArg,
extraArg,
extraArg,
},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var boot mgsdk.BootstrapConfig
sdkCall := sdkMock.On("UpdateBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall1 := sdkMock.On("UpdateBootstrapConnection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall2 := sdkMock.On("UpdateBootstrapCerts", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{updCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
sdkCall2.Unset()
})
}
}
func TestWhitelistConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d}", clientID, 1)
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
}{
{
desc: "whitelist config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "whitelist config with invalid args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "whitelist config with invalid json",
args: []string{
fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d", clientID, 1),
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "whitelist config with invalid token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("Whitelist", mock.Anything, mock.Anything, tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{whitelistCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func TestBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
crptoKey := "v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp"
invalidKey := "invalid key"
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
boot mgsdk.BootstrapConfig
}{
{
desc: "bootstrap secure config successfully",
args: []string{
"secure",
bootConfig.ExternalID,
bootConfig.ExternalKey,
crptoKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap config successfully",
args: []string{
bootConfig.ExternalID,
bootConfig.ExternalKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap secure config with invalid args",
args: []string{
crptoKey,
},
logType: usageLog,
},
{
desc: "bootstrap secure config with invalid key",
args: []string{
"secure",
bootConfig.ExternalID,
invalidKey,
crptoKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
{
desc: "bootstrap config with invalid key",
args: []string{
bootConfig.ExternalID,
invalidKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("BootstrapSecure", mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstrap", mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{bootStrapCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
// CRUD and common commands
const (
createCmd = "create"
updateCmd = "update"
getCmd = "get"
enableCmd = "enable"
disableCmd = "disable"
updCmd = "update"
delCmd = "delete"
rmCmd = "remove"
whitelistCmd = "whitelist"
bootStrapCmd = "bootstrap"
)
+319
View File
@@ -0,0 +1,319 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"io"
"net/url"
"os"
"reflect"
"strconv"
"strings"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
"github.com/absmach/supermq/pkg/errors"
"github.com/pelletier/go-toml"
"github.com/spf13/cobra"
)
const (
defURL string = "http://localhost"
defGroupsURL string = defURL + ":9004"
defUsersURL string = defURL + ":9002"
defChannelsURL string = defURL + ":9005"
defClientsURL string = defURL + ":9006"
defReaderURL string = defURL + ":9011"
defBootstrapURL string = defURL + ":9013"
defDomainsURL string = defURL + ":9003"
defCertsURL string = defURL + ":9019"
defInvitationsURL string = defURL + ":9020"
defHTTPURL string = defURL + ":8008"
defJournalURL string = defURL + ":9021"
defTLSVerification bool = false
defOffset string = "0"
defLimit string = "10"
defTopic string = ""
defRawOutput string = "false"
)
type remotes struct {
ChannelsURL string `toml:"channels_url"`
ClientsURL string `toml:"clients_url"`
GroupsURL string `toml:"groups_url"`
UsersURL string `toml:"users_url"`
ReaderURL string `toml:"reader_url"`
DomainsURL string `toml:"domains_url"`
HTTPAdapterURL string `toml:"http_adapter_url"`
BootstrapURL string `toml:"bootstrap_url"`
CertsURL string `toml:"certs_url"`
InvitationsURL string `toml:"invitations_url"`
JournalURL string `toml:"journal_url"`
HostURL string `toml:"host_url"`
TLSVerification bool `toml:"tls_verification"`
}
type filter struct {
Offset string `toml:"offset"`
Limit string `toml:"limit"`
Topic string `toml:"topic"`
}
type config struct {
Remotes remotes `toml:"remotes"`
Filter filter `toml:"filter"`
UserToken string `toml:"user_token"`
RawOutput string `toml:"raw_output"`
}
// Readable by all user groups but writeable by the user only.
const filePermission = 0o644
var (
errReadFail = errors.New("failed to read config file")
errNoKey = errors.New("no such key")
errUnsupportedKeyValue = errors.New("unsupported data type for key")
errWritingConfig = errors.New("error in writing the updated config to file")
errInvalidURL = errors.New("invalid url")
errURLParseFail = errors.New("failed to parse url")
defaultConfigPath = "./config.toml"
)
func read(file string) (config, error) {
c := config{}
data, err := os.Open(file)
if err != nil {
return c, errors.Wrap(errReadFail, err)
}
defer data.Close()
buf, err := io.ReadAll(data)
if err != nil {
return c, errors.Wrap(errReadFail, err)
}
if err := toml.Unmarshal(buf, &c); err != nil {
return config{}, err
}
return c, nil
}
// ParseConfig - parses the config file.
func ParseConfig(sdkConf mgsdk.Config) (mgsdk.Config, error) {
if ConfigPath == "" {
ConfigPath = defaultConfigPath
}
_, err := os.Stat(ConfigPath)
switch {
// If the file does not exist, create it with default values.
case os.IsNotExist(err):
defaultConfig := config{
Remotes: remotes{
ChannelsURL: defChannelsURL,
ClientsURL: defClientsURL,
GroupsURL: defGroupsURL,
UsersURL: defUsersURL,
ReaderURL: defReaderURL,
DomainsURL: defDomainsURL,
HTTPAdapterURL: defHTTPURL,
BootstrapURL: defBootstrapURL,
CertsURL: defCertsURL,
InvitationsURL: defInvitationsURL,
JournalURL: defJournalURL,
HostURL: defURL,
TLSVerification: defTLSVerification,
},
Filter: filter{
Offset: defOffset,
Limit: defLimit,
Topic: defTopic,
},
RawOutput: defRawOutput,
}
buf, err := toml.Marshal(defaultConfig)
if err != nil {
return sdkConf, err
}
if err = os.WriteFile(ConfigPath, buf, filePermission); err != nil {
return sdkConf, errors.Wrap(errWritingConfig, err)
}
case err != nil:
return sdkConf, err
}
config, err := read(ConfigPath)
if err != nil {
return sdkConf, err
}
if config.Filter.Offset != "" && Offset == 0 {
offset, err := strconv.ParseUint(config.Filter.Offset, 10, 64)
if err != nil {
return sdkConf, err
}
Offset = offset
}
if config.Filter.Limit != "" && Limit == 0 {
limit, err := strconv.ParseUint(config.Filter.Limit, 10, 64)
if err != nil {
return sdkConf, err
}
Limit = limit
}
if config.Filter.Topic != "" && Topic == "" {
Topic = config.Filter.Topic
}
if config.RawOutput != "" {
rawOutput, err := strconv.ParseBool(config.RawOutput)
if err != nil {
return sdkConf, err
}
// check for config file value or flag input value is true
RawOutput = rawOutput || RawOutput
}
if sdkConf.ClientsURL == "" && config.Remotes.ClientsURL != "" {
sdkConf.ClientsURL = config.Remotes.ClientsURL
}
if sdkConf.UsersURL == "" && config.Remotes.UsersURL != "" {
sdkConf.UsersURL = config.Remotes.UsersURL
}
if sdkConf.ReaderURL == "" && config.Remotes.ReaderURL != "" {
sdkConf.ReaderURL = config.Remotes.ReaderURL
}
if sdkConf.DomainsURL == "" && config.Remotes.DomainsURL != "" {
sdkConf.DomainsURL = config.Remotes.DomainsURL
}
if sdkConf.HTTPAdapterURL == "" && config.Remotes.HTTPAdapterURL != "" {
sdkConf.HTTPAdapterURL = config.Remotes.HTTPAdapterURL
}
if sdkConf.BootstrapURL == "" && config.Remotes.BootstrapURL != "" {
sdkConf.BootstrapURL = config.Remotes.BootstrapURL
}
if sdkConf.CertsURL == "" && config.Remotes.CertsURL != "" {
sdkConf.CertsURL = config.Remotes.CertsURL
}
if sdkConf.InvitationsURL == "" && config.Remotes.InvitationsURL != "" {
sdkConf.InvitationsURL = config.Remotes.InvitationsURL
}
if sdkConf.JournalURL == "" && config.Remotes.JournalURL != "" {
sdkConf.JournalURL = config.Remotes.JournalURL
}
if sdkConf.HostURL == "" && config.Remotes.HostURL != "" {
sdkConf.HostURL = config.Remotes.HostURL
}
sdkConf.TLSVerification = config.Remotes.TLSVerification || sdkConf.TLSVerification
return sdkConf, nil
}
// New config command to store params to local TOML file.
func NewConfigCmd() *cobra.Command {
return &cobra.Command{
Use: "config <key> <value>",
Short: "CLI local config",
Long: "Local param storage to prevent repetitive passing of keys",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := setConfigValue(args[0], args[1]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
}
}
func setConfigValue(key, value string) error {
config, err := read(ConfigPath)
if err != nil {
return err
}
if strings.Contains(key, "url") {
u, err := url.Parse(value)
if err != nil {
return errors.Wrap(errInvalidURL, err)
}
if u.Scheme == "" || u.Host == "" {
return errors.Wrap(errInvalidURL, err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return errors.Wrap(errURLParseFail, err)
}
}
configKeyToField := map[string]interface{}{
"channels_url": &config.Remotes.ChannelsURL,
"clients_url": &config.Remotes.ClientsURL,
"groups_url": &config.Remotes.GroupsURL,
"users_url": &config.Remotes.UsersURL,
"reader_url": &config.Remotes.ReaderURL,
"http_adapter_url": &config.Remotes.HTTPAdapterURL,
"bootstrap_url": &config.Remotes.BootstrapURL,
"certs_url": &config.Remotes.CertsURL,
"tls_verification": &config.Remotes.TLSVerification,
"offset": &config.Filter.Offset,
"limit": &config.Filter.Limit,
"topic": &config.Filter.Topic,
"raw_output": &config.RawOutput,
"user_token": &config.UserToken,
}
fieldPtr, ok := configKeyToField[key]
if !ok {
return errNoKey
}
fieldValue := reflect.ValueOf(fieldPtr).Elem()
switch fieldValue.Kind() {
case reflect.String:
fieldValue.SetString(value)
case reflect.Int:
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
fieldValue.SetUint(uint64(intValue))
case reflect.Bool:
boolValue, err := strconv.ParseBool(value)
if err != nil {
return err
}
fieldValue.SetBool(boolValue)
default:
return errors.Wrap(errUnsupportedKeyValue, err)
}
buf, err := toml.Marshal(config)
if err != nil {
return err
}
if err = os.WriteFile(ConfigPath, buf, filePermission); err != nil {
return errors.Wrap(errWritingConfig, err)
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
mgsdk "github.com/absmach/magistrala/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdSubscription = []cobra.Command{
{
Use: "create <topic> <contact> <user_auth_token>",
Short: "Create subscription",
Long: `Create new subscription`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
id, err := sdk.CreateSubscription(args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logCreatedCmd(*cmd, id)
},
},
{
Use: "get [all | <sub_id>] <user_auth_token>",
Short: "Get subscription",
Long: `Get subscription.
all - lists all subscriptions
<sub_id> - view subscription of <sub_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
Topic: Topic,
Contact: Contact,
}
if args[0] == "all" {
sub, err := sdk.ListSubscriptions(pageMetadata, args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, sub)
return
}
c, err := sdk.ViewSubscription(args[0], args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "remove <sub_id> <user_auth_token>",
Short: "Remove subscription",
Long: `Removes removes a subscription with the provided id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.DeleteSubscription(args[0], args[1]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
}
// NewSubscriptionCmd returns subscription command.
func NewSubscriptionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "subscription [create | get | remove ]",
Short: "Subscription management",
Long: `Subscription management: create, get, or delete subscription`,
}
for i := range cmdSubscription {
cmd.AddCommand(&cmdSubscription[i])
}
return &cmd
}
+266
View File
@@ -0,0 +1,266 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/absmach/magistrala/cli"
"github.com/absmach/magistrala/internal/testsutil"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
userID = testsutil.GenerateUUID(&testing.T{})
subscription = mgsdk.Subscription{
ID: testsutil.GenerateUUID(&testing.T{}),
OwnerID: userID,
Topic: "topic",
Contact: "identity@example.com",
}
)
func TestCreateSubscriptionCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
response string
id string
}{
{
desc: "create subscription successfully",
args: []string{
subscription.Topic,
subscription.Contact,
validToken,
},
id: userID,
response: fmt.Sprintf("\ncreated: %s\n\n", userID),
logType: createLog,
},
{
desc: "create subscription with invalid args",
args: []string{
subscription.Topic,
subscription.Contact,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "create subscription with invalid token",
args: []string{
subscription.Topic,
subscription.Contact,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("CreateSubscription", tc.args[0], tc.args[1], tc.args[2]).Return(tc.id, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
switch tc.logType {
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case createLog:
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
}
sdkCall.Unset()
})
}
}
func TestGetSubscriptionsCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
var sub mgsdk.Subscription
var page mgsdk.SubscriptionPage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
page mgsdk.SubscriptionPage
subscription mgsdk.Subscription
logType outputLog
errLogMessage string
}{
{
desc: "get all subscriptions successfully",
args: []string{
all,
validToken,
},
page: mgsdk.SubscriptionPage{
Subscriptions: []mgsdk.Subscription{subscription},
},
logType: entityLog,
},
{
desc: "get subscription with id",
args: []string{
subscription.ID,
validToken,
},
logType: entityLog,
subscription: subscription,
},
{
desc: "get subscriptions with invalid args",
args: []string{
all,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "get all subscriptions with invalid token",
args: []string{
all,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "get subscription with invalid id",
args: []string{
invalidID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewSubscription", tc.args[0], tc.args[1]).Return(tc.subscription, tc.sdkErr)
sdkCall1 := sdkMock.On("ListSubscriptions", mock.Anything, tc.args[1]).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[1] == all {
err := json.Unmarshal([]byte(out), &page)
assert.Nil(t, err)
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
} else {
err := json.Unmarshal([]byte(out), &sub)
assert.Nil(t, err)
assert.Equal(t, tc.subscription, sub, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.subscription, sub))
}
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
func TestRemoveSubscriptionCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "remove subscription successfully",
args: []string{
subscription.ID,
validToken,
},
logType: okLog,
},
{
desc: "remove subscription with invalid args",
args: []string{
subscription.ID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "remove subscription with invalid subscription id",
args: []string{
invalidID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
{
desc: "remove subscription with invalid token",
args: []string{
subscription.ID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("DeleteSubscription", tc.args[0], tc.args[1]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
+410
View File
@@ -0,0 +1,410 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"time"
"github.com/0x6flab/namegenerator"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
const (
jsonExt = ".json"
csvExt = ".csv"
PublishType = "publish"
SubscribeType = "subscribe"
)
var (
msgFormat = `[{"bn":"provision:", "bu":"V", "t": %d, "bver":5, "n":"voltage", "u":"V", "v":%d}]`
namesgenerator = namegenerator.NewGenerator()
)
var cmdProvision = []cobra.Command{
{
Use: "clients <clients_file> <domain_id> <user_token>",
Short: "Provision clients",
Long: `Bulk create clients`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
logErrorCmd(*cmd, err)
return
}
clients, err := clientsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
clients, err = sdk.CreateClients(clients, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, clients)
},
},
{
Use: "channels <channels_file> <domain_id> <user_token>",
Short: "Provision channels",
Long: `Bulk create channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
channels, err := channelsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
var chs []smqsdk.Channel
for _, c := range channels {
c, err = sdk.CreateChannel(c, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
chs = append(chs, c)
}
channels = chs
logJSONCmd(*cmd, channels)
},
},
{
Use: "connect <connections_file> <domain_id> <user_token>",
Short: "Provision connections",
Long: `Bulk connect clients to channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
connIDs, err := connectionsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
for _, conn := range connIDs {
if err := sdk.Connect(conn, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
}
logOKCmd(*cmd)
},
},
{
Use: "test",
Short: "test",
Long: `Provisions test setup: one test user, two clients and two channels. \
Connect both clients to one of the channels, \
and only on client to other channel.`,
Run: func(cmd *cobra.Command, args []string) {
numClients := 2
numChan := 2
clients := []smqsdk.Client{}
channels := []smqsdk.Channel{}
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
// Create test user
name := namesgenerator.Generate()
user := smqsdk.User{
FirstName: name,
Email: fmt.Sprintf("%s@email.com", name),
Credentials: smqsdk.Credentials{
Username: name,
Secret: "12345678",
},
Status: smqsdk.EnabledStatus,
}
user, err := sdk.CreateUser(user, "")
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err := sdk.CreateToken(smqsdk.Login{Username: user.Credentials.Username, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// create domain
domain := smqsdk.Domain{
Name: fmt.Sprintf("%s-domain", name),
Status: smqsdk.EnabledStatus,
}
domain, err = sdk.CreateDomain(domain, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err = sdk.CreateToken(smqsdk.Login{Username: user.Email, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create clients
for i := 0; i < numClients; i++ {
t := smqsdk.Client{
Name: fmt.Sprintf("%s-client-%d", name, i),
Status: smqsdk.EnabledStatus,
}
clients = append(clients, t)
}
clients, err = sdk.CreateClients(clients, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create channels
for i := 0; i < numChan; i++ {
c := smqsdk.Channel{
Name: fmt.Sprintf("%s-channel-%d", name, i),
Status: smqsdk.EnabledStatus,
}
c, err = sdk.CreateChannel(c, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
channels = append(channels, c)
}
// Connect clients to channels - first client to both channels, second only to first
conIDs := smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[1].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[1].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
// send message to test connectivity
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[1].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(channels[1].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, user, ut, clients, channels)
},
},
}
// NewProvisionCmd returns provision command.
func NewProvisionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "provision [clients | channels | connect | test]",
Short: "Provision clients and channels from a config file",
Long: `Provision clients and channels: use json or csv file to bulk provision clients and channels`,
}
for i := range cmdProvision {
cmd.AddCommand(&cmdProvision[i])
}
return &cmd
}
func clientsFromFile(path string) ([]smqsdk.Client, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Client{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Client{}, err
}
defer file.Close()
clients := []smqsdk.Client{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Client{}, err
}
if len(l) < 1 {
return []smqsdk.Client{}, errors.New("empty line found in file")
}
client := smqsdk.Client{
Name: l[0],
}
clients = append(clients, client)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&clients)
if err != nil {
return []smqsdk.Client{}, err
}
default:
return []smqsdk.Client{}, err
}
return clients, nil
}
func channelsFromFile(path string) ([]smqsdk.Channel, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Channel{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Channel{}, err
}
defer file.Close()
channels := []smqsdk.Channel{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Channel{}, err
}
if len(l) < 1 {
return []smqsdk.Channel{}, errors.New("empty line found in file")
}
channel := smqsdk.Channel{
Name: l[0],
}
channels = append(channels, channel)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&channels)
if err != nil {
return []smqsdk.Channel{}, err
}
default:
return []smqsdk.Channel{}, err
}
return channels, nil
}
func connectionsFromFile(path string) ([]smqsdk.Connection, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Connection{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Connection{}, err
}
defer file.Close()
connections := []smqsdk.Connection{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Connection{}, err
}
if len(l) < 1 {
return []smqsdk.Connection{}, errors.New("empty line found in file")
}
connections = append(connections, smqsdk.Connection{
ClientIDs: []string{l[0]},
ChannelIDs: []string{l[1]},
Types: []string{PublishType, SubscribeType},
})
}
case jsonExt:
err := json.NewDecoder(file).Decode(&connections)
if err != nil {
return []smqsdk.Connection{}, err
}
default:
return []smqsdk.Connection{}, err
}
return connections, nil
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import mgsdk "github.com/absmach/magistrala/pkg/sdk"
// Keep SDK handle in global var.
var sdk mgsdk.SDK
// SetSDK sets supermq SDK instance.
func SetSDK(s mgsdk.SDK) {
sdk = s
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"bytes"
"testing"
"github.com/absmach/supermq/cli"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
type outputLog uint8
const (
usageLog outputLog = iota
errLog
entityLog
okLog
createLog
revokeLog
)
func executeCommand(t *testing.T, root *cobra.Command, args ...string) string {
buffer := new(bytes.Buffer)
root.SetOut(buffer)
root.SetErr(buffer)
root.SetArgs(args)
err := root.Execute()
assert.NoError(t, err, "Error executing command")
return buffer.String()
}
func setFlags(rootCmd *cobra.Command) *cobra.Command {
// Root Flags
rootCmd.PersistentFlags().BoolVarP(
&cli.RawOutput,
"raw",
"r",
cli.RawOutput,
"Enables raw output mode for easier parsing of output",
)
// Client and Channels Flags
rootCmd.PersistentFlags().Uint64VarP(
&cli.Limit,
"limit",
"l",
10,
"Limit query parameter",
)
rootCmd.PersistentFlags().Uint64VarP(
&cli.Offset,
"offset",
"o",
0,
"Offset query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Name,
"name",
"n",
"",
"Name query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Identity,
"identity",
"I",
"",
"User identity query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Metadata,
"metadata",
"m",
"",
"Metadata query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Status,
"status",
"S",
"",
"User status query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.State,
"state",
"z",
"",
"Bootstrap state query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Topic,
"topic",
"T",
"",
"Subscription topic query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Contact,
"contact",
"C",
"",
"Subscription contact query parameter",
)
return rootCmd
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
"fmt"
"github.com/fatih/color"
"github.com/hokaccha/go-prettyjson"
"github.com/spf13/cobra"
)
var (
// Limit query parameter.
Limit uint64 = 10
// Offset query parameter.
Offset uint64 = 0
// Name query parameter.
Name string = ""
// Identity query parameter.
Identity string = ""
// Metadata query parameter.
Metadata string = ""
// Status query parameter.
Status string = ""
// ConfigPath config path parameter.
ConfigPath string = ""
// State query parameter.
State string = ""
// Topic query parameter.
Topic string = ""
// Contact query parameter.
Contact string = ""
// RawOutput raw output mode.
RawOutput bool = false
// Username query parameter.
Username string = ""
// FirstName query parameter.
FirstName string = ""
// LastName query parameter.
LastName string = ""
)
func logJSONCmd(cmd cobra.Command, iList ...interface{}) {
for _, i := range iList {
m, err := json.Marshal(i)
if err != nil {
logErrorCmd(cmd, err)
return
}
pj, err := prettyjson.Format(m)
if err != nil {
logErrorCmd(cmd, err)
return
}
fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", string(pj))
}
}
func logUsageCmd(cmd cobra.Command, u string) {
fmt.Fprintf(cmd.OutOrStdout(), color.YellowString("\nusage: %s\n\n"), u)
}
func logErrorCmd(cmd cobra.Command, err error) {
boldRed := color.New(color.FgRed, color.Bold)
boldRed.Fprintf(cmd.ErrOrStderr(), "\nerror: ")
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", color.RedString(err.Error()))
}
func logOKCmd(cmd cobra.Command) {
fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", color.BlueString("ok"))
}
func logCreatedCmd(cmd cobra.Command, e string) {
if RawOutput {
fmt.Fprintln(cmd.OutOrStdout(), e)
} else {
fmt.Fprintf(cmd.OutOrStdout(), color.BlueString("\ncreated: %s\n\n"), e)
}
}
+281
View File
@@ -0,0 +1,281 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package main contains cli main function to run the cli.
package main
import (
"log"
"github.com/absmach/magistrala/cli"
mgcli "github.com/absmach/magistrala/cli"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
smqcli "github.com/absmach/supermq/cli"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
func main() {
msgContentType := string(smqsdk.CTJSONSenML)
smqsdkConf := smqsdk.Config{
MsgContentType: smqsdk.ContentType(msgContentType),
}
mgsdkConf := mgsdk.Config{
MsgContentType: smqsdk.ContentType(msgContentType),
}
// Root
rootCmd := &cobra.Command{
Use: "magistrala-cli",
PersistentPreRun: func(_ *cobra.Command, _ []string) {
smqcliConf, err := smqcli.ParseConfig(smqsdkConf)
if err != nil {
log.Fatalf("Failed to parse config: %s", err)
}
if smqcliConf.MsgContentType == "" {
smqcliConf.MsgContentType = smqsdk.ContentType(msgContentType)
}
ss := smqsdk.NewSDK(smqcliConf)
smqcli.SetSDK(ss)
mgcliConf, err := mgcli.ParseConfig(mgsdkConf)
if err != nil {
log.Fatalf("Failed to parse config: %s", err)
}
if mgcliConf.MsgContentType == "" {
mgcliConf.MsgContentType = smqsdk.ContentType(msgContentType)
}
ms := mgsdk.NewSDK(mgcliConf)
mgcli.SetSDK(ms)
},
}
// SuperMQ API commands
healthCmd := smqcli.NewHealthCmd()
usersCmd := smqcli.NewUsersCmd()
domainsCmd := smqcli.NewDomainsCmd()
clientsCmd := smqcli.NewClientsCmd()
groupsCmd := smqcli.NewGroupsCmd()
channelsCmd := smqcli.NewChannelsCmd()
messagesCmd := smqcli.NewMessagesCmd()
certsCmd := smqcli.NewCertsCmd()
configCmd := smqcli.NewConfigCmd()
invitationsCmd := smqcli.NewInvitationsCmd()
journalCmd := smqcli.NewJournalCmd()
// Magistrala API commands
provisionCmd := mgcli.NewProvisionCmd()
bootstrapCmd := mgcli.NewBootstrapCmd()
subscriptionsCmd := mgcli.NewSubscriptionCmd()
// Root Commands
rootCmd.AddCommand(healthCmd)
rootCmd.AddCommand(usersCmd)
rootCmd.AddCommand(domainsCmd)
rootCmd.AddCommand(groupsCmd)
rootCmd.AddCommand(clientsCmd)
rootCmd.AddCommand(channelsCmd)
rootCmd.AddCommand(messagesCmd)
rootCmd.AddCommand(provisionCmd)
rootCmd.AddCommand(bootstrapCmd)
rootCmd.AddCommand(certsCmd)
rootCmd.AddCommand(subscriptionsCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(invitationsCmd)
rootCmd.AddCommand(journalCmd)
// Root Flags
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.BootstrapURL,
"bootstrap-url",
"b",
mgsdkConf.BootstrapURL,
"Bootstrap service URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.CertsURL,
"certs-url",
"s",
mgsdkConf.CertsURL,
"Certs service URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.ClientsURL,
"clients-url",
"t",
mgsdkConf.ClientsURL,
"Clients service URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.UsersURL,
"users-url",
"u",
mgsdkConf.UsersURL,
"Users service URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.DomainsURL,
"domains-url",
"d",
mgsdkConf.DomainsURL,
"Domains service URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.HTTPAdapterURL,
"http-url",
"p",
mgsdkConf.HTTPAdapterURL,
"HTTP adapter URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.ReaderURL,
"reader-url",
"R",
mgsdkConf.ReaderURL,
"Reader URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.InvitationsURL,
"invitations-url",
"v",
mgsdkConf.InvitationsURL,
"Inivitations URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.JournalURL,
"journal-url",
"a",
mgsdkConf.JournalURL,
"Journal Log URL",
)
rootCmd.PersistentFlags().StringVarP(
&mgsdkConf.HostURL,
"host-url",
"H",
mgsdkConf.HostURL,
"Host URL",
)
rootCmd.PersistentFlags().StringVarP(
&msgContentType,
"content-type",
"y",
msgContentType,
"Message content type",
)
rootCmd.PersistentFlags().BoolVarP(
&mgsdkConf.TLSVerification,
"insecure",
"i",
mgsdkConf.TLSVerification,
"Do not check for TLS cert",
)
rootCmd.PersistentFlags().StringVarP(
&cli.ConfigPath,
"config",
"c",
cli.ConfigPath,
"Config path",
)
rootCmd.PersistentFlags().BoolVarP(
&cli.RawOutput,
"raw",
"r",
cli.RawOutput,
"Enables raw output mode for easier parsing of output",
)
rootCmd.PersistentFlags().BoolVarP(
&mgsdkConf.CurlFlag,
"curl",
"x",
false,
"Convert HTTP request to cURL command",
)
// Client and Channels Flags
rootCmd.PersistentFlags().Uint64VarP(
&cli.Limit,
"limit",
"l",
10,
"Limit query parameter",
)
rootCmd.PersistentFlags().Uint64VarP(
&cli.Offset,
"offset",
"o",
0,
"Offset query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Name,
"name",
"n",
"",
"Name query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Identity,
"identity",
"I",
"",
"User identity query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Metadata,
"metadata",
"m",
"",
"Metadata query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Status,
"status",
"S",
"",
"User status query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.State,
"state",
"z",
"",
"Bootstrap state query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Topic,
"topic",
"T",
"",
"Subscription topic query parameter",
)
rootCmd.PersistentFlags().StringVarP(
&cli.Contact,
"contact",
"C",
"",
"Subscription contact query parameter",
)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
+5 -3
View File
@@ -13,11 +13,13 @@ user_token = ""
journal_url = "http://localhost:9021"
bootstrap_url = "http://localhost:9013"
certs_url = "http://localhost:9019"
domains_url = "http://localhost:8189"
domains_url = "http://localhost:9003"
host_url = "http://localhost"
http_adapter_url = "http://localhost:8008"
invitations_url = "http://localhost:9020"
reader_url = "http://localhost:9011"
things_url = "http://localhost:9000"
tls_verification = false
clients_url = "http://localhost:9006"
channels_url = "http://localhost:9005"
groups_url = "http://localhost:9004"
users_url = "http://localhost:9002"
tls_verification = false
+2 -1
View File
@@ -59,7 +59,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/color v1.18.0
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@@ -74,6 +74,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
+2
View File
@@ -155,6 +155,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+2
View File
@@ -33,6 +33,8 @@ type PageMetadata struct {
Contact string `json:"contact,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Level uint64 `json:"level,omitempty"`
State string `json:"state,omitempty"`
Name string `json:"name,omitempty"`
}
type MessagePageMetadata struct {