From fd6d40a90b9356573b085a6a746647552bdeb4c3 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Wed, 3 Jan 2024 14:59:26 +1100 Subject: [PATCH] feat: add raw query and custom command functionality --- Makefile | 4 +- cmd/raw.go | 252 +++++++++++++++++++++++++++++++++ cmd/root.go | 3 + docs/commands/lagoon.md | 2 + docs/commands/lagoon_custom.md | 42 ++++++ docs/commands/lagoon_raw.md | 41 ++++++ docs/customcommands.md | 116 +++++++++++++++ mkdocs.yml | 1 + 8 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 cmd/raw.go create mode 100644 docs/commands/lagoon_custom.md create mode 100644 docs/commands/lagoon_raw.md create mode 100644 docs/customcommands.md diff --git a/Makefile b/Makefile index 11200089..cdc78c0a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/raw.go b/cmd/raw.go new file mode 100644 index 00000000..48548cc3 --- /dev/null +++ b/cmd/raw.go @@ -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") +} diff --git a/cmd/root.go b/cmd/root.go index a98b4109..58ae33d7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 @@ -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 diff --git a/docs/commands/lagoon.md b/docs/commands/lagoon.md index 494a3dcd..efc59bfc 100644 --- a/docs/commands/lagoon.md +++ b/docs/commands/lagoon.md @@ -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 @@ -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 diff --git a/docs/commands/lagoon_custom.md b/docs/commands/lagoon_custom.md new file mode 100644 index 00000000..01507479 --- /dev/null +++ b/docs/commands/lagoon_custom.md @@ -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 + diff --git a/docs/commands/lagoon_raw.md b/docs/commands/lagoon_raw.md new file mode 100644 index 00000000..8b5c7bce --- /dev/null +++ b/docs/commands/lagoon_raw.md @@ -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 + diff --git a/docs/customcommands.md b/docs/customcommands.md new file mode 100644 index 00000000..e881677c --- /dev/null +++ b/docs/customcommands.md @@ -0,0 +1,116 @@ +# Introduction + +Lagoon allows users to create simple custom commands that can execute raw graphql queries or mutations. The response of these commands will be the JSON response from the API, so tools like `jq` can be used to parse the response. + +These commands are meant for simple tasks, and may not perform complex things very well. In some cases, the defaults of a flag may not work as you intend them to. + +> **_NOTE:_** as always, be careful with creating your own commands, especially mutations, as you must be 100% aware of the implications. + +## Location + +Custom commands must be saved to `${HOME}/.lagoon-cli/commands/${COMMAND_NAME}.yml` + +## Layout of a command file + +An example of the command file structure is as follows +```yaml +name: project-by-name +description: Query a project by name +query: | + query projectByName($name: String!) { + projectByName(name: $name) { + id + name + organization + openshift{ + name + } + environments{ + name + openshift{ + name + } + } + } + } +flags: + - name: name + description: Project name to check + variable: name + type: String + required: true +``` + +* `name` is the name of the command that the user must enter, this should be unique +* `description` is some helpful information about this command +* `query` is the query or mutation that is run +* `flags` allows you to define your own flags + * `name` is the name of the flag, eg `--name` + * `description` is some helpful information about the flag + * `variable` is the name of the variable that will be passed to the graphql query of the same name + * `type` is the type, currently only `String`, `Int`, `Boolean` are supported + * `required` is if this flag is required or not + * `default` is the default value of the flag if defined + * `String` defaults to "" + * `Int` defaults to 0 + * `Boolean` defaults to false. + +# Usage + +Once a command file has been created, they will appear as `Available Commands` of the top level `custom` command, similarly to below + +``` +$ lagoon custom +Usage: + lagoon custom [flags] + lagoon custom [command] + +Aliases: + custom, cus, cust + +Available Commands: + project-by-name Query a project by name + +``` + +You can then call this command like so, and see the output of the command is the API JSON response +``` +$ lagoon custom project-by-name --name lagoon-demo-org | jq +{ + "projectByName": { + "environments": [ + { + "name": "development", + "openshift": { + "name": "ui-kubernetes-2" + } + }, + { + "name": "main", + "openshift": { + "name": "ui-kubernetes-2" + } + }, + { + "name": "pr-15", + "openshift": { + "name": "ui-kubernetes-2" + } + }, + { + "name": "staging", + "openshift": { + "name": "ui-kubernetes-2" + } + } + ], + "id": 180, + "name": "lagoon-demo-org", + "openshift": { + "name": "ui-kubernetes-2" + }, + "organization": 1 + } +} + +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index d1831340..fa9e1583 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,3 +47,4 @@ nav: - Getting Started: index.md - Configuration: config.md - Commands: commands/lagoon.md + - Custom Commands: customcommands.md