Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add commands for named connections #51

Merged
merged 8 commits into from
Apr 16, 2024
Merged
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
220 changes: 220 additions & 0 deletions cmd/connections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)

type connectionsFlags struct {
name string
typeConnection []string
excludedType []string
category []string
outputType string
noHeaders bool
}

type FMEFlowConnections struct {
Items []Connection `json:"items"`
TotalCount int `json:"totalCount"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}

type Connection struct {
Name string `json:"name"`
Category string `json:"category"`
Type string `json:"type"`
Owner string `json:"owner"`
Shareable bool `json:"shareable"`
Parameters map[string]interface{} `json:"parameters"`
}

func newConnectionsCmd() *cobra.Command {
f := connectionsFlags{}
cmd := &cobra.Command{
Use: "connections",
Short: "Lists connections on FME Flow",
Long: "Lists connections on FME Flow. Pass in a name to retrieve information on a single connection.",
Example: `
# List all connections
fmeflow connections

# Get a single connection with the name "myConnection"
fmeflow connections --name myConnection

# List all connections of type "Google Drive"
fmeflow connections --type "Google Drive"

# List all database connections
fmeflow connections --category database

# List the PostgreSQL connections with custom columns showing the name and host of the database connections
fmeflow connections --category "database" --type "PostgreSQL" --output=custom-columns="NAME:.name,HOST:.parameters.HOST" `,
Args: NoArgs,
RunE: connectionsRun(&f),
}

cmd.Flags().StringVar(&f.name, "name", "", "Return a single project with the given name.")
cmd.Flags().StringArrayVar(&f.typeConnection, "type", []string{}, "The types of connections to return. Can be passed in multiple times")
cmd.Flags().StringArrayVar(&f.excludedType, "excluded-type", []string{}, "The types of connections to exclude. Can be passed in multiple times")
cmd.Flags().StringArrayVar(&f.category, "category", []string{}, "The categories of connections to return. Can be passed in multiple times")
cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns")
cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers")
cmd.AddCommand(newConnectionCreateCmd())
cmd.AddCommand(newConnectionUpdateCmd())
cmd.AddCommand(newConnectionDeleteCmd())

return cmd
}
func connectionsRun(f *connectionsFlags) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
// --json overrides --output
if jsonOutput {
f.outputType = "json"
}

client := &http.Client{}

url := "/fmeapiv4/connections"
if f.name != "" {
url = url + "/" + f.name
}

request, err := buildFmeFlowRequest(url, "GET", nil)
if err != nil {
return err
}

q := request.URL.Query()

for _, t := range f.typeConnection {
q.Add("types", t)
}

for _, t := range f.excludedType {
q.Add("excludedTypes", t)
}

for _, c := range f.category {
q.Add("categories", c)
}

request.URL.RawQuery = q.Encode()

response, err := client.Do(&request)
if err != nil {
return err
} else if response.StatusCode != http.StatusOK {
responseData, err := io.ReadAll(response.Body)
if err == nil {
var responseMessage Message
if err := json.Unmarshal(responseData, &responseMessage); err == nil {

// if json output is requested, output the JSON to stdout before erroring
if jsonOutput {
prettyJSON, err := prettyPrintJSON(responseData)
if err == nil {
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON)
} else {
return errors.New(response.Status)
}
}
return errors.New(responseMessage.Message)
} else {
return errors.New(response.Status)
}
} else {
return errors.New(response.Status)
}
}

// marshal into struct
var result FMEFlowConnections

responseData, err := io.ReadAll(response.Body)
if err != nil {
return err
}

if f.name == "" {
// if no name specified, request will return the full struct
if err := json.Unmarshal(responseData, &result); err != nil {
return err
}
} else {
var connectionStruct Connection

if err := json.Unmarshal(responseData, &connectionStruct); err != nil {
return err
}
result.TotalCount = 1
result.Items = append(result.Items, connectionStruct)
}

if f.outputType == "table" {

t := table.NewWriter()
t.SetStyle(defaultStyle)

t.AppendHeader(table.Row{"Name", "Type", "Category"})

for _, element := range result.Items {
t.AppendRow(table.Row{element.Name, element.Type, element.Category})
}
if f.noHeaders {
t.ResetHeaders()
}
fmt.Fprintln(cmd.OutOrStdout(), t.Render())

} else if f.outputType == "json" {
prettyJSON, err := prettyPrintJSON(responseData)
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON)
} else if strings.HasPrefix(f.outputType, "custom-columns") {
// parse the columns and json queries
columnsString := ""
if strings.HasPrefix(f.outputType, "custom-columns=") {
columnsString = f.outputType[len("custom-columns="):]
}
if len(columnsString) == 0 {
return errors.New("custom-columns format specified but no custom columns given")
}

// we have to marshal the Items array, then create an array of marshalled items
// to pass to the creation of the table.
marshalledItems := [][]byte{}
for _, element := range result.Items {
mJson, err := json.Marshal(element)
if err != nil {
return err
}
marshalledItems = append(marshalledItems, mJson)
}

columnsInput := strings.Split(columnsString, ",")
t, err := createTableFromCustomColumns(marshalledItems, columnsInput)
if err != nil {
return err
}
if f.noHeaders {
t.ResetHeaders()
}
fmt.Fprintln(cmd.OutOrStdout(), t.Render())

} else {
return errors.New("invalid output format specified")
}

return nil
}
}
156 changes: 156 additions & 0 deletions cmd/connections_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package cmd

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/spf13/cobra"
)

type NewConnection struct {
Category string `json:"category"`
Name string `json:"name"`
Type string `json:"type"`
AuthenticationMethod string `json:"authenticationMethod,omitempty"`
Username string `json:"username"`
Password string `json:"password"`
Parameters map[string]interface{} `json:"parameters,omitempty"`
}

type ConnectionCreateFlags struct {
connectionType string
name string
category string
authenticationMethod string
username string
password string
parameter []string
}

type ConnectionCreateMessage struct {
Message string `json:"message"`
Details map[string]interface{} `json:"details"`
}

func newConnectionCreateCmd() *cobra.Command {
f := ConnectionCreateFlags{}
cmd := &cobra.Command{
Use: "create",
Short: "Create a connection",
Long: `Create a connection.`,
Example: `
# Create a PostgreSQL connection
fmeflow connections create --name myPGSQLConnection --category database --type PostgreSQL --parameter HOST=myDBHost --parameter PORT=5432 --parameter DATASET=dbname --parameter USER_NAME=dbuser --parameter SSL_OPTIONS="" --parameter SSLMODE=prefer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is setting SSL_OPTIONS to an empty string required here? That is, will the endpoint complain if we don't mention it at all?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is required, even if it is an empty string 😄


# Create a Google Drive connection (web service must already exist on FME Flow)
fmeflow connections create --name googleDriveConn --category oauthV2 --type "Google Drive"
`,

Args: NoArgs,
RunE: connectionCreateRun(&f),
}

cmd.Flags().StringVar(&f.name, "name", "", "Name of the connection to create.")
cmd.Flags().StringVar(&f.category, "category", "", "Category of the connection to create. Typically it is one of: \"basic\", \"database\", \"token\", \"oauthV1\", \"oauthV2\".")
cmd.Flags().StringVar(&f.connectionType, "type", "", "Type of connection.")
cmd.Flags().StringVar(&f.authenticationMethod, "authentication-method", "", "Authentication method of the connection to create.")
cmd.Flags().StringVar(&f.username, "username", "", "Username of the connection to create.")
cmd.Flags().StringVar(&f.password, "password", "", "Password of the connection to create.")
cmd.Flags().StringArrayVar(&f.parameter, "parameter", []string{}, "Parameters of the connection to create. Must be of the form name=value. Can be specified multiple times.")

cmd.MarkFlagRequired("name")
cmd.MarkFlagRequired("category")
return cmd
}

func connectionCreateRun(f *ConnectionCreateFlags) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {

// set up http
client := &http.Client{}

var newConnection NewConnection
newConnection.Name = f.name
newConnection.Category = f.category
newConnection.Type = f.connectionType
if f.authenticationMethod != "" {
newConnection.AuthenticationMethod = f.authenticationMethod
}
newConnection.Username = f.username
newConnection.Password = f.password

if len(f.parameter) != 0 {
newConnection.Parameters = make(map[string]interface{})
}
for _, param := range f.parameter {
parts := strings.Split(param, "=")
if len(parts) != 2 {
return errors.New("parameter must be in the format name=value")
}
newConnection.Parameters[parts[0]] = parts[1]
}

jsonData, err := json.Marshal(newConnection)
if err != nil {
return err
}

request, err := buildFmeFlowRequest("/fmeapiv4/connections", "POST", bytes.NewBuffer(jsonData))
if err != nil {
return err
}

q := request.URL.Query()
q.Add("encoded", "false")

request.Header.Add("Content-Type", "application/json")

response, err := client.Do(&request)
if err != nil {
return err
} else if response.StatusCode != http.StatusCreated {
// attempt to parse the body into JSON as there could be a valuable message in there
// if fail, just output the status code
responseData, err := io.ReadAll(response.Body)
if err == nil {
var responseMessage ConnectionCreateMessage
if err := json.Unmarshal(responseData, &responseMessage); err == nil {

// if json output is requested, output the JSON to stdout before erroring
if jsonOutput {
prettyJSON, err := prettyPrintJSON(responseData)
if err == nil {
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON)
} else {
return errors.New(response.Status)
}
} else {
errorMessage := responseMessage.Message
for key, value := range responseMessage.Details {
errorMessage += fmt.Sprintf("\n%s: %v", key, value)
}
return errors.New(errorMessage)
}

} else {
return errors.New(response.Status)
}
} else {
return errors.New(response.Status)
}
} else {
if !jsonOutput {
fmt.Fprintln(cmd.OutOrStdout(), "Connection successfully created.")
} else {
fmt.Fprintln(cmd.OutOrStdout(), "{}")
}
}

return nil
}
}
Loading
Loading