Skip to content

Commit

Permalink
feat: add raw query and custom command functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon committed Jan 3, 2024
1 parent 91f21aa commit fd6d40a
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ build-linux: test
build-darwin: test
GO111MODULE=on CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOCMD) build -ldflags '${LDFLAGS} -X "${PKG}/cmd.lagoonCLIBuildGoVersion=${GO_VER}"' -o builds/lagoon-cli-${VERSION}-darwin-amd64 -v

docs: test build
GO111MODULE=on $(GOCMD) run main.go --docs
docs: test
LAGOON_GEN_DOCS=true GO111MODULE=on $(GOCMD) run main.go --docs

tidy:
GO111MODULE=on $(GOCMD) mod tidy
Expand Down
252 changes: 252 additions & 0 deletions cmd/raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/uselagoon/lagoon-cli/pkg/output"
lclient "github.com/uselagoon/machinery/api/lagoon/client"
"gopkg.in/yaml.v3"
)

// CustomCommand is the custom command data structure, this is what can be used to define custom commands
type CustomCommand struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Query string `yaml:"query"`
Flags []struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Variable string `yaml:"variable"`
Type string `yaml:"type"`
Required bool `yaml:"required"`
Default *interface{} `yaml:"default,omitempty"`
} `yaml:"flags"`
}

var emptyCmd = cobra.Command{
Use: "none",
Aliases: []string{""},
Short: "none",
Hidden: true,
PreRunE: func(_ *cobra.Command, _ []string) error {
return validateTokenE(lagoonCLIConfig.Current)
},
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}

var rawCmd = &cobra.Command{
Use: "raw",
Aliases: []string{"r"},
Short: "Run a custom query or mutation",
Long: `Run a custom query or mutation.
The output of this command will be the JSON response from the API`,
PreRunE: func(_ *cobra.Command, _ []string) error {
return validateTokenE(cmdLagoon)
},
RunE: func(cmd *cobra.Command, args []string) error {
debug, err := cmd.Flags().GetBool("debug")
if err != nil {
return err
}
raw, err := cmd.Flags().GetString("raw")
if err != nil {
return err
}
if err := requiredInputCheck("Raw query or mutation", raw); err != nil {
return err
}
current := lagoonCLIConfig.Current
token := lagoonCLIConfig.Lagoons[current].Token
lc := lclient.New(
lagoonCLIConfig.Lagoons[current].GraphQL,
lagoonCLIVersion,
&token,
debug)
if err != nil {
return err
}
rawResp, err := lc.ProcessRaw(context.TODO(), raw, nil)
if err != nil {
return err
}
r, err := json.Marshal(rawResp)
if err != nil {
return err
}
fmt.Println(string(r))
return nil
},
}

var customCmd = &cobra.Command{
Use: "custom",
Aliases: []string{"cus", "cust"},
Short: "Run a custom command",
Long: `Run a custom command.
This command alone does nothing, but you can create custom commands and put them into the custom commands directory,
these commands will then be available to use.
The directory for custom commands is ${HOME}/.lagoon-cli/commands.`,
RunE: func(cmd *cobra.Command, args []string) error {
// just return the help menu for this command as if it is just a normal parent with children commands
cmd.Help()
return nil
},
}

func ReadCustomCommands() ([]*cobra.Command, error) {
userPath, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("couldn't get $HOME: %v", err)
}
customCommandsFilePath := fmt.Sprintf("%s/%s", userPath, commandsFilePath)
if _, err := os.Stat(customCommandsFilePath); os.IsNotExist(err) {
err := os.MkdirAll(customCommandsFilePath, 0700)
if err != nil {
return nil, fmt.Errorf("couldn't create command directory %s: %v", customCommandsFilePath, err)
}
}
files, err := os.ReadDir(customCommandsFilePath)
if err != nil {
return nil, fmt.Errorf("couldn't open command directory %s: %v", customCommandsFilePath, err)
}
var cmds []*cobra.Command
if len(files) != 0 {
for _, file := range files {
if !file.IsDir() {
data, err := os.ReadFile(customCommandsFilePath + "/" + file.Name())
if err != nil {
return nil, err
}
raw := CustomCommand{}
err = yaml.Unmarshal(data, &raw)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal custom command '%s', yaml is likely invalid: %v", file.Name(), err)
}
cCmd := cobra.Command{
Use: raw.Name,
Aliases: []string{""},
Short: raw.Description,
PreRunE: func(_ *cobra.Command, _ []string) error {
return validateTokenE(lagoonCLIConfig.Current)
},
RunE: func(cmd *cobra.Command, args []string) error {
debug, err := cmd.Flags().GetBool("debug")
if err != nil {
return err
}

variables := make(map[string]interface{})
var value interface{}
// handling reading the custom flags
for _, flag := range raw.Flags {
switch flag.Type {
case "Int":
value, err = cmd.Flags().GetInt(flag.Name)
if err != nil {
return err
}
if flag.Required {
if err := requiredInputCheck(flag.Name, fmt.Sprintf("%d", value.(int))); err != nil {
return err
}
}
case "String":
value, err = cmd.Flags().GetString(flag.Name)
if err != nil {
return err
}
if flag.Required {
if err := requiredInputCheck(flag.Name, value.(string)); err != nil {
return err
}
}
case "Boolean":
value, err = cmd.Flags().GetBool(flag.Name)
if err != nil {
return err
}
}
variables[flag.Variable] = value
}

current := lagoonCLIConfig.Current
token := lagoonCLIConfig.Lagoons[current].Token
lc := lclient.New(
lagoonCLIConfig.Lagoons[current].GraphQL,
lagoonCLIVersion,
&token,
debug)
if err != nil {
return err
}

rawResp, err := lc.ProcessRaw(context.TODO(), raw.Query, variables)
if err != nil {
return err
}
r, err := json.Marshal(rawResp)
if err != nil {
return err
}
fmt.Println(string(r))
return nil
},
}
// add custom flags to the command
for _, flag := range raw.Flags {
switch flag.Type {
case "Int":
if flag.Default != nil {
cCmd.Flags().Int(flag.Name, (*flag.Default).(int), flag.Description)
} else {
cCmd.Flags().Int(flag.Name, 0, flag.Description)
}
case "String":
if flag.Default != nil {
cCmd.Flags().String(flag.Name, (*flag.Default).(string), flag.Description)
} else {
cCmd.Flags().String(flag.Name, "", flag.Description)
}
case "Boolean":
if flag.Default != nil {
cCmd.Flags().Bool(flag.Name, (*flag.Default).(bool), flag.Description)
} else {
cCmd.Flags().Bool(flag.Name, false, flag.Description)
}
}
}
cmds = append(cmds, &cCmd)
}
}
} else {
cmds = append(cmds,
// create a hidden command that does nothing so help and docs can be generated for the custom command
&emptyCmd)
}
return cmds, nil
}

func init() {
if _, ok := os.LookupEnv("LAGOON_GEN_DOCS"); ok {
// this is an override for when the docs are generated
// so that it doesn't include any custom commands
customCmd.AddCommand(&emptyCmd)
} else {
// read any custom commands
cmds, err := ReadCustomCommands()
if err != nil {
output.RenderError(err.Error(), outputOptions)
os.Exit(1)
}
for _, c := range cmds {
customCmd.AddCommand(c)
}
}
rawCmd.Flags().String("raw", "", "The raw query or mutation to run")
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var configExtension = ".yml"
var createConfig bool
var userPath string
var configFilePath string
var commandsFilePath = ".lagoon-cli/commands"
var updateDocURL = "https://uselagoon.github.io/lagoon-cli"

var skipUpdateCheck bool
Expand Down Expand Up @@ -195,6 +196,8 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e
rootCmd.AddCommand(exportCmd)
rootCmd.AddCommand(whoamiCmd)
rootCmd.AddCommand(uploadCmd)
rootCmd.AddCommand(rawCmd)
rootCmd.AddCommand(customCmd)
}

// version/build information command
Expand Down
2 changes: 2 additions & 0 deletions docs/commands/lagoon.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ lagoon [flags]
* [lagoon add](lagoon_add.md) - Add a project, or add notifications and variables to projects or environments
* [lagoon completion](lagoon_completion.md) - Generate the autocompletion script for the specified shell
* [lagoon config](lagoon_config.md) - Configure Lagoon CLI
* [lagoon custom](lagoon_custom.md) - Run a custom command
* [lagoon delete](lagoon_delete.md) - Delete a project, or delete notifications and variables from projects or environments
* [lagoon deploy](lagoon_deploy.md) - Actions for deploying or promoting branches or environments in lagoon
* [lagoon export](lagoon_export.md) - Export lagoon output to yaml
Expand All @@ -42,6 +43,7 @@ lagoon [flags]
* [lagoon kibana](lagoon_kibana.md) - Launch the kibana interface
* [lagoon list](lagoon_list.md) - List projects, environments, deployments, variables or notifications
* [lagoon login](lagoon_login.md) - Log into a Lagoon instance
* [lagoon raw](lagoon_raw.md) - Run a custom query or mutation
* [lagoon retrieve](lagoon_retrieve.md) - Trigger a retrieval operation on backups
* [lagoon run](lagoon_run.md) - Run a task against an environment
* [lagoon ssh](lagoon_ssh.md) - Display the SSH command to access a specific environment in a project
Expand Down
42 changes: 42 additions & 0 deletions docs/commands/lagoon_custom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## lagoon custom

Run a custom command

### Synopsis

Run a custom command.
This command alone does nothing, but you can create custom commands and put them into the custom commands directory,
these commands will then be available to use.
The directory for custom commands is ${HOME}/.lagoon-cli/commands.

```
lagoon custom [flags]
```

### Options

```
-h, --help help for custom
```

### Options inherited from parent commands

```
--config-file string Path to the config file to use (must be *.yml or *.yaml)
--debug Enable debugging output (if supported)
-e, --environment string Specify an environment to use
--force Force yes on prompts (if supported)
-l, --lagoon string The Lagoon instance to interact with
--no-header No header on table (if supported)
--output-csv Output as CSV (if supported)
--output-json Output as JSON (if supported)
--pretty Make JSON pretty (if supported)
-p, --project string Specify a project to use
--skip-update-check Skip checking for updates
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
```

### SEE ALSO

* [lagoon](lagoon.md) - Command line integration for Lagoon

41 changes: 41 additions & 0 deletions docs/commands/lagoon_raw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## lagoon raw

Run a custom query or mutation

### Synopsis

Run a custom query or mutation.
The output of this command will be the JSON response from the API

```
lagoon raw [flags]
```

### Options

```
-h, --help help for raw
--raw string The raw query or mutation to run
```

### Options inherited from parent commands

```
--config-file string Path to the config file to use (must be *.yml or *.yaml)
--debug Enable debugging output (if supported)
-e, --environment string Specify an environment to use
--force Force yes on prompts (if supported)
-l, --lagoon string The Lagoon instance to interact with
--no-header No header on table (if supported)
--output-csv Output as CSV (if supported)
--output-json Output as JSON (if supported)
--pretty Make JSON pretty (if supported)
-p, --project string Specify a project to use
--skip-update-check Skip checking for updates
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
```

### SEE ALSO

* [lagoon](lagoon.md) - Command line integration for Lagoon

Loading

0 comments on commit fd6d40a

Please sign in to comment.