-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5884471
Add connections command to get connections.
garnold54 fe81422
Add connections command to main command. Allow missing jsonpath values.
garnold54 24cddde
Add connection Create/Update/Delete
garnold54 68cfea4
Update docs for connections.
garnold54 f3aa343
Merge branch 'main' of github.com:safesoftware/fmeflow-cli into add-c…
garnold54 6fc9124
Refresh docs.
garnold54 eac91ff
Fix up examples for connections. Clean example text. Regen doc.
garnold54 84f68ab
Update docs/fmeflow.md
garnold54 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
# 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 | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 😄