mirror of
https://github.com/LukeHagar/sailpoint-cli.git
synced 2025-12-06 04:21:15 +00:00
PLTCONN-3577: Link & unlink and instance list
This commit is contained in:
@@ -56,6 +56,7 @@ func NewConnCmd(term terminal.Terminal) *cobra.Command {
|
||||
newConnStatsCmd(Client),
|
||||
newConnDeleteCmd(Client),
|
||||
newConnCustomizersCmd(Client),
|
||||
newConnInstancesCmd(Client),
|
||||
)
|
||||
|
||||
return conn
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ func newConnCustomizersCmd(client client.Client) *cobra.Command {
|
||||
newCustomizerUpdateCmd(client),
|
||||
newCustomizerDeleteCmd(client),
|
||||
newCustomizerCreateVersionCmd(client),
|
||||
newCustomizerLinkCmd(client),
|
||||
newCustomizerUnlinkCmd(client),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
71
cmd/connector/customizer_link.go
Normal file
71
cmd/connector/customizer_link.go
Normal 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
|
||||
}
|
||||
66
cmd/connector/customizer_unlink.go
Normal file
66
cmd/connector/customizer_unlink.go
Normal 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
25
cmd/connector/instance.go
Normal 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
|
||||
}
|
||||
55
cmd/connector/instance_list.go
Normal file
55
cmd/connector/instance_list.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user