Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions cmd/get_installation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package cmd

import (
"errors"
"os"
"sort"

"github.com/amp-labs/cli/clerk"
"github.com/amp-labs/cli/flags"
"github.com/amp-labs/cli/logger"
"github.com/amp-labs/cli/request"
"github.com/amp-labs/cli/utils"
"github.com/spf13/cobra"
)

type installationActions struct {
Read []string `json:"read"`
Write []string `json:"write"`
Subscribe []string `json:"subscribe"`
Proxy bool `json:"proxy"`
}

type installationDetail struct {
Id string `json:"id"`
IntegrationId string `json:"integrationId"`
GroupRef string `json:"groupRef"`
ConnectionId string `json:"connectionId"`
HealthStatus string `json:"healthStatus"`
RevisionId string `json:"revisionId"`
Provider string `json:"provider"`
Actions installationActions `json:"actions"`
}

func summarizeInstallation(installation *request.Installation) installationDetail {
detail := installationDetail{
Id: installation.Id,
IntegrationId: installation.IntegrationId,
GroupRef: installation.GroupRef,
ConnectionId: installation.ConnectionId,
HealthStatus: installation.HealthStatus,
Actions: installationActions{
Read: []string{},
Write: []string{},
Subscribe: []string{},
},
}

if installation.Group != nil {
detail.GroupRef = installation.Group.GroupRef
}

if installation.Connection != nil {
detail.ConnectionId = installation.Connection.Id
}

if installation.Config == nil {
return detail
}

detail.RevisionId = installation.Config.RevisionId

content, ok := installation.Config.Content.(map[string]any)
if !ok {
return detail
}

detail.Provider, _ = content["provider"].(string)
detail.Actions.Read = configuredObjects(content, "read")
detail.Actions.Write = configuredObjects(content, "write")
detail.Actions.Subscribe = configuredObjects(content, "subscribe")
detail.Actions.Proxy = proxyEnabled(content)

return detail
}

func configuredObjects(content map[string]any, action string) []string {
actionConfig, ok := content[action].(map[string]any)
if !ok {
return []string{}
}

objects, ok := actionConfig["objects"].(map[string]any)
if !ok {
return []string{}
}

names := make([]string, 0, len(objects))
for name := range objects {
names = append(names, name)
}

sort.Strings(names)

return names
}

func proxyEnabled(content map[string]any) bool {
proxy, ok := content["proxy"].(map[string]any)
if !ok {
return false
}

enabled, _ := proxy["enabled"].(bool)

return enabled
}

var getInstallationCmd = &cobra.Command{ //nolint:gochecknoglobals
Use: "get:installation <integrationId> <installationId>",
Short: "Show an installation",
Long: "Show an installation's provider, health, and configured actions without connection credentials.",
Args: cobra.ExactArgs(2), //nolint:mnd
Run: func(cmd *cobra.Command, args []string) {
projectId := flags.GetProjectOrFail()
apiKey := flags.GetAPIKey()
client := request.NewAPIClient(projectId, &apiKey)

installation, err := client.GetInstallation(cmd.Context(), args[0], args[1])
if err != nil {
if errors.Is(err, clerk.ErrNoSessions) {
logger.FatalErr("Authenticated session has expired, please log in using amp login", err)
} else {
logger.FatalErr("Unable to get installation", err)
}
}

err = utils.WriteStruct(os.Stdout, flags.GetOutputFormatForCommand(cmd), summarizeInstallation(installation))
if err != nil {
logger.FatalErr("Unable to write installation", err)
}
},
}

func init() {
err := flags.InitAndBindFormatFlag(getInstallationCmd)
if err != nil {
logger.FatalErr("unable to initialize flags", err)
}

rootCmd.AddCommand(getInstallationCmd)
}
80 changes: 80 additions & 0 deletions cmd/get_installation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cmd

import (
"reflect"
"testing"

"github.com/amp-labs/cli/request"
)

func TestSummarizeInstallationShowsEnabledActions(t *testing.T) {
t.Parallel()

installation := &request.Installation{
Id: "installation-id",
IntegrationId: "integration-id",
HealthStatus: "healthy",
Group: &request.Group{GroupRef: "group-ref"},
Connection: &request.Connection{Id: "connection-id"},
Config: &request.Config{
RevisionId: "revision-id",
Content: map[string]any{
"provider": "hubspot",
"read": map[string]any{
"objects": map[string]any{"contacts": map[string]any{}, "companies": map[string]any{}},
},
"write": map[string]any{
"objects": map[string]any{"contacts": map[string]any{}},
},
"subscribe": map[string]any{
"objects": map[string]any{"contacts": map[string]any{}},
},
"proxy": map[string]any{"enabled": true},
},
},
}

got := summarizeInstallation(installation)
want := installationDetail{
Id: "installation-id",
IntegrationId: "integration-id",
GroupRef: "group-ref",
ConnectionId: "connection-id",
HealthStatus: "healthy",
RevisionId: "revision-id",
Provider: "hubspot",
Actions: installationActions{
Read: []string{"companies", "contacts"},
Write: []string{"contacts"},
Subscribe: []string{"contacts"},
Proxy: true,
},
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("summarizeInstallation() = %#v, want %#v", got, want)
}
}

func TestSummarizeInstallationDoesNotExposeConnectionCredentials(t *testing.T) {
t.Parallel()

installation := &request.Installation{
Connection: &request.Connection{
Id: "connection-id",
ProviderApp: &request.ProviderApp{
ClientId: "client-id",
ClientSecret: "client-secret",
},
},
}

got := summarizeInstallation(installation)
if got.ConnectionId != "connection-id" {
t.Fatalf("connection ID = %q, want connection-id", got.ConnectionId)
}

if reflect.ValueOf(got).FieldByName("Connection").IsValid() {
t.Fatal("installation detail includes the connection object")
}
}
70 changes: 70 additions & 0 deletions cmd/update_installation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"errors"
"os"

"github.com/amp-labs/cli/clerk"
"github.com/amp-labs/cli/flags"
"github.com/amp-labs/cli/logger"
"github.com/amp-labs/cli/request"
"github.com/amp-labs/cli/utils"
"github.com/spf13/cobra"
)

var updateInstallationInput string //nolint:gochecknoglobals

var updateInstallationCmd = &cobra.Command{ //nolint:gochecknoglobals
Use: "update:installation <integrationId> <installationId> --input <path>",
Short: "Update an installation",
Long: "Update explicit installation fields from a JSON or YAML patch containing installation and updateMask.",
Args: cobra.ExactArgs(2), //nolint:mnd
Run: func(cmd *cobra.Command, args []string) {
var patch request.PatchInstallation

_, err := utils.ReadStructFromFile(updateInstallationInput, &patch)
if err != nil {
logger.FatalErr("Unable to read installation patch", err)
}

if len(patch.Installation) == 0 || len(patch.UpdateMask) == 0 {
logger.Fatal("Installation patch must contain installation and updateMask")
}

projectId := flags.GetProjectOrFail()
apiKey := flags.GetAPIKey()
client := request.NewAPIClient(projectId, &apiKey)

installation, err := client.PatchInstallation(cmd.Context(), args[0], args[1], &patch)
if err != nil {
if errors.Is(err, clerk.ErrNoSessions) {
logger.FatalErr("Authenticated session has expired, please log in using amp login", err)
} else {
logger.FatalErr("Unable to update installation", err)
}
}

err = utils.WriteStruct(os.Stdout, flags.GetOutputFormatForCommand(cmd), summarizeInstallation(installation))
if err != nil {
logger.FatalErr("Unable to write installation", err)
}
},
}

func init() {
updateInstallationCmd.Flags().StringVarP(
&updateInstallationInput, "input", "i", "", "Path to a JSON or YAML installation patch, or - for stdin",
)

err := updateInstallationCmd.MarkFlagRequired("input")
if err != nil {
logger.FatalErr("unable to require input flag", err)
}

err = flags.InitAndBindFormatFlag(updateInstallationCmd)
if err != nil {
logger.FatalErr("unable to initialize flags", err)
}

rootCmd.AddCommand(updateInstallationCmd)
}
18 changes: 17 additions & 1 deletion flags/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,23 @@ func InitAndBindFormatFlag(cmd *cobra.Command) error {
}

func GetOutputFormat() utils.Format {
switch strings.ToLower(viper.GetString("format")) {
return parseOutputFormat(viper.GetString("format"))
}

// GetOutputFormatForCommand reads the format flag from the command that owns it.
// Format flags are local to each command, so this avoids another command's Viper
// binding changing the selected output format.
func GetOutputFormatForCommand(cmd *cobra.Command) utils.Format {
format, err := cmd.Flags().GetString("format")
if err != nil {
return utils.Unknown
}

return parseOutputFormat(format)
}

func parseOutputFormat(format string) utils.Format {
switch strings.ToLower(format) {
case "json":
return utils.JSON
case "yaml", "yml":
Expand Down
31 changes: 31 additions & 0 deletions flags/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package flags

import (
"testing"

"github.com/amp-labs/cli/utils"
"github.com/spf13/cobra"
)

func TestGetOutputFormatForCommandUsesOwningFlag(t *testing.T) {
t.Parallel()

jsonCommand := &cobra.Command{Use: "json"}
jsonCommand.Flags().String("format", "json", "")

yamlCommand := &cobra.Command{Use: "yaml"}
yamlCommand.Flags().String("format", "json", "")

err := yamlCommand.Flags().Set("format", "yaml")
if err != nil {
t.Fatalf("set format: %v", err)
}

if got := GetOutputFormatForCommand(jsonCommand); got != utils.JSON {
t.Fatalf("JSON command format = %q, want json", got)
}

if got := GetOutputFormatForCommand(yamlCommand); got != utils.YAML {
t.Fatalf("YAML command format = %q, want yaml", got)
}
}
Loading
Loading