Skip to content

Commit ffd793e

Browse files
authored
Merge pull request #3 from OriHoch/master
download schema from cloudcli-server, add support for more commands (via schema)
2 parents f217686 + 37c2563 commit ffd793e

8 files changed

Lines changed: 286 additions & 215 deletions

File tree

CONTRIBUTING.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ Build and run the Docker image which contains the build environment:
2020
```
2121
( docker rm -f cloudwm-cli-build || true ) &&\
2222
docker build --build-arg GOOS=$GOOS --build-arg GOARCH=$GOARCH -t cloudwm-cli-build -f Dockerfile.build . &&\
23-
docker run -d --rm --name cloudwm-cli-build -v `pwd`:/go/src/github.com/cloudwm/cli cloudwm-cli-build tail -f /dev/null
23+
docker run -d --rm --name cloudwm-cli-build -v `pwd`:/go/src/github.com/cloudwm/cli \
24+
-v /etc/cloudcli:/etc/cloudcli \
25+
cloudwm-cli-build tail -f /dev/null
2426
```
2527

2628
Compile and run the CLI:
@@ -29,23 +31,30 @@ Compile and run the CLI:
2931
docker exec -it cloudwm-cli-build go run main.go
3032
```
3133

32-
Build a binary and set executable:
34+
(Optional) Enable alpha commands and set a configuration file:
3335

3436
```
35-
docker exec -it cloudwm-cli-build go build -o cloudcli main.go && sudo chown $USER ./cloudcli && sudo chmod +x ./cloudcli
37+
docker exec -e CLOUDCLI_CONFIG=/etc/cloudcli/.my-config.yaml -e CLOUDCLI_ENABLE_ALPHA=1 -it cloudwm-cli-build go run main.go
3638
```
3739

38-
Run the executable (From Linux):
40+
(Optional) For fast development iterations, define bash aliases:
3941

4042
```
41-
./cloudcli
43+
alias cloudcli="docker exec -it cloudwm-cli-build go run main.go"
44+
alias cloudcli-build="docker exec -it cloudwm-cli-build go build -o cloudcli main.go && sudo chown $USER ./cloudcli && sudo chmod +x ./cloudcli"
4245
```
4346

44-
(Optional) For fast development iterations, define bash aliases:
47+
48+
Build a binary and set executable:
4549

4650
```
47-
alias cloudcli="docker exec -it cloudwm-cli-build go run main.go"
48-
alias cloudcli-build="docker exec -it cloudwm-cli-build go build -o cloudcli main.go && sudo chown $USER ./cloudcli && sudo chmod +x ./cloudcli"
51+
docker exec -it cloudwm-cli-build go build -o cloudcli main.go && sudo chown $USER ./cloudcli && sudo chmod +x ./cloudcli
52+
```
53+
54+
Run the executable (From Linux):
55+
56+
```
57+
./cloudcli
4958
```
5059

5160
## Troubleshooting

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sudo wget -O /usr/local/bin/cloudcli https://github.com/cloudwm/cloudcli/release
1111
sudo chmod +x /usr/local/bin/cloudcli
1212
```
1313

14-
## Usage
14+
## Configure
1515

1616
Set server API host and credentials using one of the following options:
1717

@@ -32,6 +32,15 @@ Set server API host and credentials using one of the following options:
3232
**Important** Please keep your server and API credentials secure,
3333
it's recommended to use a configuration file with appropriate permissions and location.
3434

35+
## Initialize / Update
36+
37+
Update the CLI and authenticate with the API server
38+
39+
```
40+
cloudcli init
41+
```
42+
43+
Re-run the init command to get new features or bug fixes from the server.
3544

3645
## Commands
3746

@@ -43,7 +52,6 @@ See the cloudcli command help messages for full reference.
4352
cloudcli --help
4453
```
4554

46-
4755
### Server commands
4856

4957

@@ -83,3 +91,7 @@ To enable them set environment variable `CLOUDCLI_ENABLE_ALPHA=1`
8391
Create server
8492

8593
**Work In Progress**
94+
95+
96+
### Advanced Features
97+

cmd/command.go

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,26 @@ import (
77
"github.com/spf13/cobra"
88
"gopkg.in/yaml.v2"
99
"io/ioutil"
10+
"net/http"
11+
"net/url"
1012
"os"
1113
"strings"
1214
"text/tabwriter"
1315
)
1416

1517
func commandInit(cmd *cobra.Command, command SchemaCommand) {
1618
for _, flag := range command.Flags {
17-
cmd.Flags().StringP(flag.Name, "", "", flag.Usage)
19+
if flag.Array {
20+
var defaultValue []string
21+
if flag.Default != "" {
22+
defaultValue = append(defaultValue, flag.Default)
23+
}
24+
cmd.Flags().StringArrayP(flag.Name, "", defaultValue, flag.Usage)
25+
} else if flag.Bool {
26+
cmd.Flags().BoolP(flag.Name, "", flag.Default != "", flag.Usage)
27+
} else {
28+
cmd.Flags().StringP(flag.Name, "", flag.Default, flag.Usage)
29+
}
1830
if flag.Required {
1931
cmd.MarkFlagRequired(flag.Name)
2032
}
@@ -35,10 +47,14 @@ func commandRun(cmd *cobra.Command, command SchemaCommand) {
3547
}
3648

3749
func commandRunGetList(cmd *cobra.Command, command SchemaCommand) {
38-
if resp, err := resty.R().
50+
get_url := fmt.Sprintf("%s%s", apiServer, command.Run.Path)
51+
if dryrun {
52+
fmt.Printf("\nGET %s\n", get_url)
53+
os.Exit(exitCodeDryrun)
54+
} else if resp, err := resty.R().
3955
SetHeader("AuthClientId", apiClientid).
4056
SetHeader("AuthSecret", apiSecret).
41-
Get(fmt.Sprintf("%s%s", apiServer, command.Run.Path));
57+
Get(get_url);
4258
err != nil {
4359
fmt.Println(err.Error())
4460
os.Exit(exitCodeUnexpected)
@@ -90,9 +106,127 @@ func commandRunGetList(cmd *cobra.Command, command SchemaCommand) {
90106
}
91107
}
92108

109+
func commandExitErrorResponse(body []byte) {
110+
var errorResponse map[string]interface{}
111+
if err := json.Unmarshal(body, &errorResponse); err != nil {
112+
fmt.Println(string(body))
113+
fmt.Println("Failed to parse server error response")
114+
os.Exit(exitCodeInvalidResponse)
115+
} else {
116+
var message string;
117+
for k, v := range errorResponse {
118+
if k == "message" {
119+
message = v.(string);
120+
}
121+
}
122+
if format == "" && message != "" {
123+
fmt.Println(message)
124+
} else {
125+
var d []byte
126+
var err error
127+
if format == "yaml" {
128+
d, err = yaml.Marshal(&errorResponse)
129+
} else {
130+
d, err = json.Marshal(&errorResponse)
131+
}
132+
if err != nil {
133+
fmt.Println(string(body))
134+
fmt.Println("Invalid response from server")
135+
os.Exit(exitCodeInvalidResponse)
136+
} else {
137+
fmt.Println(string(d))
138+
}
139+
}
140+
os.Exit(exitCodeInvalidStatus)
141+
}
142+
}
143+
93144
func commandRunPost(cmd *cobra.Command, command SchemaCommand) {
94-
fmt.Println("post")
95-
os.Exit(exitCodeUnexpected)
145+
var qs []string
146+
for _, field := range command.Run.Fields {
147+
var value string;
148+
if field.Array {
149+
arrayValue, _ := cmd.Flags().GetStringArray(field.Flag)
150+
value = strings.Join(arrayValue, " ")
151+
} else if field.Bool {
152+
if boolValue, _ := cmd.Flags().GetBool(field.Flag); boolValue {
153+
value = "true"
154+
} else {
155+
value = ""
156+
}
157+
} else {
158+
value, _ = cmd.Flags().GetString(field.Flag)
159+
}
160+
escapedValue := url.PathEscape(value)
161+
if (debug) {
162+
fmt.Printf("\nfield %s=%s / urlpart %s=%s", field.Flag, value, field.Name, escapedValue)
163+
}
164+
qs = append(qs, fmt.Sprintf("%s=%s", field.Name, escapedValue))
165+
}
166+
payload := strings.Join(qs, "&")
167+
post_url := fmt.Sprintf("%s%s", apiServer, command.Run.Path)
168+
if dryrun {
169+
fmt.Printf("\nPOST %s\n", post_url)
170+
fmt.Printf("%s\n\n", payload)
171+
os.Exit(exitCodeDryrun)
172+
} else {
173+
if req, err := http.NewRequest("POST", post_url, strings.NewReader(payload)); err != nil {
174+
fmt.Println("Failed to create POST request")
175+
os.Exit(exitCodeUnexpected)
176+
} else {
177+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
178+
req.Header.Add("AuthClientId", apiClientid)
179+
req.Header.Add("AuthSecret", apiSecret)
180+
if r, err := http.DefaultClient.Do(req); err != nil {
181+
fmt.Println("Failed to send POST request")
182+
os.Exit(exitCodeUnexpected)
183+
} else if body, err := ioutil.ReadAll(r.Body); err != nil {
184+
fmt.Println("Failed to read POST response body")
185+
os.Exit(exitCodeInvalidResponse)
186+
} else if r.StatusCode != 200 {
187+
commandExitErrorResponse(body)
188+
} else {
189+
var commandIds []string;
190+
if err := json.Unmarshal(body, &commandIds); err != nil {
191+
fmt.Println(string(body))
192+
fmt.Println("Failed to parse response")
193+
os.Exit(exitCodeInvalidResponse)
194+
}
195+
if len(commandIds) == 0 {
196+
fmt.Println("Unexpected command failure")
197+
os.Exit(exitCodeUnexpected);
198+
}
199+
if format == "json" || format == "yaml" {
200+
parsedResponse := make(map[string][]string);
201+
parsedResponse["command_ids"] = commandIds;
202+
var d []byte
203+
var err error
204+
if format == "yaml" {
205+
d, err = yaml.Marshal(&parsedResponse)
206+
} else {
207+
d, err = json.Marshal(&parsedResponse)
208+
}
209+
if err != nil {
210+
fmt.Println(string(body))
211+
fmt.Println("Invalid response from server")
212+
os.Exit(exitCodeInvalidResponse)
213+
} else {
214+
fmt.Println(string(d))
215+
os.Exit(0)
216+
}
217+
} else if len(commandIds) == 1 {
218+
fmt.Printf("Command ID: %s\n", commandIds[0])
219+
os.Exit(0)
220+
} else {
221+
fmt.Println("Command IDs:")
222+
for _, commandId := range commandIds {
223+
fmt.Printf("%s\n", commandId)
224+
}
225+
os.Exit(0)
226+
}
227+
}
228+
}
229+
}
96230
}
97231

98232
func commandInitGetListOfLists(cmd *cobra.Command, command SchemaCommand) {

cmd/flags.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ const flagDebugKey = "debug"
3939
var debug bool
4040
var flagDebugValue bool
4141

42+
const flagDryrun = "dryrun"
43+
const flagDryrunKey = "dryrun"
44+
var dryrun bool
45+
var flagDryrunValue bool
46+
4247
func loadGlobalFlags() {
4348
debug = viper.GetBool(flagDebugKey)
4449
if debug {
@@ -74,7 +79,10 @@ func loadGlobalFlags() {
7479
} else if debug {
7580
fmt.Fprintf(os.Stderr, "%s = %s\n", flagFormatKey, format)
7681
}
77-
82+
dryrun = viper.GetBool(flagDryrunKey)
83+
if dryrun {
84+
fmt.Fprintf(os.Stderr, "Running in dry run mode, not performing actions\n")
85+
}
7886
if numFailures > 0 {
7987
os.Exit(1)
8088
}
@@ -99,4 +107,7 @@ func addGlobalFlags() {
99107

100108
rootCmd.PersistentFlags().BoolVar(&flagDebugValue, flagDebug, false, "enable debug output to stderr")
101109
viper.BindPFlag(flagDebugKey, rootCmd.PersistentFlags().Lookup(flagDebug))
110+
111+
rootCmd.PersistentFlags().BoolVar(&flagDryrunValue, flagDryrun, false, "enable dry run mode, does not perform actions")
112+
viper.BindPFlag(flagDryrunKey, rootCmd.PersistentFlags().Lookup(flagDryrun))
102113
}

cmd/output.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ package cmd
33
const exitCodeUnexpected = 1
44
const exitCodeInvalidStatus = 2
55
const exitCodeInvalidResponse = 3
6+
const exitCodeDryrun = 4

cmd/root.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,24 @@ const cloudcliEnvPrefix = "CLOUDCLI"
1212
const cloudcliConfigFilePrefix = ".cloudcli"
1313

1414
var enableAlpha = false
15+
var hasSchema = false
16+
17+
func getRootLongDescription() string {
18+
if hasSchema {
19+
return "Cloudcli server management - create, configure and manage servers"
20+
} else {
21+
return `Cloudcli server management - create, configure and manage servers
22+
23+
Cloudcli is not initialized, please run "cloudcli init" command to authenticate with an API server.
24+
25+
`
26+
}
27+
}
1528

1629
var rootCmd = &cobra.Command{
1730
Use: "cloudcli",
1831
Short: "Cloudcli server management",
19-
Long: `Cloudcli server management - create, configure and manage servers`,
32+
Long: getRootLongDescription(),
2033
PersistentPreRun: func(cmd *cobra.Command, args []string) {
2134
loadGlobalFlags()
2235
},
@@ -35,16 +48,30 @@ func init() {
3548
if os.Getenv("CLOUDCLI_ENABLE_ALPHA") != "" {
3649
enableAlpha = true
3750
}
38-
schema := loadSchema()
39-
for _, command := range schema.Commands {
40-
var cmd = createCommandFromSchema(command)
41-
for _, subcommand := range command.Commands {
42-
if ! subcommand.Alpha || enableAlpha {
43-
var subcmd = createCommandFromSchema(subcommand)
44-
cmd.AddCommand(subcmd)
51+
rootCmd.AddCommand(&cobra.Command{
52+
Use: "init",
53+
Short: "Initialize cloudcli",
54+
Long: "Authenticates to a cloudcli server and updates CLI to latest version",
55+
Run: func(cmd *cobra.Command, args []string) {
56+
schema := downloadSchema(schemaFile, fmt.Sprintf("%s%s", apiServer, "/schema"))
57+
fmt.Printf(
58+
"cloudcli v%d.%d.%d Initialized successfully.\n\nYou can now run cloudcli commands, see:\ncloudcli --help\n\n",
59+
schema.SchemaVersion[0], schema.SchemaVersion[1], schema.SchemaVersion[2],
60+
)
61+
},
62+
})
63+
var schema Schema
64+
if hasSchema, schema = loadSchema(); hasSchema {
65+
for _, command := range schema.Commands {
66+
var cmd = createCommandFromSchema(command)
67+
for _, subcommand := range command.Commands {
68+
if ! subcommand.Alpha || enableAlpha {
69+
var subcmd = createCommandFromSchema(subcommand)
70+
cmd.AddCommand(subcmd)
71+
}
4572
}
73+
rootCmd.AddCommand(cmd)
4674
}
47-
rootCmd.AddCommand(cmd)
4875
}
4976
}
5077

0 commit comments

Comments
 (0)