Skip to content

Commit 9f091a7

Browse files
committed
feat: add raw query and custom command functionality
1 parent 936d760 commit 9f091a7

File tree

5 files changed

+354
-0
lines changed

5 files changed

+354
-0
lines changed

cmd/raw.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,42 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"os"
78

89
"github.com/spf13/cobra"
10+
"github.com/uselagoon/lagoon-cli/pkg/output"
911
lclient "github.com/uselagoon/machinery/api/lagoon/client"
12+
"gopkg.in/yaml.v3"
1013
)
1114

15+
// CustomCommand is the custom command data structure, this is what can be used to define custom commands
16+
type CustomCommand struct {
17+
Name string `yaml:"name"`
18+
Description string `yaml:"description"`
19+
Query string `yaml:"query"`
20+
Flags []struct {
21+
Name string `yaml:"name"`
22+
Description string `yaml:"description"`
23+
Variable string `yaml:"variable"`
24+
Type string `yaml:"type"`
25+
Required bool `yaml:"required"`
26+
Default *interface{} `yaml:"default,omitempty"`
27+
} `yaml:"flags"`
28+
}
29+
30+
var emptyCmd = cobra.Command{
31+
Use: "none",
32+
Aliases: []string{""},
33+
Short: "none",
34+
Hidden: true,
35+
PreRunE: func(_ *cobra.Command, _ []string) error {
36+
return validateTokenE(lagoonCLIConfig.Current)
37+
},
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
return nil
40+
},
41+
}
42+
1243
var rawCmd = &cobra.Command{
1344
Use: "raw",
1445
Aliases: []string{"r"},
@@ -53,6 +84,169 @@ The output of this command will be the JSON response from the API`,
5384
},
5485
}
5586

87+
var customCmd = &cobra.Command{
88+
Use: "custom",
89+
Aliases: []string{"cus", "cust"},
90+
Short: "Run a custom command",
91+
Long: `Run a custom command.
92+
This command alone does nothing, but you can create custom commands and put them into the custom commands directory,
93+
these commands will then be available to use.
94+
The directory for custom commands is ${HOME}/.lagoon-cli/commands.`,
95+
RunE: func(cmd *cobra.Command, args []string) error {
96+
// just return the help menu for this command as if it is just a normal parent with children commands
97+
cmd.Help()
98+
return nil
99+
},
100+
}
101+
102+
func ReadCustomCommands() ([]*cobra.Command, error) {
103+
userPath, err := os.UserHomeDir()
104+
if err != nil {
105+
return nil, fmt.Errorf("couldn't get $HOME: %v", err)
106+
}
107+
customCommandsFilePath := fmt.Sprintf("%s/%s", userPath, commandsFilePath)
108+
if _, err := os.Stat(customCommandsFilePath); os.IsNotExist(err) {
109+
err := os.MkdirAll(customCommandsFilePath, 0700)
110+
if err != nil {
111+
return nil, fmt.Errorf("couldn't create command directory %s: %v", customCommandsFilePath, err)
112+
}
113+
}
114+
files, err := os.ReadDir(customCommandsFilePath)
115+
if err != nil {
116+
return nil, fmt.Errorf("couldn't open command directory %s: %v", customCommandsFilePath, err)
117+
}
118+
var cmds []*cobra.Command
119+
if len(files) != 0 {
120+
for _, file := range files {
121+
if !file.IsDir() {
122+
data, err := os.ReadFile(customCommandsFilePath + "/" + file.Name())
123+
if err != nil {
124+
return nil, err
125+
}
126+
raw := CustomCommand{}
127+
err = yaml.Unmarshal(data, &raw)
128+
if err != nil {
129+
return nil, fmt.Errorf("unable to unmarshal custom command '%s', yaml is likely invalid: %v", file.Name(), err)
130+
}
131+
cCmd := cobra.Command{
132+
Use: raw.Name,
133+
Aliases: []string{""},
134+
Short: raw.Description,
135+
PreRunE: func(_ *cobra.Command, _ []string) error {
136+
return validateTokenE(lagoonCLIConfig.Current)
137+
},
138+
RunE: func(cmd *cobra.Command, args []string) error {
139+
debug, err := cmd.Flags().GetBool("debug")
140+
if err != nil {
141+
return err
142+
}
143+
144+
variables := make(map[string]interface{})
145+
var value interface{}
146+
// handling reading the custom flags
147+
for _, flag := range raw.Flags {
148+
switch flag.Type {
149+
case "Int":
150+
value, err = cmd.Flags().GetInt(flag.Name)
151+
if err != nil {
152+
return err
153+
}
154+
if flag.Required {
155+
if err := requiredInputCheck(flag.Name, fmt.Sprintf("%d", value.(int))); err != nil {
156+
return err
157+
}
158+
}
159+
case "String":
160+
value, err = cmd.Flags().GetString(flag.Name)
161+
if err != nil {
162+
return err
163+
}
164+
if flag.Required {
165+
if err := requiredInputCheck(flag.Name, value.(string)); err != nil {
166+
return err
167+
}
168+
}
169+
case "Boolean":
170+
value, err = cmd.Flags().GetBool(flag.Name)
171+
if err != nil {
172+
return err
173+
}
174+
}
175+
variables[flag.Variable] = value
176+
}
177+
178+
current := lagoonCLIConfig.Current
179+
token := lagoonCLIConfig.Lagoons[current].Token
180+
lc := lclient.New(
181+
lagoonCLIConfig.Lagoons[current].GraphQL,
182+
lagoonCLIVersion,
183+
&token,
184+
debug)
185+
if err != nil {
186+
return err
187+
}
188+
189+
rawResp, err := lc.ProcessRaw(context.TODO(), raw.Query, variables)
190+
if err != nil {
191+
return err
192+
}
193+
r, err := json.Marshal(rawResp)
194+
if err != nil {
195+
return err
196+
}
197+
fmt.Println(string(r))
198+
return nil
199+
},
200+
}
201+
// add custom flags to the command
202+
for _, flag := range raw.Flags {
203+
switch flag.Type {
204+
case "Int":
205+
if flag.Default != nil {
206+
cCmd.Flags().Int(flag.Name, (*flag.Default).(int), flag.Description)
207+
} else {
208+
cCmd.Flags().Int(flag.Name, 0, flag.Description)
209+
}
210+
case "String":
211+
if flag.Default != nil {
212+
cCmd.Flags().String(flag.Name, (*flag.Default).(string), flag.Description)
213+
} else {
214+
cCmd.Flags().String(flag.Name, "", flag.Description)
215+
}
216+
case "Boolean":
217+
if flag.Default != nil {
218+
cCmd.Flags().Bool(flag.Name, (*flag.Default).(bool), flag.Description)
219+
} else {
220+
cCmd.Flags().Bool(flag.Name, false, flag.Description)
221+
}
222+
}
223+
}
224+
cmds = append(cmds, &cCmd)
225+
}
226+
}
227+
} else {
228+
cmds = append(cmds,
229+
// create a hidden command that does nothing so help and docs can be generated for the custom command
230+
&emptyCmd)
231+
}
232+
return cmds, nil
233+
}
234+
56235
func init() {
236+
if _, ok := os.LookupEnv("LAGOON_GEN_DOCS"); ok {
237+
// this is an override for when the docs are generated
238+
// so that it doesn't include any custom commands
239+
customCmd.AddCommand(&emptyCmd)
240+
} else {
241+
// read any custom commands
242+
cmds, err := ReadCustomCommands()
243+
if err != nil {
244+
output.RenderError(err.Error(), outputOptions)
245+
os.Exit(1)
246+
}
247+
for _, c := range cmds {
248+
customCmd.AddCommand(c)
249+
}
250+
}
57251
rawCmd.Flags().String("raw", "", "The raw query or mutation to run")
58252
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e
197197
rootCmd.AddCommand(whoamiCmd)
198198
rootCmd.AddCommand(uploadCmd)
199199
rootCmd.AddCommand(rawCmd)
200+
rootCmd.AddCommand(customCmd)
200201
}
201202

202203
// version/build information command

docs/commands/lagoon.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ lagoon [flags]
3434
* [lagoon add](lagoon_add.md) - Add a project, or add notifications and variables to projects or environments
3535
* [lagoon completion](lagoon_completion.md) - Generate the autocompletion script for the specified shell
3636
* [lagoon config](lagoon_config.md) - Configure Lagoon CLI
37+
* [lagoon custom](lagoon_custom.md) - Run a custom command
3738
* [lagoon delete](lagoon_delete.md) - Delete a project, or delete notifications and variables from projects or environments
3839
* [lagoon deploy](lagoon_deploy.md) - Actions for deploying or promoting branches or environments in lagoon
3940
* [lagoon export](lagoon_export.md) - Export lagoon output to yaml

docs/commands/lagoon_custom.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## lagoon custom
2+
3+
Run a custom command
4+
5+
### Synopsis
6+
7+
Run a custom command.
8+
This command alone does nothing, but you can create custom commands and put them into the custom commands directory,
9+
these commands will then be available to use.
10+
The directory for custom commands is ${HOME}/.lagoon-cli/commands.
11+
12+
```
13+
lagoon custom [flags]
14+
```
15+
16+
### Options
17+
18+
```
19+
-h, --help help for custom
20+
```
21+
22+
### Options inherited from parent commands
23+
24+
```
25+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
26+
--debug Enable debugging output (if supported)
27+
-e, --environment string Specify an environment to use
28+
--force Force yes on prompts (if supported)
29+
-l, --lagoon string The Lagoon instance to interact with
30+
--no-header No header on table (if supported)
31+
--output-csv Output as CSV (if supported)
32+
--output-json Output as JSON (if supported)
33+
--pretty Make JSON pretty (if supported)
34+
-p, --project string Specify a project to use
35+
--skip-update-check Skip checking for updates
36+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
37+
```
38+
39+
### SEE ALSO
40+
41+
* [lagoon](lagoon.md) - Command line integration for Lagoon
42+

docs/customcommands.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Introduction
2+
3+
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.
4+
5+
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.
6+
7+
> **_NOTE:_** as always, be careful with creating your own commands, especially mutations, as you must be 100% aware of the implications.
8+
9+
## Location
10+
11+
Custom commands must be saved to `${HOME}/.lagoon-cli/commands/${COMMAND_NAME}.yml`
12+
13+
## Layout of a command file
14+
15+
An example of the command file structure is as follows
16+
```yaml
17+
name: project-by-name
18+
description: Query a project by name
19+
query: |
20+
query projectByName($name: String!) {
21+
projectByName(name: $name) {
22+
id
23+
name
24+
organization
25+
openshift{
26+
name
27+
}
28+
environments{
29+
name
30+
openshift{
31+
name
32+
}
33+
}
34+
}
35+
}
36+
flags:
37+
- name: name
38+
description: Project name to check
39+
variable: name
40+
type: String
41+
required: true
42+
```
43+
44+
* `name` is the name of the command that the user must enter, this should be unique
45+
* `description` is some helpful information about this command
46+
* `query` is the query or mutation that is run
47+
* `flags` allows you to define your own flags
48+
* `name` is the name of the flag, eg `--name`
49+
* `description` is some helpful information about the flag
50+
* `variable` is the name of the variable that will be passed to the graphql query of the same name
51+
* `type` is the type, currently only `String`, `Int`, `Boolean` are supported
52+
* `required` is if this flag is required or not
53+
* `default` is the default value of the flag if defined
54+
* `String` defaults to ""
55+
* `Int` defaults to 0
56+
* `Boolean` defaults to false.
57+
58+
# Usage
59+
60+
Once a command file has been created, they will appear as `Available Commands` of the top level `custom` command, similarly to below
61+
62+
```
63+
$ lagoon custom
64+
Usage:
65+
lagoon custom [flags]
66+
lagoon custom [command]
67+
68+
Aliases:
69+
custom, cus, cust
70+
71+
Available Commands:
72+
project-by-name Query a project by name
73+
74+
```
75+
76+
You can then call this command like so, and see the output of the command is the API JSON response
77+
```
78+
$ lagoon custom project-by-name --name lagoon-demo-org | jq
79+
{
80+
"projectByName": {
81+
"environments": [
82+
{
83+
"name": "development",
84+
"openshift": {
85+
"name": "ui-kubernetes-2"
86+
}
87+
},
88+
{
89+
"name": "main",
90+
"openshift": {
91+
"name": "ui-kubernetes-2"
92+
}
93+
},
94+
{
95+
"name": "pr-15",
96+
"openshift": {
97+
"name": "ui-kubernetes-2"
98+
}
99+
},
100+
{
101+
"name": "staging",
102+
"openshift": {
103+
"name": "ui-kubernetes-2"
104+
}
105+
}
106+
],
107+
"id": 180,
108+
"name": "lagoon-demo-org",
109+
"openshift": {
110+
"name": "ui-kubernetes-2"
111+
},
112+
"organization": 1
113+
}
114+
}
115+
116+
```

0 commit comments

Comments
 (0)