PLTCONN-1745: Initial commit

This commit is contained in:
fangming-ning-sp
2022-07-21 13:21:00 -05:00
parent c90d4de451
commit 771c9fc136
763 changed files with 274535 additions and 0 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @sailpoint/scrum-platform-connectivity

5
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,5 @@
## Description
What is the intent of this change and why is it being made?
## How Has This Been Tested?
What testing have you done to verify this change?

24
.github/workflows/prb.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: PRB
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
sp-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Test
run: make test
- name: Install
run: make install

24
.github/workflows/prb_windows.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: PRB
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
sp-cli-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Test
run: go test -v -count=1 ./...
- name: Install
run: make install

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.idea
.vscode
# CLI binary
sp-cli

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM golang:1.17-alpine3.14
# Install node, zip, git and make
RUN apk add --no-cache gcc libc-dev npm nodejs-current zip git openssh make
# Install aws cli
RUN apk add --no-cache \
python3 \
py3-pip \
&& pip3 install --upgrade pip \
&& pip3 install --no-cache-dir \
awscli \
&& rm -rf /var/cache/apk/*
# Install sp cli
ADD . /app
WORKDIR /app
RUN go build .
RUN cp sp-cli /usr/local/bin/sp

117
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2022 SailPoint Technologies, Inc. All rights reserved.
*/
@Library('sailpoint/jenkins-release-utils')_
/**
* Jenkins pipeline for building and uploading sp-cli docker image.
*/
pipeline {
agent none
options {
// Aborts job if run time is over 24 hours
timeout(time: 24, unit: 'HOURS')
// Add timestamps to console output
timestamps()
// Don't allow concurrent builds to run
disableConcurrentBuilds()
// Keep builds for a year + 30 days.
buildDiscarder(logRotator(daysToKeepStr: '395'))
}
triggers {
// Poll for changes every 5 minutes.
pollSCM('H/5 * * * *')
}
environment {
// The scrum which owns this component
JIRA_PROJECT = "PLTCONN"
// The name of the build artifact to generate
BUILD_NUMBER = "${env.BUILD_NUMBER}"
// The maximum amount of time (in minutes) to wait for a build
BUILD_TIMEOUT = 20
// The maximum amount of time (in minutes) for tests to take before they are auto failed.
TEST_TIMEOUT = 10
// The maximum amount of time (in minutes) to wait for a deploy
DEPLOY_TIMEOUT = 30
// Which room to report successes & failures too.
SLACK_CHANNEL = "#team-eng-platform-connectivity-jnk"
// The branch releases can be cut from.
RELEASE_BRANCH = "main"
// The name of service being released
SERVICE_NAME = "sp-cli"
}
stages {
stage('Build and push sp-cli') {
when {
branch env.RELEASE_BRANCH
}
steps {
echo "${env.SERVICE_NAME} release pipeline for ${env.BUILD_NUMBER} is starting."
sendSlackNotification(
env.SLACK_CHANNEL,
"${env.SERVICE_NAME} service release pipeline for <${env.BUILD_URL}|${env.BUILD_NUMBER}> is starting.",
utils.NOTIFY_START
)
script {
node {
label 'devaws'
checkout scm
echo "Starting build of ${env.SERVICE_NAME}"
sh("make VERSION=${env.BUILD_NUMBER} docker/push")
//Git Config
sh "git config --global user.email jenkins@construct.identitysoon.com"
sh "git config --global user.name Jenkins"
// Create and push a git tag for build
TAG_NAME= "jenkins/${env.SERVICE_NAME}/${env.BUILD_NUMBER}"
sh "git tag -a -f -m 'Built by Pipeline' ${TAG_NAME}"
sh "git push origin tag ${TAG_NAME}"
}
}
}
}
}
post {
success {
sendSlackNotification(
env.SLACK_CHANNEL,
"${env.SERVICE_NAME} release pipeline for <${env.BUILD_URL}|${env.BUILD_NUMBER}> was successful.",
utils.NOTIFY_SUCCESS
)
}
failure {
sendSlackNotification(
env.SLACK_CHANNEL,
"${env.SERVICE_NAME} release pipeline for <${env.BUILD_URL}|${env.BUILD_NUMBER}> failed.",
utils.NOTIFY_FAILURE
)
}
aborted {
sendSlackNotification(
env.SLACK_CHANNEL,
"${env.SERVICE_NAME} release pipeline for <${env.BUILD_URL}|${env.BUILD_NUMBER}> was aborted.",
utils.NOTIFY_ABORTED
)
}
}
}

31
Makefile Normal file
View File

@@ -0,0 +1,31 @@
VERSION ?= dev
clean:
go clean ./...
mocks:
# Ref: https://github.com/golang/mock
mockgen -source=client/client.go -destination=mocks/client.go -package=mocks
test:
docker build -t cli .
docker run --rm cli go test -v -count=1 ./...
install:
go build -o /usr/local/bin/sp
docker/login:
ifeq ($(JENKINS_URL),) # if $JENKINS_URL is empty
aws ecr --region us-east-1 get-login-password | docker login --username AWS --password-stdin 406205545357.dkr.ecr.us-east-1.amazonaws.com
else
$$(aws ecr get-login --no-include-email --region us-east-1)
endif
docker/build: docker/login
docker build -t sailpoint/sp-cli:$(VERSION) -f Dockerfile .
docker/push: docker/build
docker tag sailpoint/sp-cli:$(VERSION) 406205545357.dkr.ecr.us-east-1.amazonaws.com/sailpoint/sp-cli:$(VERSION)
docker push 406205545357.dkr.ecr.us-east-1.amazonaws.com/sailpoint/sp-cli:$(VERSION)
.PHONY: clean mocks test install .docker/login .docker/build .docker/push

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# SP CLI
SailPoint CLI
## Install
Installation of cli requires Golang. Make sure Golang is installed on your system with version 1.17 or above.
### MacOS and Linux
Run the following make command.
```shell
$ make install
```
After that, make sure you can run the `sp` command.
```shell
$ sp -h
```
### Windows
Install cli using the following command.
```shell
$ go build -o "C:\Program Files\sp-cli\sp"
```
After that, add the following directory to the system PATH parameter. This will only need to be done the first time you install the cli.
```
C:\Program Files\sp-cli
```
Once installed, make sure to use a bash-like shell to run cli commands. You can use MinGW or Git Bash. Make sure you can run the `sp` command.
```shell
$ sp -h
```
## Configuration
Create personal access token @ https://{org}.identitysoon.com/ui/d/user-preferences/personal-access-tokens
Create a config file at "~/.sp/config.yaml"
```yaml
baseURL: https://{org}.api.cloud.sailpoint.com # or baseURL: https://localhost:7100
tokenURL: https://{org}.api.cloud.sailpoint.com/oauth/token
clientSecret: [clientSecret]
clientID: [clientID]
```
You may also specify the config as environment variables:
```shell
$ SP_CLI_BASEURL=http://localhost:7100 \
SP_CLI_TOKENURL=http://{org}.api.cloud.sailpoint.com \
SP_CLI_CLIENTSECRET=xxxx sp conn list
```
This can useful for cases like CI pipelines to avoid having to write the config
file.
## Usage
Note that for all invoke commands, the version flag `-v` is optional. If not provided, the cli will run against the version pointed by the `latest` tag.
```shell
$ sp conn help
$ sp conn init [connectorProjectName]
$ sp conn create [connectorAlias]
$ sp conn update -c [connectorID] -a [connectorAlias]
$ sp conn list
$ sp conn upload -c [connectorID | connectorAlias] -f connector.zip
$ sp conn invoke test-connection -c [connectorID | connectorAlias] -p [config.json] -v [version]
$ sp conn invoke account-list -c [connectorID | connectorAlias] -p [config.json] -v [version]
$ sp conn invoke account-read [identity] -c [connectorID | connectorAlias] -p [config.json] -v [version]
$ sp conn invoke entitlement-list -t [entitlementType] -c [connectorID | connectorAlias] -p [config.json] -v [version]
$ sp conn invoke entitlement-read [identity] -t [entitlementType] -c [connectorID | connectorAlias] -p [config.json] -v [version]
$ sp conn tags create -c [connectorID | connectorAlias] -n [tagName] -v [version]
$ sp conn tags update -c [connectorID | connectorAlias] -n [tagName] -v [version]
$ sp conn tags list -c [connectorID | connectorAlias]
$ sp conn logs
$ sp conn logs tail
$ sp conn stats
```
### Command `conn` is short for `connectors`. Both of the following commands work and they work the exact same way
```shell
$ sp conn list
$ sp connectors list
```

205
client/client.go Normal file
View File

@@ -0,0 +1,205 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package client
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
type Client interface {
Get(ctx context.Context, url 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)
VerifyToken(ctx context.Context, tokenUrl, clientID, secret string) error
}
// SpClient provides access to SP APIs.
type SpClient struct {
cfg SpClientConfig
client *http.Client
accessToken string
}
type SpClientConfig struct {
TokenURL string
ClientID string
ClientSecret string
Debug bool
}
func (c *SpClientConfig) Validate() error {
if c.TokenURL == "" {
return fmt.Errorf("Missing TokenURL configuration value")
}
if c.ClientID == "" {
return fmt.Errorf("Missing ClientID configuration value")
}
if c.ClientSecret == "" {
return fmt.Errorf("Missing ClientSecret configuration value")
}
return nil
}
func NewSpClient(cfg SpClientConfig) Client {
return &SpClient{
cfg: cfg,
client: &http.Client{},
}
}
func (c *SpClient) Get(ctx context.Context, url string) (*http.Response, error) {
if err := c.ensureAccessToken(ctx); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
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) Post(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error) {
if err := c.ensureAccessToken(ctx); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
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) Put(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error) {
if err := c.ensureAccessToken(ctx); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
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 c.cfg.Debug {
dbg, _ := httputil.DumpResponse(resp, true)
fmt.Println(string(dbg))
}
return resp, nil
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
func (c *SpClient) ensureAccessToken(ctx context.Context) error {
err := c.cfg.Validate()
if err != nil {
return err
}
if c.accessToken != "" {
return nil
}
uri, err := url.Parse(c.cfg.TokenURL)
if err != nil {
return err
}
query := &url.Values{}
query.Add("grant_type", "client_credentials")
uri.RawQuery = query.Encode()
data := &url.Values{}
data.Add("client_id", c.cfg.ClientID)
data.Add("client_secret", c.cfg.ClientSecret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), strings.NewReader(data.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to retrieve access token. status %s", resp.Status)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var tResponse tokenResponse
err = json.Unmarshal(raw, &tResponse)
if err != nil {
return err
}
c.accessToken = tResponse.AccessToken
return nil
}
func (c *SpClient) VerifyToken(ctx context.Context, tokenUrl, clientID, secret string) error {
c.cfg.TokenURL = tokenUrl
c.cfg.ClientID = clientID
c.cfg.ClientSecret = secret
return c.ensureAccessToken(ctx)
}

712
client/conn_client.go Normal file
View File

@@ -0,0 +1,712 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"path"
)
// ConnClient is an sp connect client for a specific connector
type ConnClient struct {
client Client
version *int
config json.RawMessage
connectorRef string
endpoint string
}
// NewConnClient returns a client for the provided (connectorID, version, config)
func NewConnClient(client Client, version *int, config json.RawMessage, connectorRef string, endpoint string) *ConnClient {
return &ConnClient{
client: client,
version: version,
config: config,
connectorRef: connectorRef,
endpoint: endpoint,
}
}
// TestConnectionWithConfig provides a way to run std:test-connection with an
// arbitrary config
func (cc *ConnClient) TestConnectionWithConfig(ctx context.Context, cfg json.RawMessage) error {
cmdRaw, err := cc.rawInvokeWithConfig("std:test-connection", []byte("{}"), cfg)
if err != nil {
return err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return newResponseError(resp)
}
return nil
}
// TestConnection runs the std:test-connection command
func (cc *ConnClient) TestConnection(ctx context.Context) (rawResponse []byte, err error) {
cmdRaw, err := cc.rawInvoke("std:test-connection", []byte("{}"))
if err != nil {
return nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, newResponseError(resp)
}
return io.ReadAll(resp.Body)
}
type SimpleKey struct {
ID string `json:"id"`
}
type CompoundKey struct {
LookupID string `json:"lookupId"`
UniqueID string `json:"uniqueId"`
}
type Key struct {
Simple *SimpleKey `json:"simple,omitempty"`
Compound *CompoundKey `json:"compound,omitempty"`
}
func NewSimpleKey(id string) Key {
return Key{
Simple: &SimpleKey{
ID: id,
},
}
}
func NewCompoundKey(lookupID string, uniqueID string) Key {
return Key{
Compound: &CompoundKey{
LookupID: lookupID,
UniqueID: uniqueID,
},
}
}
// Account is an sp connect account. The is used for AccountList, AccountRead
// and AccountUpdate commands.
type Account struct {
Identity string `json:"identity"`
UUID string `json:"uuid"`
Key Key `json:"key"`
Attributes map[string]interface{} `json:"attributes"`
}
func (a *Account) ID() string {
if a.Key.Simple != nil {
return a.Key.Simple.ID
}
if a.Key.Compound != nil {
return a.Key.Compound.LookupID
}
return a.Identity
}
func (a *Account) UniqueID() string {
if a.Key.Compound != nil {
return a.Key.Compound.UniqueID
}
if a.UUID != "" {
return a.UUID
}
return ""
}
// AccountList lists all accounts
func (cc *ConnClient) AccountList(ctx context.Context) (accounts []Account, rawResponse []byte, err error) {
cmdRaw, err := cc.rawInvoke("std:account:list", []byte("{}"))
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
for {
acct := &Account{}
err := decoder.Decode(acct)
if err != nil {
if err == io.EOF {
break
}
return nil, nil, err
}
accounts = append(accounts, *acct)
}
return accounts, rawResponse, nil
}
type readInput struct {
Identity string `json:"identity"`
Key Key `json:"key"`
Type string `json:"type,omitempty"`
}
// AccountRead reads a specific account
func (cc *ConnClient) AccountRead(ctx context.Context, id string, uniqueID string) (account *Account, rawResponse []byte, err error) {
input := readInput{
Identity: id,
}
if uniqueID == "" {
input.Key = NewSimpleKey(id)
} else {
input.Key = NewCompoundKey(id, uniqueID)
}
inRaw, err := json.Marshal(input)
if err != nil {
return nil, nil, err
}
cmdRaw, err := cc.rawInvoke("std:account:read", inRaw)
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
acct := &Account{}
err = decoder.Decode(acct)
if err != nil {
return nil, nil, err
}
return acct, rawResponse, nil
}
// AccountCreate creats an account
func (cc *ConnClient) AccountCreate(ctx context.Context, identity *string, attributes map[string]interface{}) (account *Account, raw []byte, err error) {
input, err := json.Marshal(map[string]interface{}{
"identity": identity,
"attributes": attributes,
})
if err != nil {
return nil, nil, err
}
cmdRaw, err := cc.rawInvoke("std:account:create", input)
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
acct := &Account{}
err = json.Unmarshal(raw, acct)
if err != nil {
return nil, nil, err
}
return acct, raw, nil
}
// AccountDelete deletes an account
func (cc *ConnClient) AccountDelete(ctx context.Context, id string, uniqueID string) (raw []byte, err error) {
input := readInput{
Identity: id,
}
if uniqueID == "" {
input.Key = NewSimpleKey(id)
} else {
input.Key = NewCompoundKey(id, uniqueID)
}
inRaw, err := json.Marshal(input)
if err != nil {
return nil, err
}
cmdRaw, err := cc.rawInvoke("std:account:delete", inRaw)
if err != nil {
return nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, newResponseError(resp)
}
return nil, nil
}
// AttributeChange describes a change to a specific attribute
type AttributeChange struct {
Op string `json:"op"`
Attribute string `json:"attribute"`
Value interface{} `json:"value"`
}
// AccountUpdate updates an account
func (cc *ConnClient) AccountUpdate(ctx context.Context, id string, uniqueID string, changes []AttributeChange) (account *Account, rawResponse []byte, err error) {
type accountUpdate struct {
readInput
Changes []AttributeChange `json:"changes"`
}
input := readInput{
Identity: id,
}
if uniqueID == "" {
input.Key = NewSimpleKey(id)
} else {
input.Key = NewCompoundKey(id, uniqueID)
}
inRaw, err := json.Marshal(accountUpdate{
readInput: input,
Changes: changes,
})
if err != nil {
return nil, nil, err
}
cmdRaw, err := cc.rawInvoke("std:account:update", inRaw)
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
acct := &Account{}
err = decoder.Decode(acct)
if err != nil {
return nil, nil, err
}
return account, rawResponse, nil
}
// AccountDiscoverSchema discovers schema for accounts
func (cc *ConnClient) AccountDiscoverSchema(ctx context.Context) (accountSchema *AccountSchema, rawResponse []byte, err error) {
cmdRaw, err := cc.rawInvoke("std:account:discover-schema", []byte("{}"))
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
schema := &AccountSchema{}
err = decoder.Decode(schema)
if err != nil {
return nil, nil, err
}
return schema, rawResponse, nil
}
// Entitlement is an sp connect entitlement, used for EntitlementList and
// EntitlementRead
type Entitlement struct {
Identity string `json:"identity"`
UUID string `json:"uuid"`
Key Key `json:"key"`
Attributes map[string]interface{} `json:"attributes"`
}
func (a *Entitlement) ID() string {
if a.Key.Simple != nil {
return a.Key.Simple.ID
}
if a.Key.Compound != nil {
return a.Key.Compound.LookupID
}
return a.Identity
}
func (a *Entitlement) UniqueID() string {
if a.Key.Compound != nil {
return a.Key.Compound.UniqueID
}
if a.UUID != "" {
return a.UUID
}
return ""
}
// EntitlementList lists all entitlements
func (cc *ConnClient) EntitlementList(ctx context.Context, t string) (entitlements []Entitlement, rawResponse []byte, err error) {
cmdRaw, err := cc.rawInvoke("std:entitlement:list", []byte(fmt.Sprintf(`{"type": %q}`, t)))
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
for {
e := &Entitlement{}
err := decoder.Decode(e)
if err != nil {
if err == io.EOF {
break
}
return nil, nil, err
}
entitlements = append(entitlements, *e)
}
return entitlements, rawResponse, nil
}
// EntitlementRead reads all entitlements
func (cc *ConnClient) EntitlementRead(ctx context.Context, id string, uniqueID string, t string) (entitlement *Entitlement, rawResponse []byte, err error) {
input := readInput{
Identity: id,
Type: t,
}
if uniqueID == "" {
input.Key = NewSimpleKey(id)
} else {
input.Key = NewCompoundKey(id, uniqueID)
}
inRaw, err := json.Marshal(input)
if err != nil {
return nil, nil, err
}
cmdRaw, err := cc.rawInvoke("std:entitlement:read", inRaw)
if err != nil {
return nil, nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
decoder := json.NewDecoder(bytes.NewReader(rawResponse))
e := &Entitlement{}
err = decoder.Decode(e)
if err != nil {
return nil, nil, err
}
return e, rawResponse, nil
}
type ReadSpecOutput struct {
Specification *ConnSpec `json:"specification"`
}
// SpecRead issues a custom:config command which is expected to return the
// connector specification. This is an experimental command used by the
// validation suite.
func (cc *ConnClient) SpecRead(ctx context.Context) (connSpec *ConnSpec, err error) {
cmdRaw, err := cc.rawInvoke("std:spec:read", []byte(`{}`))
if err != nil {
return nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, newResponseError(resp)
}
decoder := json.NewDecoder(resp.Body)
cfg := &ReadSpecOutput{}
err = decoder.Decode(cfg)
if err != nil {
return nil, err
}
return cfg.Specification, nil
}
// Invoke allows you to send an arbitrary json payload as a command
func (cc *ConnClient) Invoke(ctx context.Context, cmdType string, input json.RawMessage) (rawResponse []byte, err error) {
cmdRaw, err := cc.rawInvoke(cmdType, input)
if err != nil {
return nil, err
}
resp, err := cc.client.Post(ctx, connResourceUrl(cc.endpoint, cc.connectorRef, "invoke"), "application/json", bytes.NewReader(cmdRaw))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, newResponseError(resp)
}
rawResponse, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return rawResponse, nil
}
func newResponseError(resp *http.Response) error {
body, _ := io.ReadAll(resp.Body)
var errorPayload interface{}
err := json.Unmarshal(body, &errorPayload)
if err != nil {
return fmt.Errorf("non-200 response: %s (body %s)", resp.Status, string(body))
} else {
pretty, err := json.MarshalIndent(errorPayload, "", "\t")
if err != nil {
return fmt.Errorf("non-200 response: %s (body %s)", resp.Status, string(body))
} else {
return fmt.Errorf("non-200 response: %s (body %s)", resp.Status, string(pretty))
}
}
}
type AccountCreateTemplateField struct {
// Deprecated
Name string `json:"name"`
Key string `json:"key"`
Type string `json:"type"`
Required bool `json:"required"`
InitialValue TemplateInitialValue `json:"initialValue"`
}
type TemplateInitialValue struct {
Type string `json:"type"`
Attributes TemplateAttributes `json:"attributes"`
}
type TemplateAttributes struct {
Name string `json:"name"`
Value interface{} `json:"value"`
Template string `json:"template"`
}
type AccountCreateTemplate struct {
Fields []AccountCreateTemplateField `json:"fields"`
}
type AccountSchema struct {
DisplayAttribute string `json:"displayAttribute"`
GroupAttribute string `json:"groupAttribute"`
IdentityAttribute string `json:"identityAttribute"`
Attributes []AccountSchemaAttribute `json:"attributes"`
}
type EntitlementSchema struct {
Type string `json:"type"`
DisplayName string `json:"displayName"`
IdentityAttribute string `json:"identityAttribute"`
Attributes []EntitlementSchemaAttribute `json:"attributes"`
}
type AccountSchemaAttribute struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Entitlement bool `json:"entitlement"`
Managed bool `json:"managed"`
Multi bool `json:"multi"`
// Writable is not a standard spec field, yet
Writable bool `json:"writable"`
}
type EntitlementSchemaAttribute struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Multi bool `json:"multi"`
}
// ConnSpec is a connector config. See ConnConfig method.
type ConnSpec struct {
Name string `json:"name"`
Commands []string `json:"commands"`
AccountCreateTemplate AccountCreateTemplate `json:"accountCreateTemplate"`
AccountSchema AccountSchema `json:"accountSchema"`
EntitlementSchemas []EntitlementSchema `json:"entitlementSchemas"`
}
func (cc *ConnClient) rawInvoke(cmdType string, input json.RawMessage) (json.RawMessage, error) {
return cc.rawInvokeWithConfig(cmdType, input, cc.config)
}
func (cc *ConnClient) rawInvokeWithConfig(cmdType string, input json.RawMessage, cfg json.RawMessage) (json.RawMessage, error) {
log.Printf("Running %q with %q", cmdType, input)
invokeCmd := invokeCommand{
ConnectorRef: cc.connectorRef,
Type: cmdType,
Config: cfg,
Input: input,
}
if cc.version == nil {
invokeCmd.Tag = "latest"
} else {
invokeCmd.Version = cc.version
}
return json.Marshal(invokeCmd)
}
const connectorsEndpoint = "/beta/platform-connectors"
func connResourceUrl(endpoint string, resourceParts ...string) string {
u, err := url.Parse(endpoint)
if err != nil {
log.Fatalf("invalid endpoint: %s (%q)", err, endpoint)
}
u.Path = path.Join(append([]string{u.Path}, resourceParts...)...)
return u.String()
}
type invokeCommand struct {
ConnectorRef string `json:"connectorRef"`
Version *int `json:"version,omitempty"`
Tag string `json:"tag,omitempty"`
Type string `json:"type"`
Config json.RawMessage `json:"config"`
Input json.RawMessage `json:"input"`
}

234
client/logs_client.go Normal file
View File

@@ -0,0 +1,234 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"net/url"
"path"
"time"
)
const TimeFormatLocal = `2006-01-02T15:04:05.000-07:00`
const TimeLocationLocal = "Local"
type LogsClient struct {
client Client
endpoint string
}
// NewConnClient returns a client for the provided (connectorID, version, config)
func NewLogsClient(client Client, endpoint string) *LogsClient {
return &LogsClient{
client: client,
endpoint: endpoint,
}
}
const LogsEndpoint = "/beta/platform-logs/query"
const StatsEndpoint = "/beta/platform-logs/stats"
func logsResourceUrl(endpoint string, queryParms *map[string]string, resourceParts ...string) string {
u, err := url.Parse(endpoint)
if err != nil {
log.Fatalf("invalid endpoint: %s (%q)", err, endpoint)
}
u.Path = path.Join(append([]string{u.Path}, resourceParts...)...)
//set query parms
if queryParms != nil {
q := u.Query()
for key, value := range *queryParms {
q.Set(key, value)
}
u.RawQuery = q.Encode()
}
return u.String()
}
type LogMessage struct {
TenantID string `json:"tenantID"`
Timestamp time.Time `json:"timestamp"`
Level string `json:"level"`
Event string `json:"event"`
Component string `json:"component"`
TargetID string `json:"targetID"`
TargetName string `json:"targetName"`
RequestID string `json:"requestID"`
Message interface{} `json:"message"`
}
func (l LogMessage) RawString() string {
json, err := json.Marshal(l)
if err != nil {
return fmt.Sprintf("%v", l)
}
return string(json)
}
func (l LogMessage) MessageString() string {
if msgJson, ok := l.Message.(map[string]interface{}); ok {
if jsonString, err := json.Marshal(msgJson); err == nil {
return fmt.Sprintf("%v", string(jsonString))
}
}
return fmt.Sprintf("%v", l.Message)
}
func (l LogMessage) TimestampFormatted() string {
loc, err := time.LoadLocation(TimeLocationLocal)
if err != nil {
return l.Timestamp.Format(time.RFC3339)
}
return l.Timestamp.In(loc).Format(TimeFormatLocal)
}
type LogEvents struct {
// The token for the next set of items in the forward direction. If you have reached the
// end of the stream, it returns the same token you passed in.
NextToken *string `json:"nextToken,omitempty"`
//The log messages
Logs []LogMessage `json:"logs"`
}
type LogFilter struct {
StartTime *time.Time `json:"startTime,omitempty"`
EndTime *time.Time `json:"endTime,omitempty"`
Component string `json:"component,omitempty"`
LogLevels []string `json:"logLevels,omitempty"`
TargetID string `json:"targetID,omitempty"`
TargetName string `json:"targetName,omitempty"`
RequestID string `json:"requestID,omitempty"`
Event string `json:"event,omitempty"`
}
type LogInput struct {
Filter LogFilter `json:"filter"`
NextToken string `json:"nextToken"`
}
func (c *LogsClient) GetLogs(ctx context.Context, logInput LogInput) (*LogEvents, error) {
input, err := json.Marshal(logInput)
if err != nil {
return nil, err
}
resp, err := c.client.Post(ctx, logsResourceUrl(c.endpoint, nil), "application/json", bytes.NewReader(input))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error retrieving logs, non-200 response: %s body: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var logEvents LogEvents
err = json.Unmarshal(raw, &logEvents)
if err != nil {
return nil, err
}
return &logEvents, nil
}
type TenantStats struct {
TenantID string `json:"tenantID"`
ConnectorStats []ConnectorStats `json:"connectors"`
}
type ConnectorStats struct {
ConnectorID string `json:"connectorID"`
ConnectorAlias string `json:"alias"`
Stats []CommandStats `json:"stats"`
}
type CommandStats struct {
CommandType string `json:"commandType"`
InvocationCount uint32 `json:"invocationCount"`
ErrorCount uint32 `json:"errorCount"`
ErrorRate float64 `json:"errorRate"`
ElapsedAvg float64 `json:"elapsedAvg"`
Elapsed95th float64 `json:"elapsed95th"`
}
func (c CommandStats) Columns() []string {
return []string{c.CommandType,
fmt.Sprintf("%v", c.InvocationCount),
fmt.Sprintf("%v", c.ErrorCount),
fmt.Sprintf("%.2f", c.ErrorRate),
fmt.Sprintf("%v", timeDuration(c.ElapsedAvg)),
fmt.Sprintf("%v", timeDuration(c.Elapsed95th))}
}
func timeDuration(n float64) time.Duration {
nRounded := math.Round(n*100) / 100
return time.Duration(nRounded * float64(time.Millisecond))
}
func (c *LogsClient) GetStats(ctx context.Context, from time.Time, connectorID string) (*TenantStats, error) {
queryFilter := fmt.Sprintf(`from eq "%v"`, from.Format(time.RFC3339))
if connectorID != "" {
queryFilter = queryFilter + fmt.Sprintf(` and connectorID eq "%v"`, connectorID)
}
resp, err := c.client.Get(ctx, logsResourceUrl(c.endpoint, &map[string]string{"filters": queryFilter}))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error retrieving logs, non-200 response: %s. Body: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var tenantStats TenantStats
err = json.Unmarshal(raw, &tenantStats)
if err != nil {
return nil, err
}
return &tenantStats, nil
}
// Define the order of time formats to attempt to use to parse our input absolute time
var absoluteTimeFormats = []string{
time.RFC3339,
"2006-01-02",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05.000-07:00",
"2006-01-02T15:04:05.000Z",
}
// Parse the input string into a time.Time object.
// Provide the currentTime as a parameter to support relative time.
func ParseTime(timeStr string, currentTime time.Time) (time.Time, error) {
relative, err := time.ParseDuration(timeStr)
if err == nil {
return currentTime.Add(-relative), nil
}
// Iterate over available absolute time formats until we find one that works
for _, timeFormat := range absoluteTimeFormats {
absolute, err := time.Parse(timeFormat, timeStr)
if err == nil {
return absolute, err
}
}
return time.Time{}, fmt.Errorf("could not parse relative or absolute time")
}

97
cmd/conn.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"path"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
const (
connectorsEndpoint = "/beta/platform-connectors"
)
func connResourceUrl(endpoint string, resourceParts ...string) string {
u, err := url.Parse(endpoint)
if err != nil {
log.Fatalf("invalid endpoint: %s (%q)", err, endpoint)
}
u.Path = path.Join(append([]string{u.Path}, resourceParts...)...)
return u.String()
}
func newConnCmd(client client.Client) *cobra.Command {
conn := &cobra.Command{
Use: "connectors",
Short: "Manage Connectors",
Aliases: []string{"conn"},
Run: func(cmd *cobra.Command, args []string) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cmd.UsageString())
},
}
conn.PersistentFlags().StringP("conn-endpoint", "e", viper.GetString("baseurl")+connectorsEndpoint, "Override connectors endpoint")
conn.AddCommand(
newConnInitCmd(),
newConnListCmd(client),
newConnGetCmd(client),
newConnUpdateCmd(client),
newConnCreateCmd(client),
newConnCreateVersionCmd(client),
newConnVersionsCmd(client),
newConnInvokeCmd(client),
newConnValidateCmd(client),
newConnTagCmd(client),
newConnValidateSourcesCmd(client),
newConnLogsCmd(client),
newConnStatsCmd(client),
)
return conn
}
type devConfig struct {
ID string `yaml:"id"`
Config map[string]interface{} `yaml:"config"`
}
func bindDevConfig(flags *pflag.FlagSet) {
cfg := &devConfig{}
raw, err := os.ReadFile(".dev.yaml")
if err != nil {
return
}
err = yaml.Unmarshal(raw, cfg)
if err != nil {
log.Printf("Failed to unmarshal '.dev.yaml': %s", err)
return
}
if cfg.ID != "" {
f := flags.Lookup("id")
if f != nil && !f.Changed {
flags.Set("id", cfg.ID)
}
}
if len(cfg.Config) > 0 {
f := flags.Lookup("config-json")
if f != nil && !f.Changed {
raw, err := json.Marshal(cfg.Config)
if err != nil {
panic(fmt.Sprintf("Failed to encode config as json: %s", err))
}
flags.Set("config-json", string(raw))
}
}
}

128
cmd/conn_configure.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bufio"
"context"
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"log"
"path/filepath"
"os"
)
const (
baseURLTemplate = "https://%s.api.cloud.sailpoint.com"
tokenURLTemplate = "%s/oauth/token"
configFolder = ".sp"
configYamlFile = "config.yaml"
)
type OrgConfig struct {
Org string `mapstructure:"org"`
BaseUrl string `mapstructure:"baseURL"`
TokenUrl string `mapstructure:"tokenURL"`
ClientSecret string `mapstructure:"clientSecret"`
ClientID string `mapstructure:"clientID"`
}
func newConfigureCmd(client client.Client) *cobra.Command {
conn := &cobra.Command{
Use: "configure",
Short: "Configure CLI",
Aliases: []string{"conf"},
RunE: func(cmd *cobra.Command, args []string) error {
config, err := getConfigureParamsFromStdin()
if err != nil {
return err
}
err = updateConfigFile(config)
if err != nil {
return err
}
err = client.VerifyToken(context.Background(), config.TokenUrl, config.ClientID, config.ClientSecret)
if err != nil {
return err
}
return nil
},
}
return conn
}
func updateConfigFile(conf *OrgConfig) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
if _, err := os.Stat(filepath.Join(home, ".sp")); os.IsNotExist(err) {
err = os.Mkdir(filepath.Join(home, ".sp"), 0777)
if err != nil {
log.Printf("failed to create .sp folder for config. %v", err)
}
}
viper.Set("org", conf.Org)
viper.Set("baseUrl", conf.BaseUrl)
viper.Set("tokenUrl", conf.TokenUrl)
viper.Set("clientSecret", conf.ClientSecret)
viper.Set("clientID", conf.ClientID)
err = viper.WriteConfig()
if err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err = viper.SafeWriteConfig()
if err != nil {
return err
}
} else {
return err
}
}
return nil
}
func getConfigureParamsFromStdin() (*OrgConfig, error) {
conf := &OrgConfig{}
paramsNames := []string{
"Org Name: ",
"Personal Access Token Client ID: ",
"Personal Access Token Client Secret: ",
}
scanner := bufio.NewScanner(os.Stdin)
for _, pm := range paramsNames {
fmt.Print(pm)
scanner.Scan()
value := scanner.Text()
if value == "" {
return nil, fmt.Errorf("%s parameter is empty", pm[:len(pm)-2])
}
switch pm {
case paramsNames[0]:
conf.Org = value
conf.BaseUrl = fmt.Sprintf(baseURLTemplate, value)
conf.TokenUrl = fmt.Sprintf(tokenURLTemplate, conf.BaseUrl)
case paramsNames[1]:
conf.ClientID = value
case paramsNames[2]:
conf.ClientSecret = value
}
}
return conf, nil
}

76
cmd/conn_create.go Normal file
View File

@@ -0,0 +1,76 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnCreateCmd(client client.Client) *cobra.Command {
// TODO: Clean up and not send display name
type create struct {
DisplayName string `json:"displayName"`
Alias string `json:"alias"`
}
cmd := &cobra.Command{
Use: "create <connector-name>",
Short: "Create Connector",
Long: "Create Connector",
Example: "sp connectors create \"My-Connector\"",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
alias := args[0]
if alias == "" {
return fmt.Errorf("connector alias cannot be empty")
}
raw, err := json.Marshal(create{DisplayName: alias, Alias: alias})
if err != nil {
return err
}
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Post(cmd.Context(), connResourceUrl(endpoint), "application/json", 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("create connector failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var conn connector
err = json.Unmarshal(raw, &conn)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(connectorColumns)
table.Append(conn.columns())
table.Render()
return nil
},
}
return cmd
}

45
cmd/conn_create_test.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnCreateCmd(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Post(gomock.Any(), gomock.Any(), "application/json", gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(1)
cmd := newConnCreateCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{"test-connector"})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if len(string(out)) == 0 {
t.Errorf("error empty out")
}
}

192
cmd/conn_create_version.go Normal file
View File

@@ -0,0 +1,192 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnCreateVersionCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "upload",
Short: "Upload Connector",
Long: "Upload Connector",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
archivePath := cmd.Flags().Lookup("file").Value.String()
tagName := cmd.Flags().Lookup("tag").Value.String()
f, err := os.Open(archivePath)
if err != nil {
return err
}
info, err := f.Stat()
if err != nil {
return err
}
_, err = zip.NewReader(f, info.Size())
if err != nil {
return err
}
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return err
}
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Post(cmd.Context(), connResourceUrl(endpoint, connectorRef, "versions"), "application/zip", f)
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("upload failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var v connectorVersion
err = json.Unmarshal(raw, &v)
if err != nil {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(connectorVersionColumns)
table.Append(v.columns())
table.Render()
if tagName != "" {
resp, err := client.Get(cmd.Context(), connResourceUrl(endpoint, connectorRef, "tags", tagName))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
// If tag exists, update the tag with new version.
// Otherwise create the tag
if resp.StatusCode == http.StatusOK {
err = updateTagWithVersion(cmd, client, endpoint, connectorRef, tagName, uint32(v.Version))
} else {
err = createTagWithVersion(cmd, client, endpoint, connectorRef, tagName, uint32(v.Version))
}
if err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringP("id", "c", "", "Connector ID or Alias")
_ = cmd.MarkFlagRequired("id")
cmd.Flags().StringP("file", "f", "", "ZIP Archive")
_ = cmd.MarkFlagRequired("file")
cmd.Flags().StringP("tag", "t", "", "Update a tag with this version. Tag will be created if not exist. (Optional)")
bindDevConfig(cmd.Flags())
return cmd
}
// updateTagWithVersion updates an exiting tag with a new version of connector code
func updateTagWithVersion(cmd *cobra.Command, client client.Client, endpoint string, connectorID string, tagName string, version uint32) error {
raw, err := json.Marshal(TagUpdate{ActiveVersion: version})
if err != nil {
return err
}
resp, err := client.Put(cmd.Context(), connResourceUrl(endpoint, connectorID, "tags", tagName), "application/json", bytes.NewReader(raw))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("update connector tag failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var t tag
err = json.Unmarshal(raw, &t)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(tagColumns)
table.Append(t.columns())
table.Render()
return nil
}
// createTagWithVersion creates a tag pointing to a version of connector code
func createTagWithVersion(cmd *cobra.Command, client client.Client, endpoint string, connectorID string, tagName string, version uint32) error {
raw, err := json.Marshal(TagCreate{TagName: tagName, ActiveVersion: version})
if err != nil {
return err
}
resp, err := client.Post(cmd.Context(), connResourceUrl(endpoint, connectorID, "tags"), "application/json", 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("create connector tag failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var t tag
err = json.Unmarshal(raw, &t)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(tagColumns)
table.Append(t.columns())
table.Render()
return nil
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnCreateVersionCmd_missingRequiredFlags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Post(gomock.Any(), gomock.Any(), "application/zip", gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(0)
cmd := newConnCreateVersionCmd(client)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}
func TestNewConnCreateVersionCmd_invalidZip(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Post(gomock.Any(), gomock.Any(), "application/zip", gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(0)
cmd := newConnCreateVersionCmd(client)
cmd.SetArgs([]string{"-c", "mockConnectorId", "-f", "not-exist.zip"})
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}

64
cmd/conn_get.go Normal file
View File

@@ -0,0 +1,64 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnGetCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Get Connector",
Long: "Get Connector",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Get(cmd.Context(), connResourceUrl(endpoint, connectorRef))
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("get connector failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var conn connector
err = json.Unmarshal(raw, &conn)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(connectorColumns)
table.Append(conn.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("id", "c", "", "Connector ID or Alias")
_ = cmd.MarkFlagRequired("id")
bindDevConfig(cmd.Flags())
return cmd
}

61
cmd/conn_get_test.go Normal file
View File

@@ -0,0 +1,61 @@
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnGetCmd_missingRequiredFlags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(0)
cmd := newConnGetCmd(client)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}
func TestNewConnGetCmd(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(1)
cmd := newConnGetCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{"-c", "test-connector"})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if len(string(out)) == 0 {
t.Errorf("error empty out")
}
}

182
cmd/conn_init.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"embed"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/spf13/cobra"
)
//go:embed static/*
var staticDir embed.FS
const (
staticDirName = "static"
packageJsonName = "package.json"
connectorSpecName = "connector-spec.json"
)
// newConnInitCmd is a connectors subcommand used to initialize a new connector project.
// It accepts one argument, project name, and generates appropriate directories and files
// to set up a working, sample project.
func newConnInitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init <connector-name>",
Short: "Initialize new connector project",
Long: `init sets up a new TypeScript project with sample connector included for reference.`,
Example: "sp connectors init \"My Connector\"",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
projName := args[0]
if projName == "" {
printError(cmd.ErrOrStderr(), errors.New("connector name cannot be empty"))
return
}
if f, err := os.Stat(projName); err == nil && f.IsDir() && f.Name() == projName {
printError(cmd.ErrOrStderr(), fmt.Errorf("Error: project '%s' already exists.\n", projName))
return
}
if err := createDir(projName); err != nil {
_ = os.RemoveAll(projName)
printError(cmd.ErrOrStderr(), err)
return
}
err := fs.WalkDir(staticDir, staticDirName, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Name() == staticDirName {
return nil
}
if d.IsDir() {
if err := createDir(filepath.Join(projName, d.Name())); err != nil {
return err
}
} else {
fileName := filepath.Join(projName, strings.TrimPrefix(path, staticDirName))
data, err := staticDir.ReadFile(path)
if err != nil {
return err
}
if err := createFile(fileName, data); err != nil {
return err
}
}
if d.Name() == packageJsonName || d.Name() == connectorSpecName {
fileAbsPath, err := filepath.Abs(filepath.Join(projName, strings.TrimPrefix(path, staticDirName)))
if err != nil {
return err
}
if err := createFileFromTemplate(projName, d.Name(), fileAbsPath); err != nil {
return err
}
return nil
}
return nil
})
if err != nil {
_ = os.RemoveAll(projName)
printError(cmd.ErrOrStderr(), err)
return
}
printDir(cmd.OutOrStdout(), projName, 0)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully created project '%s'.\nRun `npm install` to install dependencies.\n", projName)
},
}
return cmd
}
// createFileFromTemplate fills the template file withe parameters,
// then creates the file in the target location
func createFileFromTemplate(projName string, filename string, fileAbsPath string) error {
t, err := template.ParseFiles(fileAbsPath)
if err != nil {
return err
}
templateData := struct {
ProjectName string
}{
ProjectName: projName,
}
packageJson := &bytes.Buffer{}
if err := t.Execute(packageJson, templateData); err != nil {
return err
}
if err := createFile(filepath.Join(projName, filename), packageJson.Bytes()); err != nil {
return err
}
return nil
}
// createDir is a wrapper of os.MkdirAll, to generate project directories
func createDir(path string) error {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
return nil
}
// createFile is a wrapper of os.WriteFile, to generate new source templates
func createFile(name string, data []byte) error {
if err := os.WriteFile(name, data, 0644); err != nil {
return err
}
return nil
}
// printError prints error in uniform format
func printError(w io.Writer, err error) {
_, _ = fmt.Fprintf(w, "%v", err)
}
// printFile prints file branch
func printFile(w io.Writer, name string, depth int) {
_, _ = fmt.Fprintf(w, "%s|-- %s\n", strings.Repeat("| ", depth), filepath.Base(name))
}
// printDir prints directory tree from specified path
func printDir(w io.Writer, path string, depth int) {
entries, err := os.ReadDir(path)
if err != nil {
_, _ = fmt.Fprintf(w, "error reading %s: %v", path, err)
return
}
printFile(w, path, depth)
for _, entry := range entries {
if entry.IsDir() {
printDir(w, filepath.Join(path, entry.Name()), depth+1)
} else {
printFile(w, entry.Name(), depth+1)
}
}
}

103
cmd/conn_init_test.go Normal file
View File

@@ -0,0 +1,103 @@
package cmd
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestNewConnInitCmd_noArgs(t *testing.T) {
cmd := newConnInitCmd()
cmd.SetArgs([]string{})
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}
func TestNewConnInitCmd_emptyName(t *testing.T) {
cmd := newConnInitCmd()
b := new(bytes.Buffer)
cmd.SetErr(b)
cmd.SetArgs([]string{""})
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if !strings.Contains(string(out), "connector name cannot be empty") {
t.Errorf("expected: %s, actual: %s", "Error: connector name cannot be empty", string(out))
}
}
func TestNewConnInitCmd(t *testing.T) {
cmd := newConnInitCmd()
testProjName := "test-connector-project"
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{testProjName})
if err := cmd.Execute(); err != nil {
t.Fatalf("error execute cmd: %v", err)
}
defer func(command *exec.Cmd) {
_ = command.Run()
}(exec.Command("rm", "-rf", testProjName))
expectedEntries := map[string]bool{
testProjName: true,
filepath.Join(testProjName, packageJsonName): true,
filepath.Join(testProjName, connectorSpecName): true,
filepath.Join(testProjName, "src"): true,
filepath.Join(testProjName, "src", "index.spec.ts"): true,
filepath.Join(testProjName, "src", "index.ts"): true,
filepath.Join(testProjName, "src", "my-client.spec.ts"): true,
filepath.Join(testProjName, "src", "my-client.ts"): true,
filepath.Join(testProjName, "tsconfig.json"): true,
filepath.Join(testProjName, ".gitignore"): true,
}
numEntries := 0
err := filepath.Walk(filepath.Join(".", testProjName),
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if _, ok := expectedEntries[path]; !ok {
t.Errorf("error file not created: %s", path)
}
numEntries++
return nil
})
if err != nil {
t.Errorf("error walk '%s' dir: %v", testProjName, err)
}
if numEntries != len(expectedEntries) {
t.Errorf("expected entries: %d, actual: %d", len(expectedEntries), numEntries)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if !strings.Contains(string(out), "Successfully created project") {
t.Errorf("expected out to contain '%s', actual: %s", "Successfully created project", string(out))
}
}

113
cmd/conn_invoke.go Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"os"
"strconv"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
const (
stdAccountCreate = "std:account:create"
stdAccountList = "std:account:list"
stdAccountRead = "std:account:read"
stdAccountUpdate = "std:account:update"
stdAccountDelete = "std:account:delete"
stdEntitlementList = "std:entitlement:list"
stdEntitlementRead = "std:entitlement:read"
stdTestConnection = "std:test-connection"
)
func newConnInvokeCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "invoke",
Short: "Invoke Command on a connector",
Run: func(cmd *cobra.Command, args []string) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cmd.UsageString())
},
}
cmd.PersistentFlags().StringP("version", "v", "", "Optional. Run against a specific version if provided. Otherwise run against the latest tag.")
cmd.PersistentFlags().StringP("config-path", "p", "", "Path to config to use for commands")
cmd.PersistentFlags().StringP("config-json", "", "", "Config JSON to use for commands")
cmd.PersistentFlags().StringP("id", "c", "", "Connector ID or Alias")
_ = cmd.MarkPersistentFlagRequired("id")
cmd.AddCommand(
newConnInvokeTestConnectionCmd(client),
newConnInvokeAccountCreateCmd(client),
newConnInvokeAccountDiscoverSchemaCmd(client),
newConnInvokeAccountListCmd(client),
newConnInvokeAccountReadCmd(client),
newConnInvokeAccountUpdateCmd(client),
newConnInvokeAccountDeleteCmd(client),
newConnInvokeEntitlementListCmd(client),
newConnInvokeEntitlementReadCmd(client),
newConnInvokeRaw(client),
)
bindDevConfig(cmd.PersistentFlags())
return cmd
}
func invokeConfig(cmd *cobra.Command) (json.RawMessage, error) {
if cmd.Flags().Lookup("config-path").Value.String() == "" && cmd.Flags().Lookup("config-json").Value.String() == "" {
return nil, fmt.Errorf("Either config-path or config-json must be set")
}
if cmd.Flags().Lookup("config-json") != nil && cmd.Flags().Lookup("config-json").Value.String() != "" {
return json.RawMessage(cmd.Flags().Lookup("config-json").Value.String()), nil
}
return os.ReadFile(cmd.Flags().Lookup("config-path").Value.String())
}
type invokeCommand struct {
ConnectorID string `json:"connectorID"`
Version *int `json:"version"`
Type string `json:"type"`
Config json.RawMessage `json:"config"`
Input json.RawMessage `json:"input"`
}
func connClient(cmd *cobra.Command, spClient client.Client) (*client.ConnClient, error) {
connectorRef := cmd.Flags().Lookup("id").Value.String()
version := cmd.Flags().Lookup("version").Value.String()
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
var v *int
if version != "" {
ver, err := strconv.Atoi(version)
if err != nil {
return nil, err
}
v = &ver
}
cfg, err := invokeConfig(cmd)
if err != nil {
return nil, err
}
cc := client.NewConnClient(spClient, v, cfg, connectorRef, endpoint)
return cc, nil
}
func connClientWithCustomParams(spClient client.Client, cfg json.RawMessage, connectorID, version, endpoint string) (*client.ConnClient, error) {
v, err := strconv.Atoi(version)
if err != nil {
return nil, err
}
cc := client.NewConnClient(spClient, &v, cfg, connectorID, endpoint)
return cc, nil
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountCreateCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-create [identity] [--attributes <value>]",
Short: "Invoke a std:account:create command",
Example: `sp connectors invoke account-create john.doe --attributes '{"email": "john.doe@example.com"}'`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
var identity *string = nil
if len(args) > 0 {
identity = &args[0]
}
attributesRaw := cmd.Flags().Lookup("attributes").Value.String()
var attributes map[string]interface{}
if err := json.Unmarshal([]byte(attributesRaw), &attributes); err != nil {
return err
}
_, rawResponse, err := cc.AccountCreate(ctx, identity, attributes)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
cmd.Flags().StringP("attributes", "a", "{}", "Attributes")
return cmd
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountDeleteCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-delete <identity>",
Short: "Invoke a std:account:delete command",
Example: `sp connectors invoke account-delete john.doe`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
uniqueID := ""
if len(args) > 1 {
uniqueID = args[1]
}
rawResponse, err := cc.AccountDelete(ctx, args[0], uniqueID)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
return cmd
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountDiscoverSchemaCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-discover-schema",
Short: "Invoke a std:account:discover-schema command",
Example: `sp connectors invoke account-discover-schema`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
_, rawResponse, err := cc.AccountDiscoverSchema(ctx)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
return cmd
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountListCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-list",
Short: "Invoke a std:account:list command",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
_, rawResponse, err := cc.AccountList(ctx)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
return cmd
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountReadCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-read [id/lookupId] [uniqueId]",
Short: "Invoke a std:account:read command",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
uniqueID := ""
if len(args) > 1 {
uniqueID = args[1]
}
_, rawResponse, err := cc.AccountRead(ctx, args[0], uniqueID)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
return cmd
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeAccountUpdateCmd(spClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "account-update [id/lookupId] [uniqueId] [--changes <value>]",
Short: "Invoke a std:account:update command",
Example: `sp connectors invoke account-update john.doe --changes '[{"op":"Add","attribute":"groups","value":["Group1","Group2"]},{"op":"Set","attribute":"phone","value":2223334444},{"op":"Remove","attribute":"location"}]'`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, spClient)
if err != nil {
return err
}
changesRaw := cmd.Flags().Lookup("changes").Value.String()
var changes []client.AttributeChange
if err := json.Unmarshal([]byte(changesRaw), &changes); err != nil {
return err
}
uniqueID := ""
if len(args) > 1 {
uniqueID = args[1]
}
_, rawResponse, err := cc.AccountUpdate(ctx, args[0], uniqueID, changes)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
cmd.Flags().String("changes", "[]", "Attribute Changes")
return cmd
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeEntitlementListCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "entitlement-list [--type <value>]",
Short: "Invoke a std:entitlement:list command",
Example: `sp connectors invoke entitlement-list --type group`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
t := cmd.Flags().Lookup("type").Value.String()
_, rawResponse, err := cc.EntitlementList(ctx, t)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
cmd.Flags().StringP("type", "t", "", "Entitlement Type")
_ = cmd.MarkFlagRequired("type")
return cmd
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeEntitlementReadCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "entitlement-read [id/lookupId] [uniqueId]",
Short: "Invoke a std:entitlement:read command",
Example: `sp connectors invoke entitlement-read john.doe --type group`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
t := cmd.Flags().Lookup("type").Value.String()
uniqueID := ""
if len(args) > 1 {
uniqueID = args[1]
}
_, rawResponse, err := cc.EntitlementRead(ctx, args[0], uniqueID, t)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
cmd.Flags().StringP("type", "t", "", "Entitlement Type")
_ = cmd.MarkFlagRequired("type")
return cmd
}

62
cmd/conn_invoke_raw.go Normal file
View File

@@ -0,0 +1,62 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
type rawCommand struct {
Type string `json:"type"`
Input json.RawMessage `json:"input"`
}
func newConnInvokeRaw(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: `raw < command.json`,
Short: "Invoke a raw command",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, client)
if err != nil {
return err
}
raw := &rawCommand{}
filePath := cmd.Flags().Lookup("file").Value.String()
var reader io.Reader
if len(filePath) > 0 {
reader, err = os.Open(filePath)
if err != nil {
return err
}
} else {
reader = os.Stdin
}
decoder := json.NewDecoder(reader)
err = decoder.Decode(raw)
if err != nil {
return err
}
rawResponse, err := cc.Invoke(ctx, raw.Type, raw.Input)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
cmd.Flags().StringP("file", "f", "", "JSON file containing a command")
return cmd
}

47
cmd/conn_invoke_test.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
// Unit tests for conn_invoke.go and its subcommands
// Expected number of subcommands to `sp` root command
const numConnInvokeSubcommands = 10
func TestNewConnInvokeCmd_noArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := newConnInvokeCmd(mocks.NewMockClient(ctrl))
if len(cmd.Commands()) != numConnInvokeSubcommands {
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnInvokeSubcommands)
}
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
cmd.PersistentFlags().Set("id", "connector-id")
cmd.PersistentFlags().Set("version", "455455")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if string(out) != cmd.UsageString() {
t.Errorf("expected: %s, actual: %s", cmd.UsageString(), string(out))
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnInvokeTestConnectionCmd(spClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "test-connection",
Short: "Invoke a std:test-connection command",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cc, err := connClient(cmd, spClient)
if err != nil {
return err
}
rawResponse, err := cc.TestConnection(ctx)
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), string(rawResponse))
return nil
},
}
return cmd
}

59
cmd/conn_list.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnListCmd(client client.Client) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List Connectors",
Long: "List Connectors For Tenant",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Get(cmd.Context(), endpoint)
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("non-200 response: %s\nbody: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var conns []connector
err = json.Unmarshal(raw, &conns)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(connectorColumns)
for _, v := range conns {
table.Append(v.columns())
}
table.Render()
return nil
},
}
}

45
cmd/conn_list_test.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnListCmd(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
Times(1)
cmd := newConnListCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if len(string(out)) == 0 {
t.Errorf("error empty out")
}
}

130
cmd/conn_logs.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"time"
"github.com/fatih/color"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var logInput = client.LogInput{}
func newConnLogsCmd(spClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "logs",
Short: "List Logs",
Example: "sp logs",
RunE: func(cmd *cobra.Command, args []string) error {
if err := formatDates(cmd); err != nil {
return err
}
if logInput.Filter.StartTime == nil {
from := time.Now().Add(-1 * time.Hour)
logInput.Filter.StartTime = &from
}
if err := getAllLogs(spClient, cmd, printLogs); err != nil {
return err
}
return nil
},
}
cmd.PersistentFlags().StringP("logs-endpoint", "o", viper.GetString("baseurl")+client.LogsEndpoint, "Override logs endpoint")
//date filters
cmd.Flags().StringP("start", "s", "", `start time - get the logs from this point. An absolute timestamp in RFC3339 format, or a relative time (eg. 2h). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
cmd.Flags().StringP("stop", "", "", `end time - get the logs upto this point. An absolute timestamp in RFC3339 format, or a relative time (eg. 2h). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
//other filters
cmd.PersistentFlags().StringVar(&logInput.Filter.Component, "component", "", "component type")
cmd.PersistentFlags().StringVar(&logInput.Filter.TargetID, "target-id", "", "id of the specific target object")
cmd.PersistentFlags().StringVar(&logInput.Filter.TargetName, "target-name", "", "name of the specifiy target")
cmd.PersistentFlags().StringVar(&logInput.Filter.RequestID, "request-id", "", "associated request id")
cmd.PersistentFlags().StringVar(&logInput.Filter.Event, "event", "", "event name")
cmd.PersistentFlags().StringSliceVar(&logInput.Filter.LogLevels, "level", nil, "log levels")
cmd.PersistentFlags().BoolP("raw", "r", false, "")
cmd.AddCommand(newConnLogsTailCmd(spClient))
return cmd
}
func printLogs(logEvents *client.LogEvents, cmd *cobra.Command) error {
rawPrint, _ := cmd.Flags().GetBool("raw")
if rawPrint {
for _, t := range logEvents.Logs {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), t.RawString())
}
} else {
for _, t := range logEvents.Logs {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), formatLog(t))
}
}
return nil
}
func getAllLogs(spClient client.Client, cmd *cobra.Command, fn func(logEvents *client.LogEvents, cmd *cobra.Command) error) error {
endpoint := cmd.Flags().Lookup("logs-endpoint").Value.String()
lc := client.NewLogsClient(spClient, endpoint)
logInput.NextToken = ""
for {
logEvents, err := lc.GetLogs(cmd.Context(), logInput)
if err != nil {
return err
}
if err := fn(logEvents, cmd); err != nil {
return err
}
if logEvents.NextToken == nil {
break
} else {
logInput.NextToken = *logEvents.NextToken
}
}
return nil
}
func formatDates(cmd *cobra.Command) error {
now := time.Now()
startTimeFlag := cmd.Flags().Lookup("start").Value.String()
stopTimeFlag := cmd.Flags().Lookup("stop").Value.String()
if stopTimeFlag != "" && startTimeFlag == "" {
return fmt.Errorf(`must provide a "--start" time when "--stop" specified`)
}
if startTimeFlag != "" {
retTime, err := client.ParseTime(startTimeFlag, now)
if err != nil {
return err
}
logInput.Filter.StartTime = &retTime
}
if stopTimeFlag != "" {
retTime, err := client.ParseTime(stopTimeFlag, now)
if err != nil {
return err
}
logInput.Filter.EndTime = &retTime
}
return nil
}
//Format log message for display
func formatLog(logMessage client.LogMessage) string {
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
logLevelColor := color.New(color.FgHiWhite).SprintFunc()
if logMessage.Level == "ERROR" {
logLevelColor = color.New(color.FgHiRed).SprintFunc()
}
return fmt.Sprintf("%s%s%s%s", green(fmt.Sprintf("[%s]", logMessage.TimestampFormatted())),
logLevelColor(fmt.Sprintf(" %-5s |", logMessage.Level)),
yellow(fmt.Sprintf(" %-16s", logMessage.Event)),
logLevelColor(fmt.Sprintf(" ▶︎ %s", logMessage.MessageString())))
}

64
cmd/conn_logs_tail.go Normal file
View File

@@ -0,0 +1,64 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"time"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnLogsTailCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "tail",
Short: "Tail Logs",
Example: "sp logs tail",
RunE: func(cmd *cobra.Command, args []string) error {
if err := tailLogs(client, cmd); err != nil {
return err
}
return nil
},
}
return cmd
}
func tailLogs(spClient client.Client, cmd *cobra.Command) error {
handleLogs := func(logEvents *client.LogEvents, cmd *cobra.Command) error {
if err := printLogs(logEvents, cmd); err != nil {
return err
}
for _, l := range logEvents.Logs {
updateLastSeenTime(l.Timestamp)
}
return nil
}
for {
logInput.Filter.StartTime = nextFromTime()
if err := getAllLogs(spClient, cmd, handleLogs); err != nil {
return err
}
time.Sleep(2 * time.Second)
}
}
var lastSeenTime *int64
func updateLastSeenTime(ts time.Time) {
nextTimeMilli := ts.UnixMilli()
if lastSeenTime == nil || nextTimeMilli > *lastSeenTime {
lastSeenTime = &nextTimeMilli
}
}
func nextFromTime() *time.Time {
from := time.Now().Add(-5 * time.Minute)
if lastSeenTime != nil {
//to fetch from next millisecond
from = time.UnixMilli(*lastSeenTime + 1)
}
return &from
}

View File

@@ -0,0 +1,41 @@
package cmd
import (
"testing"
"time"
)
func Test_updateLastSeenTime(t *testing.T) {
now := time.Now()
if nextFromTime().UnixMilli() >= now.UnixMilli() {
t.Errorf("unexepected first time returned")
}
for i := 0; i < 10; i++ {
updateLastSeenTime(now)
if i < 10 {
now = time.Now()
}
}
if nextFromTime().UnixMilli() != now.Add(1*time.Millisecond).UnixMilli() {
t.Errorf("unexepected next time returned")
}
}
func Test_updateLastSeenTimeOutOfSequence(t *testing.T) {
first := time.Now()
time.Sleep(1 * time.Millisecond)
second := time.Now()
time.Sleep(1 * time.Millisecond)
now := time.Now()
for i := 0; i < 10; i++ {
updateLastSeenTime(now)
if i < 10 {
now = time.Now()
}
}
updateLastSeenTime(first)
updateLastSeenTime(second)
if nextFromTime().UnixMilli() != now.Add(1*time.Millisecond).UnixMilli() {
t.Errorf("unexepected next time returned")
}
}

109
cmd/conn_stats.go Normal file
View File

@@ -0,0 +1,109 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"time"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var statColumns = []string{"Command", "Invocation Count", "Error Count", "Error Rate", "Elapsed Avg", "Elapsed 95th Percentile"}
const (
day = int64(24 * time.Hour)
week = int64(7 * 24 * time.Hour)
)
var durationMap = map[byte]int64{
'd': day,
'w': week,
}
func newConnStatsCmd(spClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "stats",
Short: "Command Stats",
Long: "Command execution stats for a tenant, default to last 24hs if duration not specified",
Example: "sp conn stats",
RunE: func(cmd *cobra.Command, args []string) error {
if err := getTenantStats(spClient, cmd); err != nil {
return err
}
return nil
},
}
cmd.PersistentFlags().StringP("stats-endpoint", "o", viper.GetString("baseurl")+client.StatsEndpoint, "Override stats endpoint")
cmd.Flags().StringP("duration", "d", "", `Length of time represented by an integer(1-9) and a duration unit. Supported duration units: d,w. eg 1d, 3w`)
cmd.Flags().StringP("id", "c", "", "Connector ID")
return cmd
}
func getTenantStats(spClient client.Client, cmd *cobra.Command) error {
endpoint := cmd.Flags().Lookup("stats-endpoint").Value.String()
lc := client.NewLogsClient(spClient, endpoint)
connectorID := cmd.Flags().Lookup("id").Value.String()
durationStr := cmd.Flags().Lookup("duration").Value.String()
duration, err := parseDuration(durationStr)
if err != nil {
return err
}
if duration == nil {
return fmt.Errorf("invalid duration")
}
from := time.Now().Add(-*duration)
tenantStats, err := lc.GetStats(cmd.Context(), from, connectorID)
if err != nil {
return err
}
for _, c := range tenantStats.ConnectorStats {
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(statColumns)
connAlias := ""
if c.ConnectorAlias != "" {
connAlias = fmt.Sprintf("(%v)", c.ConnectorAlias)
}
connTitle := fmt.Sprintf("Connector : %v %s ", c.ConnectorID, connAlias)
_, _ = fmt.Fprintln(cmd.OutOrStdout(), connTitle)
for _, v := range c.Stats {
table.Append(v.Columns())
}
table.Render()
fmt.Fprintln(cmd.OutOrStdout())
}
return nil
}
func parseDuration(durationStr string) (*time.Duration, error) {
defaultDuration := time.Duration(day)
if len(durationStr) == 0 {
return &defaultDuration, nil
}
if !validDuration(durationStr) {
return nil, fmt.Errorf("invalid duration")
}
durationNum := int64(durationStr[0] - '0')
duration := time.Duration(durationNum * durationMap[durationStr[1]])
return &duration, nil
}
func validDuration(durationStr string) bool {
if len(durationStr) != 2 {
return false
}
// The first character must be [1-9]
if !('1' <= durationStr[0] && durationStr[0] <= '9') {
return false
}
// The second character must be on of the supported duration[d,w]
if _, ok := durationMap[durationStr[1]]; !ok {
return false
}
return true
}

79
cmd/conn_stats_test.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"testing"
"time"
)
func Test_parseDuration(t *testing.T) {
tests := []struct {
name string
durationStr string
want time.Duration
wantErr bool
}{
{
name: "1. valid day",
durationStr: "1d",
want: 24 * time.Hour,
wantErr: false,
},
{
name: "2. valid 9 day",
durationStr: "9d",
want: 9 * 24 * time.Hour,
wantErr: false,
},
{
name: "3. Invalid 15 day",
durationStr: "15d",
wantErr: true,
},
{
name: "4. valid week",
durationStr: "1w",
want: 7 * 24 * time.Hour,
wantErr: false,
},
{
name: "5. valid 9 week",
durationStr: "9w",
want: 9 * 7 * 24 * time.Hour,
wantErr: false,
},
{
name: "6. Invalid 15 week",
durationStr: "15d",
wantErr: true,
},
{
name: "7. Invalid text",
durationStr: "sd",
wantErr: true,
},
{
name: "8. Invalid text",
durationStr: "sde",
wantErr: true,
},
{
name: "9. Invalid text",
durationStr: "234",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseDuration(tt.durationStr)
if (err != nil) != tt.wantErr {
t.Errorf("GetTenantStats.getCommandStats() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (got != nil && *got != tt.want) || (got == nil && err == nil) {
t.Errorf("validDuration() = %v, want %v", got, tt.want)
}
})
}
}

30
cmd/conn_tag.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnTagCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "tags",
Short: "Manage tags",
Run: func(cmd *cobra.Command, args []string) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cmd.UsageString())
},
}
cmd.PersistentFlags().StringP("id", "c", "", "Connector ID or Alias")
_ = cmd.MarkPersistentFlagRequired("id")
cmd.AddCommand(
newConnTagListCmd(client),
newConnTagCreateCmd(client),
newConnTagUpdateCmd(client),
)
return cmd
}

78
cmd/conn_tag_create.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnTagCreateCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create Connector Tag",
Example: "sp conn tags create -n rc -v 10",
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
tagName := cmd.Flags().Lookup("name").Value.String()
versionStr := cmd.Flags().Lookup("version").Value.String()
version, err := strconv.Atoi(versionStr)
if err != nil {
return err
}
raw, err := json.Marshal(TagCreate{TagName: tagName, ActiveVersion: uint32(version)})
if err != nil {
return err
}
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Post(cmd.Context(), connResourceUrl(endpoint, connectorRef, "tags"), "application/json", 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("create connector tag failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var t tag
err = json.Unmarshal(raw, &t)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(tagColumns)
table.Append(t.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("name", "n", "", "Tag name")
_ = cmd.MarkFlagRequired("name")
cmd.Flags().StringP("version", "v", "", "Active version of connector upload the tag points to")
_ = cmd.MarkFlagRequired("version")
return cmd
}

61
cmd/conn_tag_list.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnTagListCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List tags for a connector",
Example: "sp conn tags list -c 1234",
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Get(cmd.Context(), connResourceUrl(endpoint, connectorRef, "tags"))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("non-200 response: %s\nbody: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var tags []tag
err = json.Unmarshal(raw, &tags)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(tagColumns)
for _, t := range tags {
table.Append(t.columns())
}
table.Render()
return nil
},
}
return cmd
}

79
cmd/conn_tag_update.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnTagUpdateCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Update Connector Tag",
Example: "sp conn tags update -n rc -v 10",
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
tagName := cmd.Flags().Lookup("name").Value.String()
versionStr := cmd.Flags().Lookup("version").Value.String()
version, err := strconv.Atoi(versionStr)
if err != nil {
return err
}
raw, err := json.Marshal(TagUpdate{ActiveVersion: uint32(version)})
if err != nil {
return err
}
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Put(cmd.Context(), connResourceUrl(endpoint, connectorRef, "tags", tagName), "application/json", 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("update connector tag failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var t tag
err = json.Unmarshal(raw, &t)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(tagColumns)
table.Append(t.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("name", "n", "", "Tag name")
_ = cmd.MarkFlagRequired("name")
cmd.Flags().StringP("version", "v", "", "Active version of connector uploads the tag points to")
_ = cmd.MarkFlagRequired("version")
return cmd
}

57
cmd/conn_test.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
// Unit tests for conn.go
// Expected number of subcommands to `connectors`
const numConnSubcommands = 13
func TestConnResourceUrl(t *testing.T) {
testEndpoint := "http://localhost:7100/resources"
testResource := "123"
expected := "http://localhost:7100/resources/123"
actual := connResourceUrl(testEndpoint, testResource)
if expected != actual {
t.Errorf("expected: %s, actual: %s", expected, actual)
}
}
func TestNewConnCmd_noArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := newConnCmd(mocks.NewMockClient(ctrl))
if len(cmd.Commands()) != numConnSubcommands {
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnSubcommands)
}
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if string(out) != cmd.UsageString() {
t.Errorf("expected: %s, actual: %s", cmd.UsageString(), string(out))
}
}

82
cmd/conn_update.go Normal file
View File

@@ -0,0 +1,82 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnUpdateCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Update Connector",
Long: "Update Connector",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
id := cmd.Flags().Lookup("id").Value.String()
alias := cmd.Flags().Lookup("alias").Value.String()
if alias == "" {
return fmt.Errorf("alias must be specified")
}
u := connectorUpdate{
DisplayName: alias,
Alias: alias,
}
raw, err := json.Marshal(u)
if err != nil {
return err
}
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Put(cmd.Context(), connResourceUrl(endpoint, id), "application/json", 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("update connector failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
var conn connector
err = json.Unmarshal(raw, &conn)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(connectorColumns)
table.Append(conn.columns())
table.Render()
return nil
},
}
cmd.Flags().StringP("id", "c", "", "Specify connector id")
_ = cmd.MarkFlagRequired("id")
cmd.Flags().StringP("alias", "a", "", "alias of the connector")
bindDevConfig(cmd.Flags())
return cmd
}

67
cmd/conn_update_test.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnUpdateCmd_missingRequiredFlags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(0)
cmd := newConnUpdateCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}
func TestNewConnUpdateCmd(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
Times(1)
cmd := newConnUpdateCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{"--id", "mockConnectorId", "--alias", "newConnectorAlias"})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if len(string(out)) == 0 {
t.Errorf("error empty out")
}
}

100
cmd/conn_validate.go Normal file
View File

@@ -0,0 +1,100 @@
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/logrusorgru/aurora"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/sailpoint/sp-cli/validate"
"github.com/spf13/cobra"
)
func newConnValidateCmd(apiClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "validate",
Short: "Validate connector behavior",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Check if we just need to list checks
list, _ := strconv.ParseBool(cmd.Flags().Lookup("list").Value.String())
if list {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Description"})
for _, c := range validate.Checks {
table.Append([]string{
c.ID,
c.Description,
})
}
table.Render()
return nil
}
cc, err := connClient(cmd, apiClient)
if err != nil {
return err
}
check := cmd.Flags().Lookup("check").Value.String()
isReadOnly, _ := strconv.ParseBool(cmd.Flags().Lookup("read-only").Value.String())
valid := validate.NewValidator(validate.Config{
Check: check,
ReadOnly: isReadOnly,
}, cc)
results, err := valid.Run(ctx)
if err != nil {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Result", "Errors", "Warnings", "Skipped"})
hasFailedCheck := false
for _, res := range results {
var result = aurora.Green("PASS")
if len(res.Errors) > 0 {
hasFailedCheck = true
result = aurora.Red("FAIL")
}
if len(res.Skipped) > 0 {
result = aurora.Yellow("SKIPPED")
}
table.Append([]string{
aurora.Blue(res.ID).String(),
result.String(),
aurora.Red(strings.Join(res.Errors, "\n\n")).String(),
aurora.Yellow(strings.Join(res.Warnings, "\n\n")).String(),
aurora.Yellow(strings.Join(res.Skipped, "\n\n")).String(),
})
}
table.Render()
if hasFailedCheck {
return fmt.Errorf("at least one check has failed")
}
return nil
},
}
cmd.PersistentFlags().StringP("check", "", "", "Run a specific check")
cmd.PersistentFlags().BoolP("list", "l", false, "List checks; don't run checks")
cmd.PersistentFlags().BoolP("read-only", "r", false, "Run all checks that don't modify connector's data")
cmd.PersistentFlags().StringP("version", "v", "", "Run against a specific version")
cmd.MarkFlagRequired("version")
cmd.PersistentFlags().StringP("config-path", "p", "", "Path to config to use for test command")
cmd.MarkFlagRequired("config-path")
cmd.PersistentFlags().StringP("id", "c", "", "Connector ID or Alias")
cmd.MarkFlagRequired("id")
return cmd
}

View File

@@ -0,0 +1,265 @@
// Copyright (c) 2022, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"strings"
"syscall"
"time"
"github.com/logrusorgru/aurora"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/sailpoint/sp-cli/validate"
"github.com/spf13/cobra"
"gopkg.in/alessio/shellescape.v1"
"gopkg.in/yaml.v2"
)
type Source struct {
// Name represents name of a source (github, smartsheet, freshservice, etc)
Name string `yaml:"name"`
// Repository is a link for a connector repository
Repository string `yaml:"repository"`
// RepositoryRef is a branch that uses for service starts
RepositoryRef string `yaml:"repositoryRef"`
// Config is an authentication data for service startup
Config string `yaml:"config"`
// ReadOnly is a flag that indicates the validation checks with data modification ('true') or without it ('false').
ReadOnly bool `yaml:"readOnly"`
}
// ValidationResults represents validation results for every source
type ValidationResults struct {
sourceName string
results map[string]*tablewriter.Table
}
const (
connectorInstanceEndpoint = "http://localhost:3000"
sourceFile = "./source.yaml"
)
func (v *ValidationResults) Render() {
fmt.Println(aurora.Blue(fmt.Sprintf("%s connectors validation results", v.sourceName)).String())
for connectorID, result := range v.results {
fmt.Println(aurora.Blue(fmt.Sprintf("Connector %s", connectorID)).String())
result.Render()
fmt.Println("---------------------------------------------------------")
}
}
func newConnValidateSourcesCmd(apiClient client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "validate-sources",
Short: "Validate connectors behavior",
Long: "Validate connectors behavior from a list that stores in sources.yaml",
Example: "sp conn validate-sources",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
listOfSources, err := getSourceFromFile(sourceFile)
if err != nil {
return err
}
var results []ValidationResults
for _, source := range listOfSources {
instance, tempFolder, err := runInstanceForValidation(source)
if err != nil {
return err
}
res, err := validateConnectors(ctx, apiClient, source, endpoint)
if err != nil {
return err
}
err = instance.Process.Signal(syscall.SIGTERM)
if err != nil {
return err
}
if instance.ProcessState != nil {
return errors.New(fmt.Sprintf("%s instance wasn't stopped", source.Name))
}
err = os.RemoveAll(fmt.Sprintf("/%s", tempFolder))
if err != nil {
return err
}
results = append(results, *res)
}
for _, r := range results {
r.Render()
}
return nil
},
}
return cmd
}
func getSourceFromFile(filePath string) ([]Source, error) {
yamlFile, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
var config []Source
err = yaml.Unmarshal(yamlFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func validateConnectors(ctx context.Context, apiClient client.Client, source Source, endpoint string) (*ValidationResults, error) {
resp, err := apiClient.Get(ctx, endpoint)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("non-200 response for getting all %s connectors: %s\nbody: %s", source.Name, resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var conns []connector
err = json.Unmarshal(raw, &conns)
if err != nil {
return nil, err
}
valRes := &ValidationResults{
sourceName: source.Name,
results: make(map[string]*tablewriter.Table),
}
connector := conns[len(conns)-1]
cc, err := connClientWithCustomParams(apiClient, json.RawMessage(source.Config), connector.ID, "0", connectorInstanceEndpoint)
if err != nil {
log.Println(err)
}
validator := validate.NewValidator(validate.Config{
Check: "",
ReadOnly: source.ReadOnly,
}, cc)
results, err := validator.Run(ctx)
if err != nil {
log.Println(err)
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Result", "Errors", "Warnings", "Skipped"})
for _, res := range results {
var result = aurora.Green("PASS")
if len(res.Errors) > 0 {
result = aurora.Red("FAIL")
}
if len(res.Skipped) > 0 {
result = aurora.Yellow("SKIPPED")
}
table.Append([]string{
aurora.Blue(res.ID).String(),
result.String(),
aurora.Red(strings.Join(res.Errors, "\n\n")).String(),
aurora.Yellow(strings.Join(res.Warnings, "\n\n")).String(),
aurora.Yellow(strings.Join(res.Skipped, "\n\n")).String(),
})
}
valRes.results[connector.ID] = table
return valRes, err
}
func createTempFolder() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
path, err := os.MkdirTemp(homeDir, "*")
if err != nil {
return "", err
}
return path, err
}
func runInstanceForValidation(source Source) (*exec.Cmd, string, error) {
path, err := createTempFolder()
if err != nil {
return nil, "", err
}
cloneRepo := exec.Command("git", "clone", shellescape.Quote(source.Repository), path)
if err := cloneRepo.Run(); err != nil {
return nil, "", err
}
log.Printf("Repo for %s is cloned\n", source.Name)
checkoutRepoRef := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s && git checkout %s", path, shellescape.Quote(source.RepositoryRef)))
if err := checkoutRepoRef.Run(); err != nil {
return nil, "", err
}
log.Printf("git checkout to %s\n", source.RepositoryRef)
cmd := exec.Command("npm", "install", "--prefix", path)
if err := cmd.Run(); err != nil {
return nil, "", err
}
log.Println("Npm install is finished")
err = ExecCommand("/bin/sh", "-c", fmt.Sprintf("npm run dev --prefix %s", path))
if err != nil {
return nil, "", err
}
for {
_, err := http.Get(connectorInstanceEndpoint)
if err == nil {
log.Printf("Service %s is successfully started for validation\n", source.Name)
break
}
time.Sleep(time.Second * 5)
}
return cmd, path, err
}

66
cmd/conn_versions.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/olekukonko/tablewriter"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func newConnVersionsCmd(client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "versions",
Short: "Get Connector Versions",
Long: "Get Connector Versions",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
connectorRef := cmd.Flags().Lookup("id").Value.String()
endpoint := cmd.Flags().Lookup("conn-endpoint").Value.String()
resp, err := client.Get(cmd.Context(), connResourceUrl(endpoint, connectorRef, "versions"))
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("list versions failed. status: %s\nbody: %s", resp.Status, body)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var vs []connectorVersion
err = json.Unmarshal(raw, &vs)
if err != nil {
return err
}
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader(connectorVersionColumns)
for _, v := range vs {
table.Append(v.columns())
}
table.Render()
return nil
},
}
cmd.Flags().StringP("id", "c", "", "Connector ID or Alias")
_ = cmd.MarkFlagRequired("id")
bindDevConfig(cmd.Flags())
return cmd
}

67
cmd/conn_versions_test.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
func TestNewConnVersionsCmd_missingRequiredFlags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
Times(0)
cmd := newConnVersionsCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}
func TestNewConnVersionsCmd(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
client.EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
Times(1)
cmd := newConnVersionsCmd(client)
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{"--id", "mockConnectorId"})
cmd.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if len(string(out)) == 0 {
t.Errorf("error empty out")
}
}

19
cmd/exec.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) 2022, SailPoint Technologies, Inc. All rights reserved.
//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd
// +build linux darwin dragonfly freebsd netbsd openbsd
package cmd
import (
"os/exec"
"syscall"
)
// ExecCommand runs commands on non windows environment with Setpgid flag set to true
func ExecCommand(name string, arg ...string) error {
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return cmd.Start()
}

20
cmd/exec_windows.go Normal file
View File

@@ -0,0 +1,20 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
//go:build windows
// +build windows
package cmd
import (
"os/exec"
"syscall"
)
// ExecCommand runs commands on windows environment with CREATE_NEW_PROCESS_GROUP flag,
// equivalent to Setpgid in linux like environment
func ExecCommand(name string, arg ...string) error {
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
return cmd.Start()
}

57
cmd/models.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"strconv"
)
type connector struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Alias string `json:"alias"`
}
func (c connector) columns() []string {
return []string{c.ID, c.Alias}
}
var connectorColumns = []string{"ID", "Alias"}
type connectorVersion struct {
ConnectorID string `json:"connectorId"`
Version int `json:"version"`
}
type connectorUpdate struct {
DisplayName string `json:"displayName"`
Alias string `json:"alias"`
}
func (v connectorVersion) columns() []string {
return []string{v.ConnectorID, strconv.Itoa(v.Version)}
}
var connectorVersionColumns = []string{"Connector ID", "Version"}
// tag is an anchor point pointing to a version of the connector
type tag struct {
ID string `json:"id"`
TagName string `json:"tagName"`
ActiveVersion uint32 `json:"activeVersion"`
}
func (t tag) columns() []string {
return []string{t.ID, t.TagName, fmt.Sprint(t.ActiveVersion)}
}
var tagColumns = []string{"ID", "Tag Name", "Active Version"}
type TagCreate struct {
TagName string `json:"tagName"`
ActiveVersion uint32 `json:"activeVersion"`
}
type TagUpdate struct {
ActiveVersion uint32 `json:"activeVersion"`
}

30
cmd/root.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/sailpoint/sp-cli/client"
"github.com/spf13/cobra"
)
func NewRootCmd(client client.Client) *cobra.Command {
root := &cobra.Command{
Use: "sp",
Short: "sp",
SilenceUsage: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
DisableNoDescFlag: true,
DisableDescriptions: true,
},
Run: func(cmd *cobra.Command, args []string) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cmd.UsageString())
},
}
root.AddCommand(
newConnCmd(client),
newConfigureCmd(client),
)
return root
}

58
cmd/root_test.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package cmd
import (
"bytes"
"io"
"testing"
"github.com/golang/mock/gomock"
"github.com/sailpoint/sp-cli/mocks"
)
// Expected number of subcommands to `sp` root command
const numRootSubcommands = 2
func TestNewRootCmd_noArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := NewRootCmd(mocks.NewMockClient(ctrl))
if len(cmd.Commands()) != numRootSubcommands {
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numRootSubcommands)
}
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{})
err := cmd.Execute()
if err != nil {
t.Fatalf("error execute cmd: %v", err)
}
out, err := io.ReadAll(b)
if err != nil {
t.Fatalf("error read out: %v", err)
}
if string(out) != cmd.UsageString() {
t.Errorf("expected: %s, actual: %s", cmd.UsageString(), string(out))
}
}
func TestNewRootCmd_completionDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := NewRootCmd(mocks.NewMockClient(ctrl))
b := new(bytes.Buffer)
cmd.SetOut(b)
cmd.SetArgs([]string{"completion"})
if err := cmd.Execute(); err == nil {
t.Error("expected command to fail")
}
}

19
cmd/static/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# macOS General
.DS_Store
.AppleDouble
.LSOverride
# Visual Studio Code
.vscode/
.history/
# Intellij
.idea/
*.iml
# Dependency directories
node_modules/
# Compiled source
dist/
coverage/

View File

@@ -0,0 +1,42 @@
{
"name": "{{$.ProjectName}}",
"commands": [
"std:account:list",
"std:account:read",
"std:test-connection"
],
"sourceConfig": [
{
"type": "section",
"items": [
{
"key": "token",
"label": "Token",
"type": "text"
}
]
}
],
"accountSchema":{
"displayAttribute": "firstName",
"identityAttribute": "email",
"attributes":[
{
"name": "firstName",
"type": "string",
"description": "First name of the account"
},
{
"name": "lastName",
"type": "string",
"description": "Last name of the account"
},
{
"name": "email",
"type": "string",
"description": "Email of the account"
}
]
},
"entitlementSchemas": []
}

50
cmd/static/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "{{$.ProjectName}}",
"version": "0.1.0",
"main": "dist/index.js",
"scripts": {
"clean": "shx rm -rf ./dist",
"prebuild": "npm run clean",
"build": "npx ncc build ./src/index.ts -o ./dist -m -C",
"dev": "spcx dist/index.js",
"prettier": "npx prettier --write .",
"test": "jest --coverage",
"prepack-zip": "npm ci && npm run build",
"pack-zip": "(cp connector-spec.json ./dist/ && cd ./dist && bestzip $npm_package_name-$npm_package_version.zip ./index.js ./connector-spec.json)"
},
"private": true,
"dependencies": {
"@sailpoint/connector-sdk": "github:sailpoint/saas-connector-sdk-js#main"
},
"devDependencies": {
"@types/jest": "^27.0.1",
"@vercel/ncc": "^0.28.6",
"bestzip": "^2.2.0",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"shx": "^0.3.3",
"ts-jest": "^27.0.5",
"typescript": "^4.3.5"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"clearMocks": true,
"collectCoverage": true,
"coverageThreshold": {
"global": {
"statements": 60,
"branches": 50,
"functions": 40,
"lines": 60
}
}
},
"prettier": {
"printWidth": 120,
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}
}

View File

@@ -0,0 +1,24 @@
import { connector } from './index'
import { StandardCommand } from '@sailpoint/connector-sdk'
import { PassThrough } from 'stream'
const mockConfig: any = {
token: 'xxx123'
}
process.env.CONNECTOR_CONFIG = Buffer.from(JSON.stringify(mockConfig)).toString('base64')
describe('connector unit tests', () => {
it('connector SDK major version should be 0', async () => {
expect((await connector()).sdkVersion).toStrictEqual(0)
})
it('should execute stdTestConnectionHandler', async () => {
await (await connector())._exec(
StandardCommand.StdTestConnection,
{},
undefined,
new PassThrough({ objectMode: true }).on('data', (chunk) => expect(chunk).toStrictEqual({}))
)
})
})

59
cmd/static/src/index.ts Normal file
View File

@@ -0,0 +1,59 @@
import {
Context,
createConnector,
readConfig,
Response,
logger,
StdAccountListOutput,
StdAccountReadInput,
StdAccountReadOutput,
StdTestConnectionOutput,
} from '@sailpoint/connector-sdk'
import { MyClient } from './my-client'
// Connector must be exported as module property named connector
export const connector = async () => {
// Get connector source config
const config = await readConfig()
// Use the vendor SDK, or implement own client as necessary, to initialize a client
const myClient = new MyClient(config)
return createConnector()
.stdTestConnection(async (context: Context, input: undefined, res: Response<StdTestConnectionOutput>) => {
logger.info("Running test connection")
res.send(await myClient.testConnection())
})
.stdAccountList(async (context: Context, input: undefined, res: Response<StdAccountListOutput>) => {
const accounts = await myClient.getAllAccounts()
for (const account of accounts) {
res.send({
identity: account.username,
uuid: account.id,
attributes: {
firstName: account.firstName,
lastName: account.lastName,
email: account.email,
},
})
}
logger.info(`stdAccountList sent ${accounts.length} accounts`)
})
.stdAccountRead(async (context: Context, input: StdAccountReadInput, res: Response<StdAccountReadOutput>) => {
const account = await myClient.getAccount(input.identity)
res.send({
identity: account.username,
uuid: account.id,
attributes: {
firstName: account.firstName,
lastName: account.lastName,
email: account.email,
},
})
logger.info(`stdAccountRead read account : ${input.identity}`)
})
}

View File

@@ -0,0 +1,37 @@
import { ConnectorError, StandardCommand } from '@sailpoint/connector-sdk'
import { MyClient } from './my-client'
const mockConfig: any = {
token: 'xxx123'
}
describe('connector client unit tests', () => {
const myClient = new MyClient(mockConfig)
it('connector client list accounts', async () => {
let allAccounts = await myClient.getAllAccounts()
expect(allAccounts.length).toStrictEqual(2)
})
it('connector client get account', async () => {
let account = await myClient.getAccount('john.doe')
expect(account.username).toStrictEqual('john.doe')
})
it('connector client test connection', async () => {
expect(await myClient.testConnection()).toStrictEqual({})
})
it('connector client test connection', async () => {
expect(await myClient.testConnection()).toStrictEqual({})
})
it('invalid connector client', async () => {
try {
new MyClient({})
} catch (e) {
expect(e instanceof ConnectorError).toBeTruthy()
}
})
})

View File

@@ -0,0 +1,51 @@
import { ConnectorError } from "@sailpoint/connector-sdk"
const MOCK_DATA = new Map([
[
'john.doe',
{
id: '1',
username: 'john.doe',
firstName: 'john',
lastName: 'doe',
email: 'john.doe@example.com',
},
],
[
'jane.doe',
{
id: '2',
username: 'jane.doe',
firstName: 'jane',
lastName: 'doe',
email: 'jane.doe@example.com',
},
],
])
export class MyClient {
private readonly token?: string
constructor(config: any) {
// Fetch necessary properties from config.
// Following properties actually do not exist in the config -- it just serves as an example.
this.token = config?.token
if (this.token == null) {
throw new ConnectorError('token must be provided from config')
}
}
async getAllAccounts(): Promise<any[]> {
return Array.from(MOCK_DATA.values())
}
async getAccount(identity: string): Promise<any> {
// In a real use case, this requires a HTTP call out to SaaS app to fetch an account,
// which is why it's good practice for this to be async and return a promise.
return MOCK_DATA.get(identity)
}
async testConnection(): Promise<any> {
return {}
}
}

15
cmd/static/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts", "**/*.spec.js"]
}

3
config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"access_token": "your_source_access_token"
}

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module github.com/sailpoint/sp-cli
go 1.16
require (
github.com/fatih/color v1.7.0
github.com/golang/mock v1.6.0
github.com/kr/pretty v0.3.0
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
gopkg.in/yaml.v2 v2.4.0
)

617
go.sum Normal file
View File

@@ -0,0 +1,617 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM=
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

60
main.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package main
import (
"os"
"path/filepath"
"github.com/sailpoint/sp-cli/client"
"github.com/sailpoint/sp-cli/cmd"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
c client.Client
rootCmd *cobra.Command
)
func initConfig() {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(filepath.Join(home, ".sp"))
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.SetEnvPrefix("SP_CLI")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
// IGNORE they may be using env vars
} else {
// Config file was found but another error was produced
cobra.CheckErr(err)
}
}
}
func init() {
initConfig()
c = client.NewSpClient(client.SpClientConfig{
TokenURL: viper.GetString("tokenurl"),
ClientID: viper.GetString("clientid"),
ClientSecret: viper.GetString("clientsecret"),
Debug: viper.GetBool("debug"),
})
rootCmd = cmd.NewRootCmd(c)
}
// main the entry point for commands. Note that we do not need to do cobra.CheckErr(err)
// here. When a command returns error, cobra already logs it. Adding CheckErr here will
// cause error messages to be logged twice. We do need to exit with error code if something
// goes wrong. This will exit the cli container during pipeline build and fail that stage.
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

96
mocks/client.go Normal file
View File

@@ -0,0 +1,96 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: client/client.go
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
http "net/http"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockClient) Get(ctx context.Context, url string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, url)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockClientMockRecorder) Get(ctx, url interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), ctx, url)
}
// Post mocks base method.
func (m *MockClient) Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Post", ctx, url, contentType, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Post indicates an expected call of Post.
func (mr *MockClientMockRecorder) Post(ctx, url, contentType, body interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockClient)(nil).Post), ctx, url, contentType, body)
}
// Put mocks base method.
func (m *MockClient) Put(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Put", ctx, url, contentType, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Put indicates an expected call of Put.
func (mr *MockClientMockRecorder) Put(ctx, url, contentType, body interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockClient)(nil).Put), ctx, url, contentType, body)
}
// VerifyToken mocks base method.
func (m *MockClient) VerifyToken(ctx context.Context, tokenUrl, clientID, secret string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyToken", ctx, tokenUrl, clientID, secret)
ret0, _ := ret[0].(error)
return ret0
}
// VerifyToken indicates an expected call of VerifyToken.
func (mr *MockClientMockRecorder) VerifyToken(ctx, tokenUrl, clientID, secret interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyToken", reflect.TypeOf((*MockClient)(nil).VerifyToken), ctx, tokenUrl, clientID, secret)
}

35
source.yaml Normal file
View File

@@ -0,0 +1,35 @@
- name: smartsheet
repository: git@github.com:sailpoint/saas-conn-smartsheet.git
repositoryRef: main
readOnly: true
config: '{ "access_token" : "your_token_for_smartsheet" }'
- name: freshservice
repository: https://github.com/sailpoint/saas-conn-freshservice
repositoryRef: main
readOnly: true
config: '{ "company": "your_company", "apiKey": "your_api_key" }'
- name: github
repository: git@github.com:sailpoint/saas-conn-github-legit.git
repositoryRef: master
readOnly: true
config: '{ "org": "test", "token": "test" }'
- name: lastpass
repository: git@github.com:sailpoint/saas-conn-lastpass.git
repositoryRef: master
readOnly: true
config: '{ "cid": "test", "provhash": "test", "privateKey": "test", "apiKey": "test" }'
- name: docusign
repository: git@github.com:sailpoint/saas-conn-docusign.git
repositoryRef: master
readOnly: true
config: '{ "apiUrl": "test", "accountId": "test", "clientId": "test", "userId": "test", "privateKey": "test" }'
- name: aha
repository: git@github.com:sailpoint/saas-conn-aha.git
repositoryRef: master
readOnly: true
config: '{ "apiUrl": "test", "accountId": "test", "clientId": "test", "userId": "test", "privateKey": "test" }'
- name: scim
repository: git@github.com:sailpoint/saas-conn-scim.git
repositoryRef: master
readOnly: true
config: '{ "apiUrl": "test", "accountId": "test", "clientId": "test", "userId": "test", "privateKey": "test" }'

197
validate/account_create.go Normal file
View File

@@ -0,0 +1,197 @@
package validate
import (
"context"
"time"
"github.com/kr/pretty"
"github.com/sailpoint/sp-cli/client"
)
var accountCreateChecks = []Check{
{
ID: "account-create-empty",
Description: "Creating an account with no attributes should fail",
IsDataModifier: true,
RequiredCommands: []string{
"std:account:create",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
input := map[string]interface{}{}
_, _, err := cc.AccountCreate(ctx, nil, input)
if err == nil {
res.errf("expected error for empty account created")
}
},
},
{
ID: "account-create-minimal",
Description: "Creating an account with only required fields should be successful",
IsDataModifier: true,
RequiredCommands: []string{
"std:account:create",
"std:account:read",
"std:account:delete",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
input := map[string]interface{}{}
for _, field := range spec.AccountCreateTemplate.Fields {
if field.Required {
input[getFieldName(field)] = genCreateField(field)
}
}
identity := getIdentity(input)
acct, _, err := cc.AccountCreate(ctx, &identity, input)
if err != nil {
res.errf("creating account: %s", err)
return
}
diffs := compareIntersection(input, acct.Attributes)
for _, diff := range diffs {
res.errf("input vs read mismatch %+v", diff)
}
acctRead, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("reading account: %s", err)
return
}
diffs = compareIntersection(input, acctRead.Attributes)
for _, diff := range diffs {
res.errf("account diffs %+v", diff)
}
_, err = cc.AccountDelete(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("deleting account: %s", err)
}
},
},
{
ID: "account-create-maximal",
Description: "Creating an account with all fields should be successful",
IsDataModifier: true,
RequiredCommands: []string{
"std:account:create",
"std:account:read",
"std:account:delete",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
input := map[string]interface{}{}
for _, field := range spec.AccountCreateTemplate.Fields {
input[getFieldName(field)] = genCreateField(field)
}
identity := getIdentity(input)
acct, _, err := cc.AccountCreate(ctx, &identity, input)
if err != nil {
res.errf("creating account: %s", err)
return
}
diffs := compareIntersection(input, acct.Attributes)
for _, diff := range diffs {
res.errf("account diffs %+v", diff)
}
acctRead, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("reading account: %s", err)
return
}
diffs = compareIntersection(input, acctRead.Attributes)
for _, diff := range diffs {
res.errf("account diffs %+v", diff)
}
_, err = cc.AccountDelete(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("deleting account: %s", err)
}
},
},
{
ID: "account-create-list-delete",
Description: "Created accounts should show up in list accounts response; after deletion they should not",
IsDataModifier: true,
RequiredCommands: []string{
"std:account:create",
"std:account:read",
"std:account:delete",
"std:account:list",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
accountsPreCreate, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
return
}
input := map[string]interface{}{}
for _, field := range spec.AccountCreateTemplate.Fields {
if field.Required {
input[getFieldName(field)] = genCreateField(field)
}
}
identity := getIdentity(input)
acct, _, err := cc.AccountCreate(ctx, &identity, input)
if err != nil {
res.errf("creating account: %s", err)
return
}
accountsPostCreate, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
return
}
accountRead, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.err(err)
return
}
acctDiffs := pretty.Diff(*acct, *accountRead)
if len(acctDiffs) > 0 {
for _, diff := range acctDiffs {
res.errf("[identity=%s] Diff: %s", acct.Identity, diff)
}
}
_, err = cc.AccountDelete(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("deleting account: %s", err)
}
// Allow deletion to propagate
time.Sleep(5 * time.Second)
_, _, err = cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err == nil {
res.errf("was able to read deleted account: %q", acct.Identity)
}
accountsPostDelete, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
}
if len(accountsPreCreate) != len(accountsPostDelete) {
res.errf("expected # of accounts to match before creation (%d) and after deletion (%d)", len(accountsPreCreate), len(accountsPostDelete))
}
if len(accountsPreCreate)+1 != len(accountsPostCreate) {
res.errf("expected # of accounts to be 1 larger after creation (%d) compare to before creation (%d)", len(accountsPostCreate), len(accountsPreCreate))
}
return
},
},
}

141
validate/account_read.go Normal file
View File

@@ -0,0 +1,141 @@
package validate
import (
"context"
"fmt"
"github.com/kr/pretty"
"strconv"
"github.com/sailpoint/sp-cli/client"
)
var accountReadChecks = []Check{
{
ID: "account-list-and-read",
Description: "List accounts and read each account individual; ensure responses are equivalent",
IsDataModifier: false,
RequiredCommands: []string{
"std:account:read",
"std:account:list",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
accounts, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
return
}
for _, account := range accounts {
acct, _, err := cc.AccountRead(ctx, account.ID(), account.UniqueID())
if err != nil {
res.err(err)
return
}
if acct.Identity != account.Identity {
res.errf("want %q; got %q", account.Identity, acct.Identity)
}
canonicalizeAttributes(account.Attributes)
canonicalizeAttributes(acct.Attributes)
diffs := pretty.Diff(account, *acct)
if len(diffs) > 0 {
for _, diff := range diffs {
res.errf("[identity=%s] Diff: %s", acct.Identity, diff)
}
}
}
},
},
{
ID: "account-not-found",
Description: "Reading an account based on an id which doesn't exist should fail",
IsDataModifier: false,
RequiredCommands: []string{
"std:account:read",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
_, _, err := cc.AccountRead(ctx, "__sailpoint__not__found__", "")
if err == nil {
res.errf("expected error for non-existant identity")
}
},
},
{
ID: "account-schema-check",
Description: "Verify account fields match schema",
IsDataModifier: false,
RequiredCommands: []string{
"std:account:list",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
additionalAttributes := map[string]string{}
attrsByName := map[string]client.AccountSchemaAttribute{}
for _, value := range spec.AccountSchema.Attributes {
attrsByName[value.Name] = value
}
accounts, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
return
}
for _, acct := range accounts {
for name, value := range acct.Attributes {
attr, found := attrsByName[name]
if !found {
additionalAttributes[name] = ""
continue
}
isMulti := false
switch value.(type) {
case []interface{}:
if len(value.([]interface{})) > 0 {
value = value.([]interface{})[0]
} else {
value = nil
}
isMulti = true
}
if attr.Multi != isMulti {
res.errf("expected multi=%t but multi=%t", isMulti, attr.Multi)
}
switch value.(type) {
case string:
if attr.Type == "int" {
_, err := strconv.Atoi(value.(string))
if err != nil {
res.errf("failed to convert int to string on field %s", name)
}
}
if attr.Type != "string" && attr.Type != "int" {
res.errf("expected type %q but was 'string'", attr.Type)
}
case bool:
if attr.Type != "boolean" {
res.errf("expected type %q but was 'boolean'", attr.Type)
}
case float64:
if attr.Type != "int" {
res.errf("expected type %q but was 'int'", attr.Type)
}
case nil:
// okay
default:
panic(fmt.Sprintf("unknown type %T for %q", value, name))
}
}
}
for additional := range additionalAttributes {
res.warnf("additional attribute %q", additional)
}
},
},
}

182
validate/account_update.go Normal file
View File

@@ -0,0 +1,182 @@
package validate
import (
"context"
"time"
"github.com/sailpoint/sp-cli/client"
)
var accountUpdateChecks = []Check{
{
ID: "account-update-single-attrs",
Description: "Test updating writable attributes",
IsDataModifier: true,
RequiredCommands: []string{
"std:account:read",
"std:account:list",
"std:account:update",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
accounts, _, err := cc.AccountList(ctx)
if err != nil {
res.err(err)
}
if len(accounts) == 0 {
res.warnf("account list is empty")
return
}
acct := accounts[len(accounts)-1]
for _, attr := range spec.AccountSchema.Attributes {
if attr.Writable {
if attr.Entitlement {
// Skip entitlement field
continue
}
change := attrChange(&acct, &attr)
_, _, err = cc.AccountUpdate(ctx, acct.ID(), acct.UniqueID(), []client.AttributeChange{change})
if err != nil {
res.errf("update for %q failed: %s", attr.Name, err.Error())
continue
}
// Give the update a chance to propagate
time.Sleep(time.Second)
acct, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.err(err)
continue
}
if acct.Attributes[attr.Name] != change.Value {
res.errf("mismatch for %s. expected %+v; got %+v", attr.Name, change.Value, acct.Attributes[attr.Name])
continue
}
}
}
},
},
{
ID: "account-update-entitlement",
Description: "Test updating entitlement field(s)",
IsDataModifier: true,
RequiredCommands: []string{
"std:entitlement:list",
"std:account:create",
"std:account:read",
"std:account:update",
"std:account:delete",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
entitlementAttr := entitlementAttr(spec)
if entitlementAttr == "" {
res.warnf("no entitlement attribute")
return
}
entitlements, _, err := cc.EntitlementList(ctx, "group")
if err != nil {
res.err(err)
return
}
if len(entitlements) == 0 {
res.warnf("no entitlements found")
return
}
// Create minimal user
input := map[string]interface{}{}
for _, field := range spec.AccountCreateTemplate.Fields {
if field.Required {
input[getFieldName(field)] = genCreateField(field)
}
}
identity := getIdentity(input)
acct, _, err := cc.AccountCreate(ctx, &identity, input)
if err != nil {
res.errf("creating account: %s", err)
return
}
// Give account creation a chance to propagate
time.Sleep(time.Second)
// Add entitlements
for _, e := range entitlements {
acct, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("failed to read account %q", acct.Identity)
}
accEntitlements, err := accountEntitlements(acct, spec)
if err != nil {
res.errf("failed to get acc entitlements")
}
if isAvailableForUpdating(accEntitlements, e.ID()) {
_, _, err = cc.AccountUpdate(ctx, acct.ID(), acct.UniqueID(), []client.AttributeChange{
{
Op: "Add",
Attribute: entitlementAttr,
Value: e.ID(),
},
})
if err != nil {
res.errf("failed to add entitlement %q", e.Identity)
}
acct, _, err = cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("failed to read account %q", acct.Identity)
}
if !accountHasEntitlement(acct, spec, e.ID()) {
res.errf("failed to add entitlement: %q", e.ID())
}
}
}
// Remove entitlements
for _, e := range entitlements {
accEntitlements, err := accountEntitlements(acct, spec)
if err != nil {
res.errf("failed to get acc entitlements")
}
if len(accEntitlements) != 1 {
_, _, err = cc.AccountUpdate(ctx, acct.ID(), acct.UniqueID(), []client.AttributeChange{
{
Op: "Remove",
Attribute: entitlementAttr,
Value: e.ID(),
},
})
if err != nil {
res.errf("failed to remove entitlement %q", e.ID())
}
acct, _, err := cc.AccountRead(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("failed to read account %q", acct.ID())
}
if accountHasEntitlement(acct, spec, e.ID()) {
res.errf("failed to remove entitlement: %q checking", e.ID())
}
}
}
_, err = cc.AccountDelete(ctx, acct.ID(), acct.UniqueID())
if err != nil {
res.errf("deleting account: %s", err)
}
},
},
}

65
validate/check.go Normal file
View File

@@ -0,0 +1,65 @@
package validate
import (
"context"
"fmt"
"github.com/sailpoint/sp-cli/client"
)
var Checks = []Check{}
func init() {
Checks = append(Checks, accountCreateChecks...)
Checks = append(Checks, accountReadChecks...)
Checks = append(Checks, accountUpdateChecks...)
Checks = append(Checks, entitlementReadChecks...)
Checks = append(Checks, testConnChecks...)
}
// Check represents a specific property we want to validate
type Check struct {
ID string
Description string
// IsDataModifier determines a checking that will modify connectors data after applying
IsDataModifier bool
Run func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult)
// RequiredCommands represents a list of commands that use for this check
RequiredCommands []string
}
// CheckResult captures the result of an individual check.
type CheckResult struct {
// ID is a short human readable slug describing the check
ID string
// Errors is a list of errors encountered when running the test.
Errors []string
// Skipped is a short description why the check was skipped
Skipped []string
// Warnings is a list of warnings encountered when running the test.
Warnings []string
}
// err adds the provided err to the list of errors for the check
func (res *CheckResult) err(err error) {
res.Errors = append(res.Errors, err.Error())
}
// errf adds an error to the check result
func (res *CheckResult) errf(format string, a ...interface{}) {
res.Errors = append(res.Errors, fmt.Sprintf(format, a...))
}
// warnf adds an warning to the check result
func (res *CheckResult) warnf(format string, a ...interface{}) {
res.Warnings = append(res.Warnings, fmt.Sprintf(format, a...))
}
// skipf adds a reason of a skipped check to the check result
func (res *CheckResult) skipf(format string, a ...interface{}) {
res.Skipped = append(res.Skipped, fmt.Sprintf(format, a...))
}

View File

@@ -0,0 +1,101 @@
package validate
import (
"context"
"github.com/kr/pretty"
"github.com/sailpoint/sp-cli/client"
)
var entitlementReadChecks = []Check{
{
ID: "entitlement-not-found",
Description: "Verify reading a non existant entitlement fails",
IsDataModifier: false,
RequiredCommands: []string{
"std:entitlement:read",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
_, _, err := cc.EntitlementRead(ctx, "__sailpoint__not__found__", "", "group")
if err == nil {
res.errf("expected error for non-existant entitlement")
}
return
},
},
{
ID: "entitlement-list-read",
Description: "Verify that we can list each entitlement and then read; results should match",
IsDataModifier: false,
RequiredCommands: []string{
"std:entitlement:read",
"std:entitlement:list",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
entitlements, _, err := cc.EntitlementList(ctx, "group")
if err != nil {
res.err(err)
return
}
if len(entitlements) == 0 {
res.warnf("no entitlements")
return
}
for _, e := range entitlements {
eRead, _, err := cc.EntitlementRead(ctx, e.ID(), e.UniqueID(), "group")
if err != nil {
res.errf("failed to read entitlement %q: %s", e.Identity, err.Error())
return
}
if e.Identity != eRead.Identity {
res.errf("want %q; got %q", e.Identity, eRead.Identity)
}
diffs := pretty.Diff(e, *eRead)
if len(diffs) > 0 {
for _, diff := range diffs {
res.errf("[identity=%s] Diff: %s", e.Identity, diff)
}
}
}
},
},
{
ID: "entitlement-schema-check",
Description: "Verify entitlement schema field match",
IsDataModifier: false,
RequiredCommands: []string{
"std:entitlement:list",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
additionalAttributes := map[string]string{}
attrsByName := map[string]client.EntitlementSchemaAttribute{}
for _, value := range spec.EntitlementSchemas[0].Attributes {
attrsByName[value.Name] = value
}
entitlements, _, err := cc.EntitlementList(ctx, "group")
if err != nil {
res.err(err)
return
}
for _, acct := range entitlements {
for name, value := range acct.Attributes {
attr, found := attrsByName[name]
if !found {
additionalAttributes[name] = ""
continue
}
testSchema(res, name, value, attr.Multi, attr.Type)
}
}
for additional := range additionalAttributes {
res.warnf("additional attribute %q", additional)
}
},
},
}

39
validate/test_conn.go Normal file
View File

@@ -0,0 +1,39 @@
package validate
import (
"context"
"encoding/json"
"github.com/sailpoint/sp-cli/client"
)
var testConnChecks = []Check{
{
ID: "test-connection-empty",
Description: "Verify that test connection fails with an empty config",
IsDataModifier: false,
RequiredCommands: []string{
"std:test-connection",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
err := cc.TestConnectionWithConfig(ctx, json.RawMessage("{}"))
if err == nil {
res.errf("expected test-connection failure for empty config")
}
},
},
{
ID: "test-connection-success",
Description: "Verify that test connection succeeds with provided config",
IsDataModifier: false,
RequiredCommands: []string{
"std:test-connection",
},
Run: func(ctx context.Context, spec *client.ConnSpec, cc *client.ConnClient, res *CheckResult) {
_, err := cc.TestConnection(ctx)
if err != nil {
res.err(err)
}
},
},
}

378
validate/util.go Normal file
View File

@@ -0,0 +1,378 @@
package validate
import (
"fmt"
"log"
"math/rand"
"regexp"
"sort"
"strconv"
"strings"
"github.com/sailpoint/sp-cli/client"
)
// entitlementAttr returns the attribute for entitlements
func entitlementAttr(spec *client.ConnSpec) string {
for _, attr := range spec.AccountSchema.Attributes {
if attr.Entitlement {
return attr.Name
}
}
return ""
}
// accountEntitlements returns all entitlements on the account
func accountEntitlements(account *client.Account, spec *client.ConnSpec) ([]string, error) {
entitlementAttr := entitlementAttr(spec)
if entitlementAttr == "" {
return nil, fmt.Errorf("no entitlement attr found")
}
entitlements := []string{}
for _, identity := range account.Attributes[entitlementAttr].([]interface{}) {
entitlements = append(entitlements, identity.(string))
}
return entitlements, nil
}
// accountHasEntitlement returns whether or not an account has a specific entitlement
func accountHasEntitlement(account *client.Account, spec *client.ConnSpec, entitlementID string) bool {
entitlements, err := accountEntitlements(account, spec)
if err != nil {
panic(err.Error())
}
for _, id := range entitlements {
if id == entitlementID {
return true
}
}
return false
}
// Diff is a difference between two values
type diff struct {
Field string
A interface{}
B interface{}
}
// compareIntersection compares two objects and returns any differences between
// the fields that are common to both objects.
func compareIntersection(a map[string]interface{}, b map[string]interface{}) (diffs []diff) {
for key := range a {
if _, found := b[key]; !found {
continue
}
switch v := b[key].(type) {
case []interface{}:
var sliceB []string
for _, val := range v {
sliceB = append(sliceB, val.(string))
}
sliceA, ok := a[key].([]string)
if !ok {
log.Println("failed to convert to sliceA to slice of strings")
}
for i := range sliceA {
if sliceA[i] != sliceB[i] {
diffs = append(diffs, diff{
Field: key,
A: a[key],
B: b[key],
})
}
}
default:
if a[key] != b[key] {
diffs = append(diffs, diff{
Field: key,
A: a[key],
B: b[key],
})
}
}
}
return diffs
}
const (
fieldTypeStatic = "static"
fieldTypeGenerator = "generator"
generatorPassword = "Create Password"
generatorAccountId = "Create Unique Account ID"
)
// genCreateField generates a value for the provided account create template field
func genCreateField(field client.AccountCreateTemplateField) interface{} {
// Return typed based value if the field is in deprecated format
// TODO: Once we move away from the old format, this should also be removed
if field.Key == "" {
return genValueByTypeAndName(field)
}
// Return default value if field is set to static
if field.InitialValue.Type == fieldTypeStatic {
return field.InitialValue.Attributes.Value
}
// Build value for generator field
if field.InitialValue.Type == fieldTypeGenerator {
if field.InitialValue.Attributes.Name == generatorPassword {
return fmt.Sprintf("RandomPassword.%d", rand.Intn(65536))
}
if field.InitialValue.Attributes.Name == generatorAccountId {
template := field.InitialValue.Attributes.Template
counterRegex := regexp.MustCompile(`\$\(uniqueCounter\)`)
template = counterRegex.ReplaceAllString(template, strconv.Itoa(rand.Intn(65536)))
stringRegex := regexp.MustCompile(`\$\(.*?\)`)
template = stringRegex.ReplaceAllString(template, fmt.Sprintf("string%d", rand.Intn(99)))
return template
}
}
// For other cases including identity attributes, use the default way to generate value by type.
return genValueByTypeAndName(field)
}
// getFieldName returns the name of the field
// TODO: This is to support both key and name base field. Once the name based filds are gone, we can remove this helper method
func getFieldName(field client.AccountCreateTemplateField) string {
if field.Key == "" {
return field.Name
}
return field.Key
}
// genValueByTypeAndName generates attribute values base on field type and name
func genValueByTypeAndName(field client.AccountCreateTemplateField) interface{} {
switch field.Type {
case "string":
if getFieldName(field) == "email" || getFieldName(field) == "name" {
return fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
} else if getFieldName(field) == "siteRole" {
return "Creator"
} else {
return fmt.Sprintf("string.%d", rand.Intn(65536))
}
case "boolean":
// TODO: we want to eventually remove these. These fields needs only for Smartsheet connectors
if getFieldName(field) == "admin" {
return false
}
if getFieldName(field) == "groupAdmin" {
return false
}
if getFieldName(field) == "licensedSheetCreator" {
return false
}
if getFieldName(field) == "resourceViewer" {
return false
}
return true
case "array":
// TODO: we need to avoid hardcoding any specific code in the validation suite.
// Freshservice connector only
if getFieldName(field) == "roles" {
return []string{"27000245813:entire_helpdesk"}
}
return nil
default:
panic(fmt.Sprintf("unknown type: %q", field.Type))
}
}
// testSchema verifies that value is of the expectedType
func testSchema(res *CheckResult, attrName string, value interface{}, expectedMulti bool, expectedType string) {
// Check if it's a multi value (array) and unwrap if necessary
// TODO should we check all values in the array?
isMulti := false
switch value.(type) {
case []interface{}:
if len(value.([]interface{})) > 0 {
value = value.([]interface{})[0]
} else {
value = nil
}
isMulti = true
}
if expectedMulti != isMulti {
res.errf("expected multi=%t but multi=%t", expectedMulti, isMulti)
}
switch value.(type) {
case string:
if expectedType == "int" {
_, err := strconv.Atoi(value.(string))
if err != nil {
res.errf("failed to convert int to string on field %s", attrName)
}
}
if expectedType != "string" && expectedType != "int" {
res.errf("%s expected type %q but was 'string'", attrName, expectedType)
}
case bool:
if expectedType != "boolean" {
res.errf("expected type %q but was 'boolean'", expectedType)
}
case float64:
if expectedType != "int" {
res.errf("expected type %q but was 'int'", expectedType)
}
case nil:
// If a value is nil we can't validate the type.
default:
res.errf("unknown type %T for %q", value, attrName)
}
}
// attrChange generates an attribute change event for the provided account and
// attribute.
func attrChange(acct *client.Account, attr *client.AccountSchemaAttribute) client.AttributeChange {
var op string
switch attr.Multi {
case true:
op = "Add"
case false:
op = "Set"
}
var newValue interface{}
switch attr.Type {
case "string":
if attr.Name == "email" {
newValue = fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
} else {
newValue = fmt.Sprintf("string.%x", rand.Intn(16777216))
}
case "int":
if current, found := acct.Attributes[attr.Name]; found {
newValue = current.(int) + 1
} else {
newValue = 42
}
case "boolean":
// flip
if current, found := acct.Attributes[attr.Name]; found {
newValue = current.(bool)
} else {
newValue = true
}
}
return client.AttributeChange{
Op: op,
Attribute: attr.Name,
Value: newValue,
}
}
func isAvailableForUpdating(entitlements []string, entitlementID string) bool {
entID := strings.Split(entitlementID, ":")[0]
for _, ent := range entitlements {
if entID == strings.Split(ent, ":")[0] {
return false
}
}
return true
}
func getIdentity(input map[string]interface{}) string {
_, ok := input["email"]
if ok {
return input["email"].(string)
}
_, ok = input["username"]
if ok {
return input["username"].(string)
}
_, ok = input["name"]
if ok {
return input["name"].(string)
}
return fmt.Sprintf("test.%d@example.com", rand.Intn(65536))
}
func canonicalizeAttributes(attrs map[string]interface{}) {
for key, val := range attrs {
switch val.(type) {
case []interface{}:
var arrayOfStrings []string
for _, elem := range val.([]interface{}) {
arrayOfStrings = append(arrayOfStrings, fmt.Sprintf("%v", elem))
}
sort.Strings(arrayOfStrings)
attrs[key] = arrayOfStrings
case []float64:
var arrayOfFloats []float64
for _, elem := range val.([]float64) {
arrayOfFloats = append(arrayOfFloats, elem)
}
sort.Float64s(arrayOfFloats)
attrs[key] = arrayOfFloats
case []int:
var arrayOfInts []int
for _, elem := range val.([]int) {
arrayOfInts = append(arrayOfInts, elem)
}
sort.Ints(arrayOfInts)
attrs[key] = arrayOfInts
case []string:
var arrayOfStrings []string
for _, elem := range val.([]string) {
arrayOfStrings = append(arrayOfStrings, elem)
}
sort.Strings(arrayOfStrings)
attrs[key] = arrayOfStrings
}
}
}
func isCheckPossible(commands, checkCommands []string) (bool, []string) {
var result []string
commandsMap := make(map[string]bool)
for _, c := range commands {
commandsMap[c] = true
}
for _, cc := range checkCommands {
_, ok := commandsMap[cc]
if !ok {
result = append(result, cc)
}
}
if len(result) != 0 {
return false, result
}
return true, nil
}

74
validate/validate.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
package validate
import (
"context"
"fmt"
"log"
"math/rand"
"strings"
"time"
"github.com/sailpoint/sp-cli/client"
)
// Validator runs checks for a specific connector
type Validator struct {
cfg Config
cc *client.ConnClient
connSpec *client.ConnSpec
}
// Config provides options for how the validator runs
type Config struct {
// Check specifies a single check that should be run. If this is empty then
// all checks are run.
Check string
// ReadOnly specifies a type of validation.
// If ReadOnly set 'true' validator will run all checks that don't make any modifications.
ReadOnly bool
}
// NewValidator creates a new validator with provided config and ConnClient
func NewValidator(cfg Config, cc *client.ConnClient) *Validator {
return &Validator{
cfg: cfg,
cc: cc,
}
}
// Run runs the validator suite
func (v *Validator) Run(ctx context.Context) (results []CheckResult, err error) {
rand.Seed(time.Now().UnixNano())
spec, err := v.cc.SpecRead(ctx)
if err != nil {
return nil, err
}
for _, check := range Checks {
if v.cfg.ReadOnly && check.IsDataModifier {
continue
}
if len(v.cfg.Check) > 0 && check.ID != v.cfg.Check {
continue
}
log.Printf("running check %q", check.ID)
res := &CheckResult{
ID: check.ID,
}
if ok, results := isCheckPossible(spec.Commands, check.RequiredCommands); ok {
check.Run(ctx, spec, v.cc, res)
} else {
res.skipf("Skipping check due to unimplemented commands on a connector: %s", strings.Join(results, ", "))
}
results = append(results, *res)
fmt.Println()
}
return results, nil
}

5
vendor/github.com/fatih/color/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,5 @@
language: go
go:
- 1.8.x
- tip

27
vendor/github.com/fatih/color/Gopkg.lock generated vendored Normal file
View File

@@ -0,0 +1,27 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e8a50671c3cb93ea935bf210b1cd20702876b9d9226129be581ef646d1565cdc"
solver-name = "gps-cdcl"
solver-version = 1

30
vendor/github.com/fatih/color/Gopkg.toml generated vendored Normal file
View File

@@ -0,0 +1,30 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/mattn/go-colorable"
version = "0.0.9"
[[constraint]]
name = "github.com/mattn/go-isatty"
version = "0.0.3"

20
vendor/github.com/fatih/color/LICENSE.md generated vendored Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

179
vendor/github.com/fatih/color/README.md generated vendored Normal file
View File

@@ -0,0 +1,179 @@
# Color [![GoDoc](https://godoc.org/github.com/fatih/color?status.svg)](https://godoc.org/github.com/fatih/color) [![Build Status](https://img.shields.io/travis/fatih/color.svg?style=flat-square)](https://travis-ci.org/fatih/color)
Color lets you use colorized outputs in terms of [ANSI Escape
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It
has support for Windows too! The API can be used in several ways, pick one that
suits you.
![Color](https://i.imgur.com/c1JI0lA.png)
## Install
```bash
go get github.com/fatih/color
```
Note that the `vendor` folder is here for stability. Remove the folder if you
already have the dependencies in your GOPATH.
## Examples
### Standard colors
```go
// Print with default helper functions
color.Cyan("Prints text in cyan.")
// A newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// These are using the default foreground colors
color.Red("We have red")
color.Magenta("And many others ..")
```
### Mix and reuse colors
```go
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with white background.")
```
### Use your own output (io.Writer)
```go
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(writer, "This will print text in blue.")
```
### Custom print functions (PrintFunc)
```go
// Create a custom print function for convenience
red := color.New(color.FgRed).PrintfFunc()
red("Warning")
red("Error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("Don't forget this...")
```
### Custom fprint functions (FprintFunc)
```go
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, "Don't forget this...")
```
### Insert into noncolor strings (SprintFunc)
```go
// Create SprintXxx functions to mix strings with other non-colorized strings:
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error"))
info := color.New(color.FgWhite, color.BgGreen).SprintFunc()
fmt.Printf("This %s rocks!\n", info("package"))
// Use helper functions
fmt.Println("This", color.RedString("warning"), "should be not neglected.")
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.")
// Windows supported too! Just don't forget to change the output to color.Output
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
```
### Plug into existing code
```go
// Use handy standard colors
color.Set(color.FgYellow)
fmt.Println("Existing text will now be in yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // Don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // Use it in your function
fmt.Println("All text will now be bold magenta.")
```
### Disable/Enable color
There might be a case where you want to explicitly disable/enable color output. the
`go-isatty` package will automatically disable color output for non-tty output streams
(for example if the output were piped directly to `less`)
`Color` has support to disable/enable colors both globally and for single color
definitions. For example suppose you have a CLI app and a `--no-color` bool flag. You
can easily disable the color output with:
```go
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
```
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
```go
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
```
## Todo
* Save/Return previous values
* Evaluate fmt.Formatter interface
## Credits
* [Fatih Arslan](https://github.com/fatih)
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable)
## License
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details

603
vendor/github.com/fatih/color/color.go generated vendored Normal file
View File

@@ -0,0 +1,603 @@
package color
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
)
var (
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. This is a global option and affects all colors. For more control
// over each color block use the methods DisableColor() individually.
NoColor = os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
// Output defines the standard output of the print functions. By default
// os.Stdout is used.
Output = colorable.NewColorableStdout()
// Error defines a color supporting writer for os.Stderr.
Error = colorable.NewColorableStderr()
// colorsCache is used to reduce the count of created Color objects and
// allows to reuse already created objects with required Attribute.
colorsCache = make(map[Attribute]*Color)
colorsCacheMu sync.Mutex // protects colorsCache
)
// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
params []Attribute
noColor *bool
}
// Attribute defines a single SGR Code
type Attribute int
const escape = "\x1b"
// Base attributes
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// Foreground text colors
const (
FgBlack Attribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack Attribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack Attribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack Attribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
// New returns a newly created color object.
func New(value ...Attribute) *Color {
c := &Color{params: make([]Attribute, 0)}
c.Add(value...)
return c
}
// Set sets the given parameters immediately. It will change the color of
// output with the given SGR parameters until color.Unset() is called.
func Set(p ...Attribute) *Color {
c := New(p...)
c.Set()
return c
}
// Unset resets all escape attributes and clears the output. Usually should
// be called after Set().
func Unset() {
if NoColor {
return
}
fmt.Fprintf(Output, "%s[%dm", escape, Reset)
}
// Set sets the SGR sequence.
func (c *Color) Set() *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprintf(Output, c.format())
return c
}
func (c *Color) unset() {
if c.isNoColorSet() {
return
}
Unset()
}
func (c *Color) setWriter(w io.Writer) *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprintf(w, c.format())
return c
}
func (c *Color) unsetWriter(w io.Writer) {
if c.isNoColorSet() {
return
}
if NoColor {
return
}
fmt.Fprintf(w, "%s[%dm", escape, Reset)
}
// Add is used to chain SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
func (c *Color) Add(value ...Attribute) *Color {
c.params = append(c.params, value...)
return c
}
func (c *Color) prepend(value Attribute) {
c.params = append(c.params, 0)
copy(c.params[1:], c.params[0:])
c.params[0] = value
}
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprint(w, a...)
}
// Print formats using the default formats for its operands and writes to
// standard output. Spaces are added between operands when neither is a
// string. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Print(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprint(Output, a...)
}
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprintf(w, format, a...)
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
// This is the standard fmt.Printf() method wrapped with the given color.
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintf(Output, format, a...)
}
// Fprintln formats using the default formats for its operands and writes to w.
// Spaces are always added between operands and a newline is appended.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprintln(w, a...)
}
// Println formats using the default formats for its operands and writes to
// standard output. Spaces are always added between operands and a newline is
// appended. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintln(Output, a...)
}
// Sprint is just like Print, but returns a string instead of printing it.
func (c *Color) Sprint(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...interface{}) string {
return c.wrap(fmt.Sprintln(a...))
}
// Sprintf is just like Printf, but returns a string instead of printing it.
func (c *Color) Sprintf(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
// FprintFunc returns a new function that prints the passed arguments as
// colorized with color.Fprint().
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprint(w, a...)
}
}
// PrintFunc returns a new function that prints the passed arguments as
// colorized with color.Print().
func (c *Color) PrintFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Print(a...)
}
}
// FprintfFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintf().
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) {
return func(w io.Writer, format string, a ...interface{}) {
c.Fprintf(w, format, a...)
}
}
// PrintfFunc returns a new function that prints the passed arguments as
// colorized with color.Printf().
func (c *Color) PrintfFunc() func(format string, a ...interface{}) {
return func(format string, a ...interface{}) {
c.Printf(format, a...)
}
}
// FprintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintln().
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprintln(w, a...)
}
}
// PrintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Println().
func (c *Color) PrintlnFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Println(a...)
}
}
// SprintFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprint(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output, example:
//
// put := New(FgYellow).SprintFunc()
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
func (c *Color) SprintFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
}
// SprintfFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
return func(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
}
// SprintlnFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprintln(a...))
}
}
// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
// an example output might be: "1;36" -> bold cyan
func (c *Color) sequence() string {
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
// wrap wraps the s string with the colors attributes. The string is ready to
// be printed.
func (c *Color) wrap(s string) string {
if c.isNoColorSet() {
return s
}
return c.format() + s + c.unformat()
}
func (c *Color) format() string {
return fmt.Sprintf("%s[%sm", escape, c.sequence())
}
func (c *Color) unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
// DisableColor disables the color output. Useful to not change any existing
// code and still being able to output. Can be used for flags like
// "--no-color". To enable back use EnableColor() method.
func (c *Color) DisableColor() {
c.noColor = boolPtr(true)
}
// EnableColor enables the color output. Use it in conjunction with
// DisableColor(). Otherwise this method has no side effects.
func (c *Color) EnableColor() {
c.noColor = boolPtr(false)
}
func (c *Color) isNoColorSet() bool {
// check first if we have user setted action
if c.noColor != nil {
return *c.noColor
}
// if not return the global option, which is disabled by default
return NoColor
}
// Equals returns a boolean value indicating whether two colors are equal.
func (c *Color) Equals(c2 *Color) bool {
if len(c.params) != len(c2.params) {
return false
}
for _, attr := range c.params {
if !c2.attrExists(attr) {
return false
}
}
return true
}
func (c *Color) attrExists(a Attribute) bool {
for _, attr := range c.params {
if attr == a {
return true
}
}
return false
}
func boolPtr(v bool) *bool {
return &v
}
func getCachedColor(p Attribute) *Color {
colorsCacheMu.Lock()
defer colorsCacheMu.Unlock()
c, ok := colorsCache[p]
if !ok {
c = New(p)
colorsCache[p] = c
}
return c
}
func colorPrint(format string, p Attribute, a ...interface{}) {
c := getCachedColor(p)
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
if len(a) == 0 {
c.Print(format)
} else {
c.Printf(format, a...)
}
}
func colorString(format string, p Attribute, a ...interface{}) string {
c := getCachedColor(p)
if len(a) == 0 {
return c.SprintFunc()(format)
}
return c.SprintfFunc()(format, a...)
}
// Black is a convenient helper function to print with black foreground. A
// newline is appended to format by default.
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) }
// Red is a convenient helper function to print with red foreground. A
// newline is appended to format by default.
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) }
// Green is a convenient helper function to print with green foreground. A
// newline is appended to format by default.
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) }
// Yellow is a convenient helper function to print with yellow foreground.
// A newline is appended to format by default.
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) }
// Blue is a convenient helper function to print with blue foreground. A
// newline is appended to format by default.
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) }
// Magenta is a convenient helper function to print with magenta foreground.
// A newline is appended to format by default.
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) }
// Cyan is a convenient helper function to print with cyan foreground. A
// newline is appended to format by default.
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) }
// White is a convenient helper function to print with white foreground. A
// newline is appended to format by default.
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) }
// BlackString is a convenient helper function to return a string with black
// foreground.
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) }
// RedString is a convenient helper function to return a string with red
// foreground.
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) }
// GreenString is a convenient helper function to return a string with green
// foreground.
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) }
// YellowString is a convenient helper function to return a string with yellow
// foreground.
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) }
// BlueString is a convenient helper function to return a string with blue
// foreground.
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) }
// MagentaString is a convenient helper function to return a string with magenta
// foreground.
func MagentaString(format string, a ...interface{}) string {
return colorString(format, FgMagenta, a...)
}
// CyanString is a convenient helper function to return a string with cyan
// foreground.
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) }
// WhiteString is a convenient helper function to return a string with white
// foreground.
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) }
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
// newline is appended to format by default.
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) }
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
// newline is appended to format by default.
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) }
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
// newline is appended to format by default.
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) }
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
// A newline is appended to format by default.
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) }
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
// newline is appended to format by default.
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) }
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
// A newline is appended to format by default.
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) }
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
// newline is appended to format by default.
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) }
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
// newline is appended to format by default.
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) }
// HiBlackString is a convenient helper function to return a string with hi-intensity black
// foreground.
func HiBlackString(format string, a ...interface{}) string {
return colorString(format, FgHiBlack, a...)
}
// HiRedString is a convenient helper function to return a string with hi-intensity red
// foreground.
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) }
// HiGreenString is a convenient helper function to return a string with hi-intensity green
// foreground.
func HiGreenString(format string, a ...interface{}) string {
return colorString(format, FgHiGreen, a...)
}
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
// foreground.
func HiYellowString(format string, a ...interface{}) string {
return colorString(format, FgHiYellow, a...)
}
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
// foreground.
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) }
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
// foreground.
func HiMagentaString(format string, a ...interface{}) string {
return colorString(format, FgHiMagenta, a...)
}
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
// foreground.
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) }
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
// foreground.
func HiWhiteString(format string, a ...interface{}) string {
return colorString(format, FgHiWhite, a...)
}

133
vendor/github.com/fatih/color/doc.go generated vendored Normal file
View File

@@ -0,0 +1,133 @@
/*
Package color is an ANSI color package to output colorized or SGR defined
output to the standard output. The API can be used in several way, pick one
that suits you.
Use simple and default helper functions with predefined foreground colors:
color.Cyan("Prints text in cyan.")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
However there are times where custom color mixes are required. Below are some
examples to create custom color objects and use the print functions of each
separate color object.
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc()
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended.
However only for color.SprintXXX functions, user should use fmt.FprintXXX and
set the output to color.Output:
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
Using with existing code is possible. Just use the Set() method to set the
standard output to the given parameters. That way a rewrite of an existing
code is not required.
// Use handy standard colors.
color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function
fmt.Println("All text will be now bold magenta.")
There might be a case where you want to disable color output (for example to
pipe the standard output of your app to somewhere else). `Color` has support to
disable colors both globally and for single color definition. For example
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
the color output with:
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
*/
package color

12
vendor/github.com/fsnotify/fsnotify/.editorconfig generated vendored Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*.go]
indent_style = tab
indent_size = 4
insert_final_newline = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

1
vendor/github.com/fsnotify/fsnotify/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1 @@
go.sum linguist-generated

6
vendor/github.com/fsnotify/fsnotify/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,6 @@
# Setup a Global .gitignore for OS and editor generated files:
# https://help.github.com/articles/ignoring-files
# git config --global core.excludesfile ~/.gitignore_global
.vagrant
*.sublime-project

36
vendor/github.com/fsnotify/fsnotify/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,36 @@
sudo: false
language: go
go:
- "stable"
- "1.11.x"
- "1.10.x"
- "1.9.x"
matrix:
include:
- go: "stable"
env: GOLINT=true
allow_failures:
- go: tip
fast_finish: true
before_install:
- if [ ! -z "${GOLINT}" ]; then go get -u golang.org/x/lint/golint; fi
script:
- go test --race ./...
after_script:
- test -z "$(gofmt -s -l -w . | tee /dev/stderr)"
- if [ ! -z "${GOLINT}" ]; then echo running golint; golint --set_exit_status ./...; else echo skipping golint; fi
- go vet ./...
os:
- linux
- osx
- windows
notifications:
email: false

52
vendor/github.com/fsnotify/fsnotify/AUTHORS generated vendored Normal file
View File

@@ -0,0 +1,52 @@
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# You can update this list using the following command:
#
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}'
# Please keep the list sorted.
Aaron L <aaron@bettercoder.net>
Adrien Bustany <adrien@bustany.org>
Amit Krishnan <amit.krishnan@oracle.com>
Anmol Sethi <me@anmol.io>
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Bruno Bigras <bigras.bruno@gmail.com>
Caleb Spare <cespare@gmail.com>
Case Nelson <case@teammating.com>
Chris Howey <chris@howey.me> <howeyc@gmail.com>
Christoffer Buchholz <christoffer.buchholz@gmail.com>
Daniel Wagner-Hall <dawagner@gmail.com>
Dave Cheney <dave@cheney.net>
Evan Phoenix <evan@fallingsnow.net>
Francisco Souza <f@souza.cc>
Hari haran <hariharan.uno@gmail.com>
John C Barstow
Kelvin Fo <vmirage@gmail.com>
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp>
Matt Layher <mdlayher@gmail.com>
Nathan Youngman <git@nathany.com>
Nickolai Zeldovich <nickolai@csail.mit.edu>
Patrick <patrick@dropbox.com>
Paul Hammond <paul@paulhammond.org>
Pawel Knap <pawelknap88@gmail.com>
Pieter Droogendijk <pieter@binky.org.uk>
Pursuit92 <JoshChase@techpursuit.net>
Riku Voipio <riku.voipio@linaro.org>
Rob Figueiredo <robfig@gmail.com>
Rodrigo Chiossi <rodrigochiossi@gmail.com>
Slawek Ligus <root@ooz.ie>
Soge Zhang <zhssoge@gmail.com>
Tiffany Jernigan <tiffany.jernigan@intel.com>
Tilak Sharma <tilaks@google.com>
Tom Payne <twpayne@gmail.com>
Travis Cline <travis.cline@gmail.com>
Tudor Golubenco <tudor.g@gmail.com>
Vahe Khachikyan <vahe@live.ca>
Yukang <moorekang@gmail.com>
bronze1man <bronze1man@gmail.com>
debrando <denis.brandolini@gmail.com>
henrikedwards <henrik.edwards@gmail.com>
铁哥 <guotie.9@gmail.com>

317
vendor/github.com/fsnotify/fsnotify/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,317 @@
# Changelog
## v1.4.7 / 2018-01-09
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
* Tests: Fix missing verb on format string (thanks @rchiossi)
* Linux: Fix deadlock in Remove (thanks @aarondl)
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
* Docs: Moved FAQ into the README (thanks @vahe)
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
* Docs: replace references to OS X with macOS
## v1.4.2 / 2016-10-10
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
## v1.4.1 / 2016-10-04
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack)
## v1.4.0 / 2016-10-01
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie)
## v1.3.1 / 2016-06-28
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc)
## v1.3.0 / 2016-04-19
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135)
## v1.2.10 / 2016-03-02
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj)
## v1.2.9 / 2016-01-13
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep)
## v1.2.8 / 2015-12-17
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test)
* inotify: fix race in test
* enable race detection for continuous integration (Linux, Mac, Windows)
## v1.2.5 / 2015-10-17
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki)
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken)
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie)
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion)
## v1.2.1 / 2015-10-14
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx)
## v1.2.0 / 2015-02-08
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD)
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD)
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59)
## v1.1.1 / 2015-02-05
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD)
## v1.1.0 / 2014-12-12
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43)
* add low-level functions
* only need to store flags on directories
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13)
* done can be an unbuffered channel
* remove calls to os.NewSyscallError
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher)
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48)
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## v1.0.4 / 2014-09-07
* kqueue: add dragonfly to the build tags.
* Rename source code files, rearrange code so exported APIs are at the top.
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang)
## v1.0.3 / 2014-08-19
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36)
## v1.0.2 / 2014-08-17
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
## v1.0.0 / 2014-08-15
* [API] Remove AddWatch on Windows, use Add.
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30)
* Minor updates based on feedback from golint.
## dev / 2014-07-09
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify).
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno)
## dev / 2014-07-04
* kqueue: fix incorrect mutex used in Close()
* Update example to demonstrate usage of Op.
## dev / 2014-06-28
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4)
* Fix for String() method on Event (thanks Alex Brainman)
* Don't build on Plan 9 or Solaris (thanks @4ad)
## dev / 2014-06-21
* Events channel of type Event rather than *Event.
* [internal] use syscall constants directly for inotify and kqueue.
* [internal] kqueue: rename events to kevents and fileEvent to event.
## dev / 2014-06-19
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally).
* [internal] remove cookie from Event struct (unused).
* [internal] Event struct has the same definition across every OS.
* [internal] remove internal watch and removeWatch methods.
## dev / 2014-06-12
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove().
* [API] Pluralized channel names: Events and Errors.
* [API] Renamed FileEvent struct to Event.
* [API] Op constants replace methods like IsCreate().
## dev / 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## dev / 2014-05-23
* [API] Remove current implementation of WatchFlags.
* current implementation doesn't take advantage of OS for efficiency
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes
* no tests for the current implementation
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
## v0.9.3 / 2014-12-31
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## v0.9.2 / 2014-08-17
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
## v0.9.1 / 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## v0.9.0 / 2014-01-17
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
## v0.8.12 / 2013-11-13
* [API] Remove FD_SET and friends from Linux adapter
## v0.8.11 / 2013-11-02
* [Doc] Add Changelog [#72][] (thanks @nathany)
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
## v0.8.10 / 2013-10-19
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
* [Doc] specify OS-specific limits in README (thanks @debrando)
## v0.8.9 / 2013-09-08
* [Doc] Contributing (thanks @nathany)
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
* [Doc] GoCI badge in README (Linux only) [#60][]
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
## v0.8.8 / 2013-06-17
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
## v0.8.7 / 2013-06-03
* [API] Make syscall flags internal
* [Fix] inotify: ignore event changes
* [Fix] race in symlink test [#45][] (reported by @srid)
* [Fix] tests on Windows
* lower case error messages
## v0.8.6 / 2013-05-23
* kqueue: Use EVT_ONLY flag on Darwin
* [Doc] Update README with full example
## v0.8.5 / 2013-05-09
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
## v0.8.4 / 2013-04-07
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
## v0.8.3 / 2013-03-13
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
## v0.8.2 / 2013-02-07
* [Doc] add Authors
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
## v0.8.1 / 2013-01-09
* [Fix] Windows path separators
* [Doc] BSD License
## v0.8.0 / 2012-11-09
* kqueue: directory watching improvements (thanks @vmirage)
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
## v0.7.4 / 2012-10-09
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
* [Fix] kqueue: modify after recreation of file
## v0.7.3 / 2012-09-27
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
* [Fix] kqueue: no longer get duplicate CREATE events
## v0.7.2 / 2012-09-01
* kqueue: events for created directories
## v0.7.1 / 2012-07-14
* [Fix] for renaming files
## v0.7.0 / 2012-07-02
* [Feature] FSNotify flags
* [Fix] inotify: Added file name back to event path
## v0.6.0 / 2012-06-06
* kqueue: watch files after directory created (thanks @tmc)
## v0.5.1 / 2012-05-22
* [Fix] inotify: remove all watches before Close()
## v0.5.0 / 2012-05-03
* [API] kqueue: return errors during watch instead of sending over channel
* kqueue: match symlink behavior on Linux
* inotify: add `DELETE_SELF` (requested by @taralx)
* [Fix] kqueue: handle EINTR (reported by @robfig)
* [Doc] Godoc example [#1][] (thanks @davecheney)
## v0.4.0 / 2012-03-30
* Go 1 released: build with go tool
* [Feature] Windows support using winfsnotify
* Windows does not have attribute change notifications
* Roll attribute notifications into IsModify
## v0.3.0 / 2012-02-19
* kqueue: add files when watch directory
## v0.2.0 / 2011-12-30
* update to latest Go weekly code
## v0.1.0 / 2011-10-19
* kqueue: add watch on file creation to match inotify
* kqueue: create file event
* inotify: ignore `IN_IGNORED` events
* event String()
* linux: common FileEvent functions
* initial commit
[#79]: https://github.com/howeyc/fsnotify/pull/79
[#77]: https://github.com/howeyc/fsnotify/pull/77
[#72]: https://github.com/howeyc/fsnotify/issues/72
[#71]: https://github.com/howeyc/fsnotify/issues/71
[#70]: https://github.com/howeyc/fsnotify/issues/70
[#63]: https://github.com/howeyc/fsnotify/issues/63
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#60]: https://github.com/howeyc/fsnotify/issues/60
[#59]: https://github.com/howeyc/fsnotify/issues/59
[#49]: https://github.com/howeyc/fsnotify/issues/49
[#45]: https://github.com/howeyc/fsnotify/issues/45
[#40]: https://github.com/howeyc/fsnotify/issues/40
[#36]: https://github.com/howeyc/fsnotify/issues/36
[#33]: https://github.com/howeyc/fsnotify/issues/33
[#29]: https://github.com/howeyc/fsnotify/issues/29
[#25]: https://github.com/howeyc/fsnotify/issues/25
[#24]: https://github.com/howeyc/fsnotify/issues/24
[#21]: https://github.com/howeyc/fsnotify/issues/21

77
vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,77 @@
# Contributing
## Issues
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues).
* Please indicate the platform you are using fsnotify on.
* A code example to reproduce the problem is appreciated.
## Pull Requests
### Contributor License Agreement
fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual).
Please indicate that you have signed the CLA in your pull request.
### How fsnotify is Developed
* Development is done on feature branches.
* Tests are run on BSD, Linux, macOS and Windows.
* Pull requests are reviewed and [applied to master][am] using [hub][].
* Maintainers may modify or squash commits rather than asking contributors to.
* To issue a new release, the maintainers will:
* Update the CHANGELOG
* Tag a version, which will become available through gopkg.in.
### How to Fork
For smooth sailing, always use the original import path. Installing with `go get` makes this easy.
1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Ensure everything works and the tests pass (see below)
4. Commit your changes (`git commit -am 'Add some feature'`)
Contribute upstream:
1. Fork fsnotify on GitHub
2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`)
3. Push to the branch (`git push fork my-new-feature`)
4. Create a new Pull Request on GitHub
This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/).
### Testing
fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows.
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on.
To aid in cross-platform testing there is a Vagrantfile for Linux and BSD.
* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/)
* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder.
* Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password)
* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd fsnotify/fsnotify; go test'`.
* When you're done, you will want to halt or destroy the Vagrant boxes.
Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory.
Right now there is no equivalent solution for Windows and macOS, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads).
### Maintainers
Help maintaining fsnotify is welcome. To be a maintainer:
* Submit a pull request and sign the CLA as above.
* You must be able to run the test suite on Mac, Windows, Linux and BSD.
To keep master clean, the fsnotify project uses the "apply mail" workflow outlined in Nathaniel Talbott's post ["Merge pull request" Considered Harmful][am]. This requires installing [hub][].
All code changes should be internal pull requests.
Releases are tagged using [Semantic Versioning](http://semver.org/).
[hub]: https://github.com/github/hub
[am]: http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.VGa5yZPF_Zs

28
vendor/github.com/fsnotify/fsnotify/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

130
vendor/github.com/fsnotify/fsnotify/README.md generated vendored Normal file
View File

@@ -0,0 +1,130 @@
# File system notifications for Go
[![GoDoc](https://godoc.org/github.com/fsnotify/fsnotify?status.svg)](https://godoc.org/github.com/fsnotify/fsnotify) [![Go Report Card](https://goreportcard.com/badge/github.com/fsnotify/fsnotify)](https://goreportcard.com/report/github.com/fsnotify/fsnotify)
fsnotify utilizes [golang.org/x/sys](https://godoc.org/golang.org/x/sys) rather than `syscall` from the standard library. Ensure you have the latest version installed by running:
```console
go get -u golang.org/x/sys/...
```
Cross platform: Windows, Linux, BSD and macOS.
| Adapter | OS | Status |
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| inotify | Linux 2.6.27 or later, Android\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
| kqueue | BSD, macOS, iOS\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
| ReadDirectoryChangesW | Windows | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) |
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) |
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/issues/12) |
| fanotify | Linux 2.6.37+ | [Planned](https://github.com/fsnotify/fsnotify/issues/114) |
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) |
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) |
\* Android and iOS are untested.
Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information.
## API stability
fsnotify is a fork of [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify) with a new API as of v1.0. The API is based on [this design document](http://goo.gl/MrYxyA).
All [releases](https://github.com/fsnotify/fsnotify/releases) are tagged based on [Semantic Versioning](http://semver.org/). Further API changes are [planned](https://github.com/fsnotify/fsnotify/milestones), and will be tagged with a new major revision number.
Go 1.6 supports dependencies located in the `vendor/` folder. Unless you are creating a library, it is recommended that you copy fsnotify into `vendor/github.com/fsnotify/fsnotify` within your project, and likewise for `golang.org/x/sys`.
## Usage
```go
package main
import (
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
err = watcher.Add("/tmp/foo")
if err != nil {
log.Fatal(err)
}
<-done
}
```
## Contributing
Please refer to [CONTRIBUTING][] before opening an issue or pull request.
## Example
See [example_test.go](https://github.com/fsnotify/fsnotify/blob/master/example_test.go).
## FAQ
**When a file is moved to another directory is it still being watched?**
No (it shouldn't be, unless you are watching where it was moved to).
**When I watch a directory, are all subdirectories watched as well?**
No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]).
**Do I have to watch the Error and Event channels in a separate goroutine?**
As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7])
**Why am I receiving multiple events for the same file on OS X?**
Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]).
**How many files can be watched at once?**
There are OS-specific limits as to how many watches can be created:
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error.
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error.
**Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?**
fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications.
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#18]: https://github.com/fsnotify/fsnotify/issues/18
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#7]: https://github.com/howeyc/fsnotify/issues/7
[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md
## Related Projects
* [notify](https://github.com/rjeczalik/notify)
* [fsevents](https://github.com/fsnotify/fsevents)

37
vendor/github.com/fsnotify/fsnotify/fen.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build solaris
package fsnotify
import (
"errors"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
return nil
}
// Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
return nil
}

68
vendor/github.com/fsnotify/fsnotify/fsnotify.go generated vendored Normal file
View File

@@ -0,0 +1,68 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !plan9
// Package fsnotify provides a platform-independent interface for file system notifications.
package fsnotify
import (
"bytes"
"errors"
"fmt"
)
// Event represents a single file system notification.
type Event struct {
Name string // Relative path to the file or directory.
Op Op // File operation that triggered the event.
}
// Op describes a set of file operations.
type Op uint32
// These are the generalized file operations that can trigger a notification.
const (
Create Op = 1 << iota
Write
Remove
Rename
Chmod
)
func (op Op) String() string {
// Use a buffer for efficient string concatenation
var buffer bytes.Buffer
if op&Create == Create {
buffer.WriteString("|CREATE")
}
if op&Remove == Remove {
buffer.WriteString("|REMOVE")
}
if op&Write == Write {
buffer.WriteString("|WRITE")
}
if op&Rename == Rename {
buffer.WriteString("|RENAME")
}
if op&Chmod == Chmod {
buffer.WriteString("|CHMOD")
}
if buffer.Len() == 0 {
return ""
}
return buffer.String()[1:] // Strip leading pipe
}
// String returns a string representation of the event in the form
// "file: REMOVE|WRITE|..."
func (e Event) String() string {
return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
}
// Common errors that can be reported by a watcher
var (
ErrEventOverflow = errors.New("fsnotify queue overflow")
)

5
vendor/github.com/fsnotify/fsnotify/go.mod generated vendored Normal file
View File

@@ -0,0 +1,5 @@
module github.com/fsnotify/fsnotify
go 1.13
require golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9

2
vendor/github.com/fsnotify/fsnotify/go.sum generated vendored Normal file
View File

@@ -0,0 +1,2 @@
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

337
vendor/github.com/fsnotify/fsnotify/inotify.go generated vendored Normal file
View File

@@ -0,0 +1,337 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux
package fsnotify
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
mu sync.Mutex // Map access
fd int
poller *fdPoller
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
// Create inotify fd
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
if fd == -1 {
return nil, errno
}
// Create epoll
poller, err := newFdPoller(fd)
if err != nil {
unix.Close(fd)
return nil, err
}
w := &Watcher{
fd: fd,
poller: poller,
watches: make(map[string]*watch),
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
if w.isClosed() {
return nil
}
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done)
// Wake up goroutine
w.poller.wake()
// Wait for goroutine to close
<-w.doneResp
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
name = filepath.Clean(name)
if w.isClosed() {
return errors.New("inotify instance already closed")
}
const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
var flags uint32 = agnosticEvents
w.mu.Lock()
defer w.mu.Unlock()
watchEntry := w.watches[name]
if watchEntry != nil {
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
if watchEntry == nil {
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = name
} else {
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil
}
// Remove stops watching the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
// Fetch the watch.
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[name]
// Remove it from inotify.
if !ok {
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
}
// We successfully removed the watch if InotifyRmWatch doesn't return an
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case.
// the only two possible errors are:
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
return errno
}
return nil
}
type watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
}
// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Events channel
func (w *Watcher) readEvents() {
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
n int // Number of bytes read with read()
errno error // Syscall errno
ok bool // For poller.wait
)
defer close(w.doneResp)
defer close(w.Errors)
defer close(w.Events)
defer unix.Close(w.fd)
defer w.poller.close()
for {
// See if we have been closed.
if w.isClosed() {
return
}
ok, errno = w.poller.wait()
if errno != nil {
select {
case w.Errors <- errno:
case <-w.done:
return
}
continue
}
if !ok {
continue
}
n, errno = unix.Read(w.fd, buf[:])
// If a signal interrupted execution, see if we've been asked to close, and try again.
// http://man7.org/linux/man-pages/man7/signal.7.html :
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
if errno == unix.EINTR {
continue
}
// unix.Read might have been woken up by Close. If so, we're done.
if w.isClosed() {
return
}
if n < unix.SizeofInotifyEvent {
var err error
if n == 0 {
// If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 {
// If an error occurred while reading.
err = errno
} else {
// Read was too short.
err = errors.New("notify: short read in readEvents()")
}
select {
case w.Errors <- err:
case <-w.done:
return
}
continue
}
var offset uint32
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-unix.SizeofInotifyEvent) {
// Point "raw" to the event in the buffer
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
if mask&unix.IN_Q_OVERFLOW != 0 {
select {
case w.Errors <- ErrEventOverflow:
case <-w.done:
return
}
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock()
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
event := newEvent(name, mask)
// Send the events that are not ignored on the events channel
if !event.ignoreLinux(mask) {
select {
case w.Events <- event:
case <-w.done:
return
}
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + nameLen
}
}
}
// Certain types of events can be "ignored" and not sent over the Events
// channel. Such as events marked ignore by the kernel, or MODIFY events
// against files that do not exist.
func (e *Event) ignoreLinux(mask uint32) bool {
// Ignore anything the inotify API says to ignore
if mask&unix.IN_IGNORED == unix.IN_IGNORED {
return true
}
// If the event is not a DELETE or RENAME, the file must exist.
// Otherwise the event is ignored.
// *Note*: this was put in place because it was seen that a MODIFY
// event was sent after the DELETE. This ignores that MODIFY and
// assumes a DELETE will come or has come if the file doesn't exist.
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
_, statErr := os.Lstat(e.Name)
return os.IsNotExist(statErr)
}
return false
}
// newEvent returns an platform-independent Event based on an inotify mask.
func newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
e.Op |= Create
}
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
e.Op |= Remove
}
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
e.Op |= Write
}
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
e.Op |= Rename
}
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
e.Op |= Chmod
}
return e
}

Some files were not shown because too many files have changed in this diff Show More