Skip to content

Commit

Permalink
feat:(meshery#10585) Add mesheryctl credentials list command
Browse files Browse the repository at this point in the history
Signed-off-by: Saurabh Kumar Singh <singh1203.ss@gmail.com>
  • Loading branch information
singh1203 committed Apr 2, 2024
1 parent 8be68d7 commit 5f1151e
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/pages/concepts/logical/credentials.md
Expand Up @@ -9,7 +9,7 @@ list: include
redirect_from:
- concepts/credentials
---
Meshery uses one or more Credentials when authenticating to a managed or unmanaged Connection. Credentials are based on the Meshery's [Credential Schema](https://github.com/meshery/schemas/blob/master/openapi/schemas/credentials.yml) with defined attributes.
Meshery uses one or more Credentials when authenticating to a managed or unmanaged Connection. Credentials are based on the Meshery's [Credential Schema](https://github.com/meshery/schemas/blob/master/schemas/constructs/v1alpha1/credentials.yml) with defined attributes.

[![States for Unmanaged Connections]({{ site.baseurl }}/assets/img/meshsync/states-for-unmanaged-connections.svg
)]({{ site.baseurl }}/assets/img/meshsync/states-for-unmanaged-connections.svg)
68 changes: 68 additions & 0 deletions mesheryctl/internal/cli/root/credentials/credential.go
@@ -0,0 +1,68 @@
// Copyright Meshery Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package credentials

import (
"fmt"

"github.com/fatih/color"
"github.com/layer5io/meshery/mesheryctl/pkg/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var (
// Maximum number of rows to be displayed in a page
maxRowsPerPage = 25

// Color for the whiteboard printer
whiteBoardPrinter = color.New(color.FgHiBlack, color.BgWhite, color.Bold)

availableSubcommands = []*cobra.Command{listCredentialCmd}
)
var CredentialCmd = &cobra.Command{
Use: "credential",
Short: "Manage credentials",
Long: `View and manage list of credentials for Meshery Environment.
Find more information at: https://docs.meshery.io/reference/mesheryctl#command-reference`,
Example: `
// To View all credentials
mesheryctl exp credential list
// To view details of a specific credential
mesheryctl exp credential view <credential-ID>
// Documentation for Credentials can be found at:
https://docs.meshery.io/concepts/logical/credentials
`,

Args: func(cmd *cobra.Command, args []string) error {
const errMsg = "Usage: mesheryctl exp credential [subcommands]\nRun 'mesheryctl exp credential --help' to see detailed help message"
if len(args) == 0 {
return utils.ErrInvalidArgument(errors.New("missing required argument: [subcommands] " + errMsg))
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if ok := utils.IsValidSubcommand(availableSubcommands, args[0]); !ok {
return errors.New(utils.CredentialsError(fmt.Sprintf("invalid subcommand: %s. Please provide options from [view]. Use 'mesheryctl exp credential --help' to display usage guide.\n", args[0]), "credential"))
}
return nil
},
}

func init() {
CredentialCmd.AddCommand(availableSubcommands...)
}
104 changes: 104 additions & 0 deletions mesheryctl/internal/cli/root/credentials/credential_test.go
@@ -0,0 +1,104 @@
package credentials

import (
"flag"
"path/filepath"
"runtime"
"testing"

"github.com/jarcoal/httpmock"
"github.com/layer5io/meshery/mesheryctl/pkg/utils"
)

var update = flag.Bool("update", false, "update golden files")

func TestCredentialCmd(t *testing.T) {
// setup current context
utils.SetupContextEnv(t)

// initialize mock server for handling requests
utils.StartMockery(t)

// create a test helper
testContext := utils.NewTestHelper(t)

// get current directory
_, filename, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("Not able to get current working directory")
}
currDir := filepath.Dir(filename)
fixturesDir := filepath.Join(currDir, "fixtures")

// test scenrios for fetching data
testcase := []struct {
Name string
Args []string
ExpectedResponse string
URLs []utils.MockURL
Token string
ExpectError bool
}{
{
Name: "credential list",
Args: []string{"list"},
ExpectedResponse: "credential.list.output.golden",
URLs: []utils.MockURL{
{
Method: "GET",
URL: testContext.BaseURL + "/api/credentials",
Response: "credential.list.api.response.golden",
ResponseCode: 200,
},
},
Token: filepath.Join(fixturesDir, "token.golden"),
ExpectError: false,
},
}
for _, test := range testcase {
t.Run(test.Name, func(t *testing.T) {
for _, url := range test.URLs {
apiResponse := utils.NewGoldenFile(t, url.Response, fixturesDir).Load()
httpmock.RegisterResponder(url.Method, url.URL,
httpmock.NewStringResponder(url.ResponseCode, apiResponse))
}
//set token
utils.TokenFlag = test.Token

// Expected output from golden files
testdataDir := filepath.Join(currDir, "testdata")
golden := utils.NewGoldenFile(t, test.ExpectedResponse, testdataDir)

buff := utils.SetupMeshkitLoggerTesting(t, false)
CredentialCmd.SetArgs(test.Args)
CredentialCmd.SetOutput(buff)

err := CredentialCmd.Execute()
if err != nil {
if test.ExpectError {

if *update {
golden.Write(err.Error())
}
expectedResponse := golden.Load()

utils.Equals(t, expectedResponse, err.Error())
return
}
t.Fatal(err)
}
//print response string to console
actualResponse := buff.String()
// write it in file
/* if *update {
golden.Write(actualResponse)
} */
expectedResponse := golden.Load()

utils.Equals(t, expectedResponse, actualResponse)
})
t.Log("Filter tests Passed")
}
// stop mock server
utils.StopMockery(t)
}
Empty file.
@@ -0,0 +1 @@
{"meshery-provider":"Meshery","token":"eyJhY2Nlc3NfdG9rZW4iOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SW5CMVlteHBZenBsT0dWbU5ERmpNeTFpWldWbUxUUmlZakV0T0dVNE1DMHpOakExTVRZeU4yTTJNakVpTENKMGVYQWlPaUpLVjFRaWZRLmV5SmhkV1FpT2x0ZExDSmpiR2xsYm5SZmFXUWlPaUp0WlhOb1pYSjVMV05zYjNWa0lpd2laWGh3SWpveE5qSXlPREk1TlRRMExDSmxlSFFpT250OUxDSnBZWFFpT2pFMk1qSTRNalU1TkRNc0ltbHpjeUk2SW1oMGRIQnpPaTh2YldWemFHVnllUzVzWVhsbGNqVXVhVzh2YUhsa2NtRXZJaXdpYW5ScElqb2lPRGMxT0RGbVpXSXROMlZpTnkwMFlqSTFMV0l3TURndE9XWTJaVEE0WXpabFkyVTJJaXdpYm1KbUlqb3hOakl5T0RJMU9UUXpMQ0p6WTNBaU9sc2liM0JsYm1sa0lpd2liMlptYkdsdVpTSmRMQ0p6ZFdJaU9pSmpSMncxWkZoT2IyTXliSFZhTWtaNVlWaHNhRHBhTW13d1lVaFdhU0o5Lk90aDJwYkJFNmFBcnBfUFVwR3E3b2ZsaEVWYmdsdTAtamdXNG44eWxHeVVTandOc0k4SmdoallIVGU5YjlUSzhWQUhoNVRyT0YwV1VRb0h4QVJGUmN6OHl2ZEdpbm1HcUZEZTd6RVpoSjZHZmNlZFl6bmpCc3FvVWthMTNXYzhvM0J2bGR2T2gtTjFGNzdHM3ZLenI0UEJaM2pXRHVEeWpjSUJnOTJVUzd0Nlg5Ymd6YklrT3lOOVhpWGVVNXQtbEJIamt2cklRazhqdWRKaTliOHVGaVBuMmdIMDVJbnhUdFJtSlFJdUhvSzV2WmxFQW0xN1J6ZER4WVI0cndqeTBqanFWdXdvWnBjbUJQM1dUNjdIVHhkYmo5N3hZM2IzNHh5ZFkxeVFVS09XR1NOckZVeXhMbW9QMmJUM24tQ0dVczJ1SWhnZExXNlZlNVQ1LV9tSGY0Z212X0NGWlFNelRsbjRFVmw2bTUxdjFxNXJzQmdfWmFuVmtXdGNHWF9ZSGs3WHpKdndXRDhvSmt5NzBleGUwYXJ3cmg2bjJkLU9jMi1Jc1F2OTBFM1hYeHBJcWxrckNfU3NiM1NpOU1jM1ptal9HY2JtOHVHbUZEejhaZEYxUEdpeDdKTjM3TzJyQnpaVldRaHFrZTV6MW42VUVITXJGSGJBNXBKVkxzUmE0ZUNBaFdwODVlZVV3ZjlUMnByc3FzNHBaMkh0eVpSMlBTdGFLZVFFai1SUXdvRHpDTEN4Zm85RnBvbEN6WmN3ZzRvLXhrb0Q0aS1MczIzODd0dm5xSTVESl8xaUlMX1hNTHByZXJtcDdxeGV2NEVDOW9abzdWenZmTDd4cDZTcnhIaldZQVpuZS12eURjQlhNZUlSMVVoeVdVZDQtaWJfZmxzdFVEME5XVV9ZIiwidG9rZW5fdHlwZSI6ImJlYXJlciIsInJlZnJlc2hfdG9rZW4iOiJXS3pZWW5BQkVJQkduekNfaWR2VW1IZUtsZlgzLWxjWm12TzBxY2ZCNlRzLm5kNXhXUFFIeWVTcTY0OUV2dy1tX2t3WDdqYWF1RDZiSExXTW9fQVhxZVUiLCJleHBpcnkiOiIyMDIxLTA2LTA0VDE3OjU5OjAzLjg0ODAyODAwOVoifQ"}
123 changes: 123 additions & 0 deletions mesheryctl/internal/cli/root/credentials/list.go
@@ -0,0 +1,123 @@
// Copyright Meshery Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package credentials

import (
"encoding/json"
"fmt"
"io"

"github.com/eiannone/keyboard"
"github.com/layer5io/meshery/mesheryctl/internal/cli/root/config"
"github.com/layer5io/meshery/mesheryctl/pkg/utils"
"github.com/layer5io/meshery/server/models"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var listCredentialCmd = &cobra.Command{
Use: "list",
Short: "List all the credentials",
Long: `Display list of all the available credentials`,
Args: cobra.MinimumNArgs(0),
Example: `
// List all the credentials
mesheryctl exp credential list
`,

RunE: func(cmd *cobra.Command, args []string) error {
//Check prerequisite
mctlCfg, err := config.GetMesheryCtl(viper.GetViper())
if err != nil {
return utils.ErrLoadConfig(err)
}
req, err := utils.NewRequest("GET", mctlCfg.GetBaseMesheryURL()+"/api/integrations/credentials", nil)
if err != nil {
utils.Log.Error(err)
return nil
}
res, err := utils.MakeRequest(req)
if err != nil {
utils.Log.Error(err)
return nil
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
utils.Log.Error(utils.ErrReadResponseBody(err))
return nil
}
credentialResponse := models.CredentialsPage{}
err = json.Unmarshal(body, &credentialResponse)
if err != nil {
utils.Log.Error(utils.ErrUnmarshal(err))
return nil
}
header := []string{"ID", "Name", "Type", "Created At", "Updated At"}
data := [][]string{}
for _, credential := range credentialResponse.Credentials {
data = append(data, []string{credential.ID.String(), credential.Name, credential.Type, credential.CreatedAt.String(), credential.UpdatedAt.String()})
}
if len(data) == 0 {
utils.Log.Info("No credentials found")
return nil
}
if cmd.Flags().Changed("page") {
utils.PrintToTable(header, data)
} else {
startIndex := 0
endIndex := min(len(data), startIndex+maxRowsPerPage)
for {
// Clear the entire terminal screen
utils.ClearLine()
// Print the number of credentials and current page number
whiteBoardPrinter.Println("Total Credentials: ", len(data))
whiteBoardPrinter.Println("Page: ", startIndex/maxRowsPerPage+1)
whiteBoardPrinter.Println("Press Enter or ↓ to continue, Esc or Ctrl+C (Ctrl+Cmd for OS user) to exit")

// Print the table
utils.PrintToTable(header, data[startIndex:endIndex])
// Wait for the user to press a key
keysEvents, err := keyboard.GetKeys(10)
if err != nil {
return err
}

defer func() {
_ = keyboard.Close()
}()

event := <-keysEvents
if event.Err != nil {
utils.Log.Error(fmt.Errorf("unable to capture keyboard events"))
break
}
if event.Key == keyboard.KeyEsc || event.Key == keyboard.KeyCtrlC {
break
}

if event.Key == keyboard.KeyEnter || event.Key == keyboard.KeyArrowDown {
startIndex += maxRowsPerPage
endIndex = min(len(data), startIndex+maxRowsPerPage)
}

if startIndex >= len(data) {
break
}
}
}
return nil
},
}
Empty file.
3 changes: 2 additions & 1 deletion mesheryctl/internal/cli/root/experimental/experimental.go
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/layer5io/meshery/mesheryctl/internal/cli/root/components"
"github.com/layer5io/meshery/mesheryctl/internal/cli/root/connections"
"github.com/layer5io/meshery/mesheryctl/internal/cli/root/credentials"
"github.com/layer5io/meshery/mesheryctl/internal/cli/root/system"
"github.com/layer5io/meshery/mesheryctl/pkg/utils"
"github.com/pkg/errors"
Expand Down Expand Up @@ -46,6 +47,6 @@ var ExpCmd = &cobra.Command{
}

func init() {
availableSubcommands = append(availableSubcommands, system.ModelCmd, components.ComponentsCmd, connections.ConnectionsCmd)
availableSubcommands = append(availableSubcommands, system.ModelCmd, components.ComponentsCmd, connections.ConnectionsCmd, credentials.CredentialCmd)
ExpCmd.AddCommand(availableSubcommands...)
}
15 changes: 14 additions & 1 deletion mesheryctl/pkg/utils/error.go
Expand Up @@ -154,6 +154,15 @@ func RegistryError(msg string, cmd string) string {
}
}

func CredentialsError(msg string, cmd string) string {
switch cmd {
case "list":
return formatError(msg, cmdCredentialList)
default:
return formatError(msg, cmdCredential)
}
}

// MeshError returns a formatted error message with a link to 'mesh' command usage page in addition to the error message
func MeshError(msg string) string {
return formatError(msg, cmdMesh)
Expand Down Expand Up @@ -503,5 +512,9 @@ func ErrBadRequest(err error) error {
}

func ErrInvalidArgument(err error) error {
return errors.New(ErrInvalidArgumentCode, errors.Alert, []string{"Invalid Argument"}, []string{err.Error()}, []string{"Invalid Argument"}, []string{"Please check the arguments passed"})
return errors.New(ErrInvalidArgumentCode, errors.Alert,
[]string{"Invalid Argument"},
[]string{err.Error()},
[]string{"Invalid Argument"},
[]string{"Please check the arguments passed"})
}
2 changes: 2 additions & 0 deletions mesheryctl/pkg/utils/helpers.go
Expand Up @@ -136,6 +136,8 @@ const (
cmdRegistry cmdType = "regisry"
cmdConnectionList cmdType = "connection list"
cmdConnectionDelete cmdType = "connection delete"
cmdCredential cmdType = "credential"
cmdCredentialList cmdType = "credential list"
)

const (
Expand Down

0 comments on commit 5f1151e

Please sign in to comment.