Skip to content

Commit

Permalink
feat(instance): import file for cloud-init (#1525)
Browse files Browse the repository at this point in the history
* feat(instance): import file for cloud-init
* fix: add io.Reader support for @file

Co-authored-by: Rémy Léone <[email protected]>
  • Loading branch information
jtherin and remyleone authored May 28, 2021
1 parent c62c15f commit cd7e8e8
Show file tree
Hide file tree
Showing 15 changed files with 573 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ARGS:
[security-group-id] The security group ID it use for this server
[placement-group-id] The placement group ID in witch the server has to be created
[bootscript-id] The bootscript ID to use, if empty the local boot will be used
[cloud-init] The cloud-init script to use
[cloud-init] The cloud-init script to use (Support file loading with @/path/to/file)
[boot-type=local] The boot type to use, if empty the local boot will be used. Will be overwritten to bootscript if bootscript-id is set. (local | bootscript | rescue)
[project-id] Project ID to use. If none is passed the default project ID will be used
[zone=fr-par-1] Zone to target. If none is passed will use default zone from the config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ARGS:
server-id UUID of the server
[name] Name of the server
[ip] IP that should be attached to the server (use ip=none to detach)
[cloud-init] The cloud-init script to use
[cloud-init] The cloud-init script to use (Support file loading with @/path/to/file)
[boot-type] (local | bootscript | rescue)
[tags.{index}] Tags of the server
[bootscript]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ USAGE:
ARGS:
server-id UUID of the server
key Key of the user data to set
content Content of the user data
content Content of the user data (Support file loading with @/path/to/file)
[zone=fr-par-1] Zone to target. If none is passed will use default zone from the config (fr-par-1 | fr-par-2 | nl-ams-1 | pl-waw-1)

FLAGS:
Expand Down
56 changes: 56 additions & 0 deletions internal/core/arg_file_content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package core

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"reflect"
"strings"

"github.com/scaleway/scaleway-sdk-go/strcase"
)

// loadArgsFileContent will hydrate args with default values.
func loadArgsFileContent(cmd *Command, cmdArgs interface{}) error {
for _, argSpec := range cmd.ArgSpecs {
if !argSpec.CanLoadFile {
continue
}

fieldName := strcase.ToPublicGoName(argSpec.Name)
fieldValues, err := getValuesForFieldByName(reflect.ValueOf(cmdArgs), strings.Split(fieldName, "."))
if err != nil {
continue
}

for _, v := range fieldValues {
switch i := v.Interface().(type) {
case io.Reader:
b, err := ioutil.ReadAll(i)
if err != nil {
return fmt.Errorf("could not read argument: %s", err)
}

if strings.HasPrefix(string(b), "@") {
content, err := ioutil.ReadFile(string(b)[1:])
if err != nil {
return fmt.Errorf("could not open requested file: %s", err)
}
test := bytes.NewBuffer(content)
v.Set(reflect.ValueOf(test))
}
case *string:
if strings.HasPrefix(*i, "@") {
content, err := ioutil.ReadFile((*i)[1:])
if err != nil {
return fmt.Errorf("could not open requested file: %s", err)
}
v.SetString(string(content))
}
}
}
}

return nil
}
3 changes: 3 additions & 0 deletions internal/core/arg_specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type ArgSpec struct {
// Deprecated is used to flag an argument as deprecated.
// Use the short field to indicate migration tips for users.
Deprecated bool

// CanLoadFile allow to use @ prefix to load a file as content
CanLoadFile bool
}

func (a *ArgSpec) Prefix() string {
Expand Down
6 changes: 5 additions & 1 deletion internal/core/cobra_usage_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ func buildUsageArgs(ctx context.Context, cmd *Command, deprecated bool) string {
func _buildUsageArgs(ctx context.Context, w io.Writer, argSpecs ArgSpecs) error {
for _, argSpec := range argSpecs {
argSpecUsageLeftPart := argSpec.Name
argSpecUsageRightPart := _buildArgShort(argSpec)
if argSpec.Default != nil {
_, doc := argSpec.Default(ctx)
argSpecUsageLeftPart = fmt.Sprintf("%s=%s", argSpecUsageLeftPart, doc)
}
if !argSpec.Required && !argSpec.Positional {
argSpecUsageLeftPart = fmt.Sprintf("[%s]", argSpecUsageLeftPart)
}
if argSpec.CanLoadFile {
argSpecUsageRightPart += " (Support file loading with @/path/to/file)"
}

_, err := fmt.Fprintf(w, " %s\t%s\n", argSpecUsageLeftPart, _buildArgShort(argSpec))
_, err := fmt.Fprintf(w, " %s\t%s\n", argSpecUsageLeftPart, argSpecUsageRightPart)
if err != nil {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions internal/core/cobra_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ func run(ctx context.Context, cobraCmd *cobra.Command, cmd *Command, rawArgs []s
return nil, err
}

// Load args file imports.
err = loadArgsFileContent(cmd, cmdArgs)
if err != nil {
return nil, err
}

// PreValidate hook.
if cmd.PreValidateFunc != nil {
err = cmd.PreValidateFunc(ctx, cmdArgs)
Expand Down
5 changes: 3 additions & 2 deletions internal/namespaces/instance/v1/custom_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,9 @@ func serverUpdateBuilder(c *core.Command) *core.Command {
Short: `IP that should be attached to the server (use ip=none to detach)`,
})
c.ArgSpecs.AddBefore("boot-type", &core.ArgSpec{
Name: "cloud-init",
Short: "The cloud-init script to use",
Name: "cloud-init",
Short: "The cloud-init script to use",
CanLoadFile: true,
})

c.Run = func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
Expand Down
5 changes: 3 additions & 2 deletions internal/namespaces/instance/v1/custom_server_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ func serverCreateCommand() *core.Command {
Short: "The bootscript ID to use, if empty the local boot will be used",
},
{
Name: "cloud-init",
Short: "The cloud-init script to use",
Name: "cloud-init",
Short: "The cloud-init script to use",
CanLoadFile: true,
},
{
Name: "boot-type",
Expand Down
7 changes: 4 additions & 3 deletions internal/namespaces/instance/v1/custom_user_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ func userDataDeleteBuilder(c *core.Command) *core.Command {

func userDataSetBuilder(c *core.Command) *core.Command {
*c.ArgSpecs.GetByName("content.name") = core.ArgSpec{
Name: "content",
Short: "Content of the user data",
Required: true,
Name: "content",
Short: "Content of the user data",
Required: true,
CanLoadFile: true,
}

c.ArgSpecs.DeleteByName("content.content-type")
Expand Down
52 changes: 52 additions & 0 deletions internal/namespaces/instance/v1/custom_user_data_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package instance

import (
"io/ioutil"
"os"
"testing"

"github.com/scaleway/scaleway-cli/internal/core"
Expand Down Expand Up @@ -49,3 +51,53 @@ func Test_UserDataList(t *testing.T) {
),
}))
}

func Test_UserDataFileUpload(t *testing.T) {
content := "cloud-init file content"

t.Run("on-cloud-init", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Server", "scw instance server create stopped=true image=ubuntu-bionic"),
func(ctx *core.BeforeFuncCtx) error {
file, _ := ioutil.TempFile("", "test")
_, _ = file.WriteString(content)
ctx.Meta["filePath"] = file.Name()
return nil
},
),
Cmd: `scw instance user-data set key=cloud-init server-id={{ .Server.ID }} content=@{{ .filePath }}`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
),
AfterFunc: core.AfterFuncCombine(
func(ctx *core.AfterFuncCtx) error {
_ = os.RemoveAll(ctx.Meta["filePath"].(string))
return nil
},
),
}))

t.Run("on-random-key", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Server", "scw instance server create stopped=true image=ubuntu-bionic"),
func(ctx *core.BeforeFuncCtx) error {
file, _ := ioutil.TempFile("", "test")
_, _ = file.WriteString(content)
ctx.Meta["filePath"] = file.Name()
return nil
},
),
Cmd: `scw instance user-data set key=foobar server-id={{ .Server.ID }} content=@{{ .filePath }}`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
),
AfterFunc: core.AfterFuncCombine(
func(ctx *core.AfterFuncCtx) error {
_ = os.RemoveAll(ctx.Meta["filePath"].(string))
return nil
},
),
}))
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
✅ Success.
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
{
"message": "Success",
"details": ""
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
✅ Success.
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
{
"message": "Success",
"details": ""
}

0 comments on commit cd7e8e8

Please sign in to comment.