Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5044102
feat: add editor
Codelax Mar 16, 2023
98abe5b
refactor and add custom marshaler
Codelax Mar 17, 2023
c9d353a
edit temporary file instead of using stdin and split arguments from e…
Codelax Mar 17, 2023
385c431
improve text editors support and add file extension to temporary file
Codelax Mar 17, 2023
ad9aee2
refactor
Codelax Mar 17, 2023
c6a973a
disable broken test
Codelax Mar 17, 2023
fddba65
lint
Codelax Mar 17, 2023
51e7aae
change yaml marshal to yamljson
Codelax Mar 17, 2023
ff7c3f5
handle slices and refactor valueMapper
Codelax Mar 20, 2023
af91d33
add putRequest bool and add TODO
Codelax Mar 20, 2023
a5e66fa
better support of pointers
Codelax Mar 20, 2023
b296cd7
delete interceptor and only expect resource to be given to the editor
Codelax Mar 21, 2023
6477ff8
always delete temporary file
Codelax Mar 21, 2023
d8eab74
fix linter
Codelax Mar 21, 2023
2b2bea7
take editor arguments as struct
Codelax Mar 21, 2023
8da487b
add back path parameter to edited request
Codelax Mar 21, 2023
f453206
fix tests
Codelax Mar 22, 2023
1e6774b
fix todo
Codelax Mar 22, 2023
d36b1c2
add security-group edit command
Codelax Mar 22, 2023
2bd0579
add marshal-mode argument
Codelax Mar 22, 2023
065644e
update goldens
Codelax Mar 22, 2023
36fa633
update doc
Codelax Mar 22, 2023
003dace
linter
Codelax Mar 22, 2023
76202e5
add default editor for windows
Codelax Mar 24, 2023
eeab091
add examples when editing
Codelax Mar 24, 2023
dda30de
add rules in example
Codelax Mar 24, 2023
22fee65
change reflect valueMapper arguments to options
Codelax Mar 24, 2023
c8e0dfa
add ignoredFields
Codelax Mar 24, 2023
9259502
improve security-group edit output
Codelax Mar 24, 2023
71525d9
fix editor doc with correct system default editor
Codelax Mar 24, 2023
ece5527
fix linter
Codelax Mar 24, 2023
d192c94
update golden
Codelax Mar 24, 2023
b94fdee
gen doc
Codelax Mar 24, 2023
4d6f4d7
fix linter
Codelax Mar 24, 2023
b31c061
Merge branch 'master' into feat/editor
remyleone Mar 24, 2023
246f825
fix edit test on windows
Codelax Mar 24, 2023
041846b
revert changes on wrong test
Codelax Mar 24, 2023
93f28af
change edit help
Codelax Mar 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
This command starts your default editor to edit a marshaled version of your resource
Default editor will be taken from $VISUAL, then $EDITOR or an editor based on your system

USAGE:
scw instance security-group edit <security-group-id ...> [arg=value ...]

ARGS:
security-group-id ID of the security group to reset.
[mode=yaml] marshaling used when editing data (yaml | json)
[zone=fr-par-1] Zone to target. If none is passed will use default zone from the config

FLAGS:
-h, --help help for edit

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ AVAILABLE COMMANDS:
create-rule Create rule
delete Delete a security group
delete-rule Delete rule
edit Edit all rules of a security group
get Get a security group
get-rule Get rule
list List security groups
Expand Down
23 changes: 23 additions & 0 deletions docs/commands/instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Instance API.
- [Create rule](#create-rule)
- [Delete a security group](#delete-a-security-group)
- [Delete rule](#delete-rule)
- [Edit all rules of a security group](#edit-all-rules-of-a-security-group)
- [Get a security group](#get-a-security-group)
- [Get rule](#get-rule)
- [List security groups](#list-security-groups)
Expand Down Expand Up @@ -1292,6 +1293,28 @@ scw instance security-group delete-rule security-group-id=a01a36e5-5c0c-42c1-ae0



### Edit all rules of a security group

This command starts your default editor to edit a marshaled version of your resource
Default editor will be taken from $VISUAL, then $EDITOR or an editor based on your system

**Usage:**

```
scw instance security-group edit <security-group-id ...> [arg=value ...]
```


**Args:**

| Name | | Description |
|------|---|-------------|
| security-group-id | Required | ID of the security group to reset. |
| mode | Default: `yaml`<br />One of: `yaml`, `json` | marshaling used when editing data |
| zone | Default: `fr-par-1` | Zone to target. If none is passed will use default zone from the config |



### Get a security group

Get the details of a Security Group with the given ID.
Expand Down
37 changes: 37 additions & 0 deletions internal/config/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package config

import (
"os"
"runtime"
)

var (
// List of env variables where to find the editor to use
// Order in slice is override order, the latest will override the first ones
editorEnvVariables = []string{"EDITOR", "VISUAL"}
)

func GetSystemDefaultEditor() string {
switch runtime.GOOS {
case "windows":
return "notepad"
default:
return "vi"
}
}

func GetDefaultEditor() string {
editor := ""
for _, envVar := range editorEnvVariables {
tmp := os.Getenv(envVar)
if tmp != "" {
editor = tmp
}
}

if editor == "" {
return GetSystemDefaultEditor()
}

return editor
}
18 changes: 18 additions & 0 deletions internal/editor/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package editor

import (
"github.com/scaleway/scaleway-cli/v2/internal/core"
)

var LongDescription = `This command starts your default editor to edit a marshaled version of your resource
Default editor will be taken from $VISUAL, then $EDITOR or an editor based on your system`

func MarshalModeArgSpec() *core.ArgSpec {
return &core.ArgSpec{
Name: "mode",
Short: "marshaling used when editing data",
Required: false,
Default: core.DefaultValueSetter(MarshalModeDefault),
EnumValues: MarshalModeEnum,
}
}
144 changes: 144 additions & 0 deletions internal/editor/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package editor

import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/scaleway/scaleway-cli/v2/internal/config"
)

var SkipEditor = false
var marshalMode = MarshalModeYAML

type GetResourceFunc func(interface{}) (interface{}, error)
type Config struct {
// PutRequest means that the request replace all fields
// If false, fields that were not edited will not be sent
// If true, all fields will be sent
PutRequest bool

MarshalMode MarshalMode

// Template is a template that will be shown before marshaled data in edited file
Template string

// IgnoreFields is a list of json tags that will be removed from marshaled data
// The content of these fields will be lost in edited data
IgnoreFields []string

// If not empty, this will replace edited text as if it was edited in the terminal
// Should be paired with global SkipEditor as true, useful for tests
editedResource string
}

func editorPathAndArgs(fileName string) (string, []string) {
defaultEditor := config.GetDefaultEditor()
editorAndArguments := strings.Fields(defaultEditor)
args := []string{fileName}

if len(editorAndArguments) > 1 {
args = append(editorAndArguments[1:], args...)
}

return editorAndArguments[0], args
}

// edit create a temporary file with given content, start a text editor then return edited content
// temporary file will be deleted on complete
// temporary file is not deleted if edit fails
func edit(content []byte) ([]byte, error) {
if SkipEditor {
return content, nil
}

tmpFileName, err := createTemporaryFile(content, marshalMode)
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tmpFileName)

editorPath, args := editorPathAndArgs(tmpFileName)
cmd := exec.Command(editorPath, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("failed to edit temporary file %q: %w", tmpFileName, err)
}

editedContent, err := os.ReadFile(tmpFileName)
if err != nil {
return nil, fmt.Errorf("failed to read temporary file %q: %w", tmpFileName, err)
}

return editedContent, nil
}

// updateResourceEditor takes a complete resource and a partial updateRequest
// will return a copy of updateRequest that has been edited
func updateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) {
// Create a copy of updateRequest completed with resource content
completeUpdateRequest := copyAndCompleteUpdateRequest(updateRequest, resource)

// TODO: fields present in updateRequest should be removed from marshal
// ex: namespace_id, region, zone
// Currently not an issue as fields that should be removed are mostly path parameter /{zone}/namespace/{namespace_id}
// Path parameter have "-" as json tag and are not marshaled

updateRequestMarshaled, err := marshal(completeUpdateRequest, cfg.MarshalMode)
if err != nil {
return nil, fmt.Errorf("failed to marshal update request: %w", err)
}

if len(cfg.IgnoreFields) > 0 {
updateRequestMarshaled, err = removeFields(updateRequestMarshaled, cfg.MarshalMode, cfg.IgnoreFields)
if err != nil {
return nil, fmt.Errorf("failed to remove ignored fields: %w", err)
}
}

if cfg.Template != "" {
updateRequestMarshaled = addTemplate(updateRequestMarshaled, cfg.Template, cfg.MarshalMode)
}

// Start text editor to edit marshaled request
updateRequestMarshaled, err = edit(updateRequestMarshaled)
if err != nil {
return nil, fmt.Errorf("failed to edit marshalled data: %w", err)
}

// If editedResource is present, override edited resource
// This is useful for testing purpose
if cfg.editedResource != "" {
updateRequestMarshaled = []byte(cfg.editedResource)
}

// Create a new updateRequest as destination for edited yaml/json
// Must be a new one to avoid merge of maps content
updateRequestEdited := newRequest(updateRequest)

// TODO: if !putRequest
// fill updateRequestEdited with only edited fields and fields present in updateRequest
// fields should be compared with completeUpdateRequest to find edited ones

// Add back required non-marshaled fields (zone, ID)
copyRequestPathParameters(updateRequestEdited, updateRequest)

err = unmarshal(updateRequestMarshaled, updateRequestEdited, cfg.MarshalMode)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal edited data: %w", err)
}

return updateRequestEdited, nil
}

// UpdateResourceEditor takes a complete resource and a partial updateRequest
// will return a copy of updateRequest that has been edited
// Only edited fields will be present in returned updateRequest
// If putRequest is true, all fields will be present, edited or not
func UpdateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) {
return updateResourceEditor(resource, updateRequest, cfg)
}
91 changes: 91 additions & 0 deletions internal/editor/editor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package editor

import (
"testing"

"github.com/alecthomas/assert"
)

func Test_updateResourceEditor(t *testing.T) {
SkipEditor = true

resource := &struct {
ID string
Name string
}{
"uuid",
"name",
}
updateRequest := &struct {
ID string
Name string
}{
"uuid",
"",
}

_, err := updateResourceEditor(resource, updateRequest, &Config{})
assert.Nil(t, err)
}

func Test_updateResourceEditor_pointers(t *testing.T) {
SkipEditor = true

type UpdateRequest struct {
ID string
Name *string
}
resource := &struct {
ID string
Name string
}{
"uuid",
"name",
}

updateRequest := &UpdateRequest{
"uuid",
nil,
}

editedUpdateRequestI, err := updateResourceEditor(resource, updateRequest, &Config{})
assert.Nil(t, err)
editedUpdateRequest := editedUpdateRequestI.(*UpdateRequest)

assert.NotNil(t, editedUpdateRequest.Name)
assert.Equal(t, resource.Name, *editedUpdateRequest.Name)
}

func Test_updateResourceEditor_map(t *testing.T) {
SkipEditor = true

type UpdateRequest struct {
ID string `json:"id"`
Env *map[string]string `json:"env"`
}
resource := &struct {
ID string `json:"id"`
Env map[string]string `json:"env"`
}{
"uuid",
map[string]string{
"foo": "bar",
},
}

updateRequest := &UpdateRequest{
"uuid",
nil,
}

editedUpdateRequestI, err := updateResourceEditor(resource, updateRequest, &Config{
editedResource: `
id: uuid
env: {}
`,
})
assert.Nil(t, err)
editedUpdateRequest := editedUpdateRequestI.(*UpdateRequest)
assert.NotNil(t, editedUpdateRequest.Env)
assert.True(t, len(*editedUpdateRequest.Env) == 0)
}
Loading