PLTCONN-3577: Link & unlink and instance list

This commit is contained in:
fangming-ning-sp
2023-09-07 11:20:40 -05:00
parent 80af0b4c6d
commit 4af26ca7e5
13 changed files with 296 additions and 19 deletions

View File

@@ -56,6 +56,7 @@ func NewConnCmd(term terminal.Terminal) *cobra.Command {
newConnStatsCmd(Client),
newConnDeleteCmd(Client),
newConnCustomizersCmd(Client),
newConnInstancesCmd(Client),
)
return conn

View File

@@ -16,7 +16,7 @@ func TestChangePasswordWithoutInput(t *testing.T) {
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
term := mocks.NewMockTerm(ctrl)
term := mocks.NewMockTerminal(ctrl)
cmd := newConnInvokeChangePasswordCmd(client, term)
addRequiredFlagsFromParentCmd(cmd)
@@ -43,7 +43,7 @@ func TestChangePasswordWithIdentityAndPassword(t *testing.T) {
Post(gomock.Any(), gomock.Any(), "application/json", bytes.NewReader([]byte(i))).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil)
term := mocks.NewMockTerm(ctrl)
term := mocks.NewMockTerminal(ctrl)
term.EXPECT().
PromptPassword(gomock.Any()).
Return("password", nil)
@@ -73,7 +73,7 @@ func TestChangePasswordWithIdentityAndPasswordAndUniqueId(t *testing.T) {
Post(gomock.Any(), gomock.Any(), "application/json", bytes.NewReader([]byte(i))).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil)
term := mocks.NewMockTerm(ctrl)
term := mocks.NewMockTerminal(ctrl)
term.EXPECT().
PromptPassword(gomock.Any()).
Return("password", nil)

View File

@@ -24,7 +24,7 @@ func TestNewConnInvokeCmd_noArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := newConnInvokeCmd(mocks.NewMockClient(ctrl), mocks.NewMockTerm(ctrl))
cmd := newConnInvokeCmd(mocks.NewMockClient(ctrl), mocks.NewMockTerminal(ctrl))
if len(cmd.Commands()) != numConnInvokeSubcommands {
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnInvokeSubcommands)
}

View File

@@ -33,7 +33,7 @@ func TestNewConnCmd_noArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := NewConnCmd(mocks.NewMockTerm(ctrl))
cmd := NewConnCmd(mocks.NewMockTerminal(ctrl))
if len(cmd.Commands()) != numConnSubcommands {
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnSubcommands)
}

View File

@@ -24,6 +24,8 @@ func newConnCustomizersCmd(client client.Client) *cobra.Command {
newCustomizerUpdateCmd(client),
newCustomizerDeleteCmd(client),
newCustomizerCreateVersionCmd(client),
newCustomizerLinkCmd(client),
newCustomizerUnlinkCmd(client),
)
return cmd

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
package connector
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
"github.com/spf13/cobra"
)
func newCustomizerLinkCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "link",
Short: "Link connector customizer to connector instance",
Example: "sail conn customizers link -c 1234 -i 5678",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
customizerID := cmd.Flags().Lookup("id").Value.String()
instanceID := cmd.Flags().Lookup("instance-id").Value.String()
raw, err := json.Marshal([]interface{}{map[string]interface{}{
"op": "replace",
"path": "/connectorCustomizerId",
"value": customizerID,
}})
if err != nil {
return err
}
resp, err := client.Patch(cmd.Context(), util.ResourceUrl(connectorInstancesEndpoint, instanceID), bytes.NewReader(raw))
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("link customizer failed. status: %s\nbody: %s", resp.Status, string(body))
}
var i instance
err = json.NewDecoder(resp.Body).Decode(&i)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(instanceColumns)
table.Append(i.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("id", "c", "", "Connector customizer ID")
_ = cmd.MarkFlagRequired("customizer-id")
cmd.Flags().StringP("instance-id", "i", "", "Connector instance ID")
_ = cmd.MarkFlagRequired("instance-id")
return cmd
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
package connector
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
"github.com/spf13/cobra"
)
func newCustomizerUnlinkCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "unlink",
Short: "Unlink connector customizer from connector instance",
Example: "sail conn customizers unlink -i 5678",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
instanceID := cmd.Flags().Lookup("instance-id").Value.String()
raw, err := json.Marshal([]interface{}{map[string]interface{}{
"op": "remove",
"path": "/connectorCustomizerId",
}})
if err != nil {
return err
}
resp, err := client.Patch(cmd.Context(), util.ResourceUrl(connectorInstancesEndpoint, instanceID), bytes.NewReader(raw))
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("link customizer failed. status: %s\nbody: %s", resp.Status, string(body))
}
var i instance
err = json.NewDecoder(resp.Body).Decode(&i)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(instanceColumns)
table.Append(i.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("instance-id", "i", "", "Connector instance ID")
_ = cmd.MarkFlagRequired("instance-id")
return cmd
}

25
cmd/connector/instance.go Normal file
View File

@@ -0,0 +1,25 @@
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
package connector
import (
"fmt"
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
"github.com/spf13/cobra"
)
func newConnInstancesCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "instances",
Short: "Manage connector instances",
Run: func(cmd *cobra.Command, args []string) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cmd.UsageString())
},
}
cmd.AddCommand(
newInstanceListCmd(client),
)
return cmd
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
package connector
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
"github.com/spf13/cobra"
)
func newInstanceListCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all connector instances",
Example: "sail conn instances list",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
resp, err := client.Get(cmd.Context(), util.ResourceUrl(connectorInstancesEndpoint))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("list connector instances failed. status: %s\nbody: %s", resp.Status, string(body))
}
var instances []instance
err = json.NewDecoder(resp.Body).Decode(&instances)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(instanceColumns)
for _, c := range instances {
table.Append(c.columns())
}
table.Render()
return nil
},
}
return cmd
}

View File

@@ -54,6 +54,18 @@ type TagUpdate struct {
ActiveVersion uint32 `json:"activeVersion"`
}
type instance struct {
ID string `json:"id"`
Name string `json:"name"`
CustomizerId string `json:"connectorCustomizerId"`
}
func (c instance) columns() []string {
return []string{c.ID, c.Name, c.CustomizerId}
}
var instanceColumns = []string{"ID", "Name", "Customizer ID"}
type customizer struct {
ID string `json:"id"`
Name string `json:"name"`

View File

@@ -17,6 +17,7 @@ type Client interface {
Delete(ctx context.Context, url string, params map[string]string) (*http.Response, error)
Post(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error)
Put(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error)
Patch(ctx context.Context, url string, body io.Reader) (*http.Response, error)
}
// SpClient provides access to SP APIs.
@@ -156,6 +157,35 @@ func (c *SpClient) Put(ctx context.Context, url string, contentType string, body
return resp, nil
}
func (c *SpClient) Patch(ctx context.Context, url string, body io.Reader) (*http.Response, error) {
if err := c.ensureAccessToken(ctx); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, c.getUrl(url), body)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+c.accessToken)
if c.cfg.Debug {
dbg, _ := httputil.DumpRequest(req, true)
fmt.Println(string(dbg))
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if c.cfg.Debug {
dbg, _ := httputil.DumpResponse(resp, true)
fmt.Println(string(dbg))
}
return resp, nil
}
func (c *SpClient) ensureAccessToken(ctx context.Context) error {
token, err := config.GetAuthToken()
if err != nil {

View File

@@ -66,6 +66,21 @@ func (mr *MockClientMockRecorder) Get(ctx, url interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), ctx, url)
}
// Patch mocks base method.
func (m *MockClient) Patch(ctx context.Context, url string, body io.Reader) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Patch", ctx, url, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockClientMockRecorder) Patch(ctx, url, body interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClient)(nil).Patch), ctx, url, body)
}
// Post mocks base method.
func (m *MockClient) Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error) {
m.ctrl.T.Helper()

View File

@@ -10,31 +10,31 @@ import (
gomock "github.com/golang/mock/gomock"
)
// MockTerm is a mock of Term interface.
type MockTerm struct {
// MockTerminal is a mock of Terminal interface.
type MockTerminal struct {
ctrl *gomock.Controller
recorder *MockTermMockRecorder
recorder *MockTerminalMockRecorder
}
// MockTermMockRecorder is the mock recorder for MockTerm.
type MockTermMockRecorder struct {
mock *MockTerm
// MockTerminalMockRecorder is the mock recorder for MockTerminal.
type MockTerminalMockRecorder struct {
mock *MockTerminal
}
// NewMockTerm creates a new mock instance.
func NewMockTerm(ctrl *gomock.Controller) *MockTerm {
mock := &MockTerm{ctrl: ctrl}
mock.recorder = &MockTermMockRecorder{mock}
// NewMockTerminal creates a new mock instance.
func NewMockTerminal(ctrl *gomock.Controller) *MockTerminal {
mock := &MockTerminal{ctrl: ctrl}
mock.recorder = &MockTerminalMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTerm) EXPECT() *MockTermMockRecorder {
func (m *MockTerminal) EXPECT() *MockTerminalMockRecorder {
return m.recorder
}
// PromptPassword mocks base method.
func (m *MockTerm) PromptPassword(promptMsg string) (string, error) {
func (m *MockTerminal) PromptPassword(promptMsg string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PromptPassword", promptMsg)
ret0, _ := ret[0].(string)
@@ -43,7 +43,7 @@ func (m *MockTerm) PromptPassword(promptMsg string) (string, error) {
}
// PromptPassword indicates an expected call of PromptPassword.
func (mr *MockTermMockRecorder) PromptPassword(promptMsg interface{}) *gomock.Call {
func (mr *MockTerminalMockRecorder) PromptPassword(promptMsg interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PromptPassword", reflect.TypeOf((*MockTerm)(nil).PromptPassword), promptMsg)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PromptPassword", reflect.TypeOf((*MockTerminal)(nil).PromptPassword), promptMsg)
}