diff --git a/README.md b/README.md index e680cedc80..c5c0ba5029 100644 --- a/README.md +++ b/README.md @@ -1128,7 +1128,7 @@ $ scw inspect myserver | jq '.[0].public_ip.address' #### Features * `scw info` now prints user/organization info from the API ([#142](https://github.com/scaleway/scaleway-cli/issues/130) -* +* Added helpers to manipulate new `user_data` API ([#150](https://github.com/scaleway/scaleway-cli/issues/150)) #### Fixes diff --git a/pkg/api/api.go b/pkg/api/api.go index d1838e306c..816c923920 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -12,6 +12,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "os" @@ -762,6 +763,7 @@ func (s *ScalewayAPI) GetServers(all bool, limit int) (*[]ScalewayServer, error) return nil, err } defer resp.Body.Close() + var servers ScalewayServers decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&servers) @@ -785,6 +787,7 @@ func (s *ScalewayAPI) GetServer(serverID string) (*ScalewayServer, error) { return nil, err } defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) if resp.StatusCode != 200 { @@ -815,6 +818,7 @@ func (s *ScalewayAPI) PostServerAction(serverID, action string) error { if err != nil { return err } + defer resp.Body.Close() // Succeed POST code if resp.StatusCode == 202 { @@ -822,7 +826,6 @@ func (s *ScalewayAPI) PostServerAction(serverID, action string) error { } var error ScalewayAPIError - defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&error) if err != nil { @@ -840,6 +843,7 @@ func (s *ScalewayAPI) DeleteServer(serverID string) error { if err != nil { return err } + defer resp.Body.Close() // Succeed POST code if resp.StatusCode == 204 { @@ -848,7 +852,6 @@ func (s *ScalewayAPI) DeleteServer(serverID string) error { } var error ScalewayAPIError - defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&error) if err != nil { @@ -868,8 +871,8 @@ func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, e if err != nil { return "", err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed POST code @@ -903,8 +906,8 @@ func (s *ScalewayAPI) PatchUserSSHKey(UserID string, definition ScalewayUserPatc if err != nil { return err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed PATCH code @@ -929,8 +932,8 @@ func (s *ScalewayAPI) PatchServer(serverID string, definition ScalewayServerPatc if err != nil { return err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed PATCH code @@ -957,12 +960,11 @@ func (s *ScalewayAPI) PostSnapshot(volumeID string, name string) (string, error) Organization: s.Organization, } resp, err := s.PostResponse("snapshots", definition) - if err != nil { return "", err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed POST code @@ -996,13 +998,13 @@ func (s *ScalewayAPI) PostImage(volumeID string, name string) (string, error) { Organization: s.Organization, Arch: "arm", } - resp, err := s.PostResponse("images", definition) + resp, err := s.PostResponse("images", definition) if err != nil { return "", err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed POST code @@ -1034,12 +1036,13 @@ func (s *ScalewayAPI) PostVolume(definition ScalewayVolumeDefinition) (string, e if definition.Type == "" { definition.Type = "l_ssd" } + resp, err := s.PostResponse("volumes", definition) if err != nil { return "", err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed POST code @@ -1071,8 +1074,8 @@ func (s *ScalewayAPI) PutVolume(volumeID string, definition ScalewayVolumePutDef if err != nil { return err } - defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) // Succeed PUT code @@ -1152,6 +1155,7 @@ func (s *ScalewayAPI) GetImages() (*[]ScalewayImage, error) { return nil, err } defer resp.Body.Close() + var images ScalewayImages decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&images) @@ -1171,6 +1175,7 @@ func (s *ScalewayAPI) GetImage(imageID string) (*ScalewayImage, error) { return nil, err } defer resp.Body.Close() + var oneImage ScalewayOneImage decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&oneImage) @@ -1188,6 +1193,7 @@ func (s *ScalewayAPI) DeleteImage(imageID string) error { s.Cache.RemoveImage(imageID) return err } + defer resp.Body.Close() // Succeed POST code if resp.StatusCode == 204 { @@ -1196,7 +1202,6 @@ func (s *ScalewayAPI) DeleteImage(imageID string) error { } var error ScalewayAPIError - defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&error) if err != nil { @@ -1212,11 +1217,13 @@ func (s *ScalewayAPI) DeleteImage(imageID string) error { func (s *ScalewayAPI) GetSnapshots() (*[]ScalewaySnapshot, error) { query := url.Values{} s.Cache.ClearSnapshots() + resp, err := s.GetResponse("snapshots?" + query.Encode()) if err != nil { return nil, err } defer resp.Body.Close() + var snapshots ScalewaySnapshots decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&snapshots) @@ -1236,6 +1243,7 @@ func (s *ScalewayAPI) GetSnapshot(snapshotID string) (*ScalewaySnapshot, error) return nil, err } defer resp.Body.Close() + var oneSnapshot ScalewayOneSnapshot decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&oneSnapshot) @@ -1250,11 +1258,13 @@ func (s *ScalewayAPI) GetSnapshot(snapshotID string) (*ScalewaySnapshot, error) func (s *ScalewayAPI) GetVolumes() (*[]ScalewayVolume, error) { query := url.Values{} s.Cache.ClearVolumes() + resp, err := s.GetResponse("volumes?" + query.Encode()) if err != nil { return nil, err } defer resp.Body.Close() + var volumes ScalewayVolumes decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&volumes) @@ -1274,6 +1284,7 @@ func (s *ScalewayAPI) GetVolume(volumeID string) (*ScalewayVolume, error) { return nil, err } defer resp.Body.Close() + var oneVolume ScalewayOneVolume decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&oneVolume) @@ -1293,6 +1304,7 @@ func (s *ScalewayAPI) GetBootscripts() (*[]ScalewayBootscript, error) { return nil, err } defer resp.Body.Close() + var bootscripts ScalewayBootscripts decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&bootscripts) @@ -1312,6 +1324,7 @@ func (s *ScalewayAPI) GetBootscript(bootscriptID string) (*ScalewayBootscript, e return nil, err } defer resp.Body.Close() + var oneBootscript ScalewayOneBootscript decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&oneBootscript) @@ -1322,6 +1335,102 @@ func (s *ScalewayAPI) GetBootscript(bootscriptID string) (*ScalewayBootscript, e return &oneBootscript.Bootscript, nil } +type ScalewayUserdatas struct { + UserData []string `json:"user_data"` +} + +// GetUserdatas gets list of userdata for a server +func (s *ScalewayAPI) GetUserdatas(serverID string) (*ScalewayUserdatas, error) { + resp, err := s.GetResponse("servers/" + serverID + "/user_data") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var userdatas ScalewayUserdatas + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&userdatas) + if err != nil { + return nil, err + } + return &userdatas, nil +} + +type ScalewayUserdata []byte + +func (s *ScalewayUserdata) String() string { + return string(*s) +} + +// GetUserdata gets a specific userdata for a server +func (s *ScalewayAPI) GetUserdata(serverID string, key string) (*ScalewayUserdata, error) { + var data ScalewayUserdata + var err error + + resp, err := s.GetResponse("servers/" + serverID + "/user_data/" + key) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("no such user_data %q (%d)", key, resp.StatusCode) + } + + data, err = ioutil.ReadAll(resp.Body) + return &data, err +} + +// PatchUserdata sets a user data +func (s *ScalewayAPI) PatchUserdata(serverID string, key string, value []byte) error { + resource := fmt.Sprintf("servers/%s/user_data/%s", serverID, key) + uri := fmt.Sprintf("%s/%s", strings.TrimRight(s.APIUrl, "/"), resource) + payload := new(bytes.Buffer) + payload.Write(value) + + req, err := http.NewRequest("PATCH", uri, payload) + if err != nil { + return err + } + + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "text/plain") + + curl, err := http2curl.GetCurlCommand(req) + if os.Getenv("SCW_SENSITIVE") != "1" { + log.Debug(s.HideAPICredentials(curl.String())) + } else { + log.Debug(curl.String()) + } + + resp, err := s.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 204 { + return nil + } + + return fmt.Errorf("cannot set user_data (%d)", resp.StatusCode) +} + +// DeleteUserdata deletes a server user_data +func (s *ScalewayAPI) DeleteUserdata(serverID string, key string) error { + resp, err := s.DeleteResponse(fmt.Sprintf("servers/%s/user_data/%s", serverID, key)) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed POST code + if resp.StatusCode == 204 { + return nil + } + return fmt.Errorf("cannot delete user_data (%d)", resp.StatusCode) +} + // GetTasks get the list of tasks from the ScalewayAPI func (s *ScalewayAPI) GetTasks() (*[]ScalewayTask, error) { query := url.Values{} @@ -1330,6 +1439,7 @@ func (s *ScalewayAPI) GetTasks() (*[]ScalewayTask, error) { return nil, err } defer resp.Body.Close() + var tasks ScalewayTasks decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&tasks) @@ -1345,13 +1455,17 @@ func (s *ScalewayAPI) CheckCredentials() error { defer s.DisableAccountAPI() query := url.Values{} query.Set("token_id", s.Token) + resp, err := s.GetResponse("tokens?" + query.Encode()) if err != nil { return err } + defer resp.Body.Close() + if resp.StatusCode != 200 { return fmt.Errorf("[%d] invalid credentials", resp.StatusCode) } + return nil } @@ -1359,14 +1473,16 @@ func (s *ScalewayAPI) CheckCredentials() error { func (s *ScalewayAPI) GetUserID() (string, error) { s.EnableAccountAPI() defer s.DisableAccountAPI() + resp, err := s.GetResponse(fmt.Sprintf("tokens/%s", s.Token)) if err != nil { return "", err } + defer resp.Body.Close() + if resp.StatusCode != 200 { return "", fmt.Errorf("[%d] invalid credentials", resp.StatusCode) } - defer resp.Body.Close() var token ScalewayTokensDefinition decoder := json.NewDecoder(resp.Body) @@ -1381,17 +1497,19 @@ func (s *ScalewayAPI) GetUserID() (string, error) { func (s *ScalewayAPI) GetOrganization() (*ScalewayOrganizationsDefinition, error) { s.EnableAccountAPI() defer s.DisableAccountAPI() + resp, err := s.GetResponse("organizations") if err != nil { return nil, err } + defer resp.Body.Close() + if resp.StatusCode != 200 { return nil, fmt.Errorf("[%d] unable to GET", resp.StatusCode) } var data ScalewayOrganizationsDefinition - defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&data) if err != nil { @@ -1408,14 +1526,16 @@ func (s *ScalewayAPI) GetUser() (*ScalewayUserDefinition, error) { } s.EnableAccountAPI() defer s.DisableAccountAPI() + resp, err := s.GetResponse(fmt.Sprintf("users/%s", userID)) if err != nil { return nil, err } + defer resp.Body.Close() + if resp.StatusCode != 200 { return nil, fmt.Errorf("[%d] no such user", resp.StatusCode) } - defer resp.Body.Close() var user ScalewayUsersDefinition decoder := json.NewDecoder(resp.Body) diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index a979672a4a..44eff4d41c 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -40,6 +40,7 @@ var Commands = []*Command{ cmdStop, cmdTag, cmdTop, + cmdUserdata, cmdVersion, cmdWait, } diff --git a/pkg/cli/test.go b/pkg/cli/test.go index f94cbf8a38..23ad437b0a 100644 --- a/pkg/cli/test.go +++ b/pkg/cli/test.go @@ -17,7 +17,7 @@ var ( "version", "wait", } secretCommands []string = []string{ - "_patch", "_completion", "_flush-cache", + "_patch", "_completion", "_flush-cache", "_userdata", } publicOptions []string = []string{ "-h, --help=false", diff --git a/pkg/cli/x_userdata.go b/pkg/cli/x_userdata.go new file mode 100644 index 0000000000..8abaa7dcf5 --- /dev/null +++ b/pkg/cli/x_userdata.go @@ -0,0 +1,85 @@ +// Copyright (C) 2015 Scaleway. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE.md file. + +package cli + +import ( + "fmt" + "strings" +) + +var cmdUserdata = &Command{ + Exec: runUserdata, + UsageLine: "_userdata [OPTIONS] SERVER [FIELD[=VALUE]]", + Description: "", + Hidden: true, + Help: "List, read and write and delete server's userdata", + Examples: ` + $ scw _userdata myserver + $ scw _userdata myserver key + $ scw _userdata myserver key=value + $ scw _userdata myserver key="" +`, +} + +func init() { + cmdUserdata.Flag.BoolVar(&userdataHelp, []string{"h", "-help"}, false, "Print usage") +} + +// Flags +var userdataHelp bool // -h, --help flag + +func runUserdata(cmd *Command, args []string) error { + if userdataHelp { + return cmd.PrintUsage() + } + if len(args) < 1 { + return cmd.PrintShortUsage() + } + + ctx := cmd.GetContext(args) + serverID := ctx.API.GetServerID(args[0]) + + switch len(args) { + case 1: + // List userdata + res, err := ctx.API.GetUserdatas(serverID) + if err != nil { + return err + } + for _, key := range res.UserData { + fmt.Fprintln(ctx.Stdout, key) + } + default: + parts := strings.Split(args[1], "=") + key := parts[0] + switch len(parts) { + case 1: + // Get userdatas + res, err := ctx.API.GetUserdata(serverID, key) + if err != nil { + return err + } + fmt.Fprintf(ctx.Stdout, "%s\n", res.String()) + default: + value := parts[1] + if value != "" { + // Set userdata + err := ctx.API.PatchUserdata(serverID, key, []byte(value)) + if err != nil { + return err + } + fmt.Fprintln(ctx.Stdout, key) + } else { + // Delete userdata + err := ctx.API.DeleteUserdata(serverID, key) + if err != nil { + return err + } + } + } + } + + return nil +}