Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and Duplicate Resource actions #2563

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img src="assets/k9s.png" alt="k9s">

## K9s - Kubernetes CLI To Manage Your Clusters In Style!
## K9s - Kubernetes CLI To Manage Your Clusters In Style

K9s provides a terminal UI to interact with your Kubernetes clusters.
The aim of this project is to make it easier to navigate, observe and manage
Expand All @@ -9,7 +9,7 @@ for changes and offers subsequent commands to interact with your observed resour

---

## Note...
## Note

K9s is not pimped out by a big corporation with deep pockets.
It is a complex OSS project that demands a lot of my time to maintain and support.
Expand Down Expand Up @@ -356,8 +356,10 @@ K9s uses aliases to navigate most K8s resources.
| To view and switch directly to another Kubernetes context (Last used view) | `:`ctx context-name⏎ | |
| To view and switch to another Kubernetes namespace | `:`ns⏎ | |
| To view all saved resources | `:`screendump or sd⏎ | |
| To launch a new editor to create a new resource | `ctrl-n` | |
| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | |
| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k` | |
| To duplicate a resource | `shift-d` | Only in YAML view, opens a new editor with the current resource |
| Launch pulses view | `:`pulses or pu⏎ | |
| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional |
| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) |
Expand Down Expand Up @@ -1056,18 +1058,18 @@ K9s will most likely blow up if...

---

## ATTA Girls/Boys!
## ATTA Girls/Boys

K9s sits on top of many open source projects and libraries. Our *sincere*
appreciations to all the OSS contributors that work nights and weekends
to make this project a reality!

---

## Meet The Core Team!
## Meet The Core Team

* [Fernand Galiana](https://github.com/derailed)
* <img src="assets/mail.png" width="16" height="auto" alt="email"/> [email protected]
* <img src="assets/mail.png" width="16" height="auto" alt="email"/> <[email protected]>
* <img src="assets/twitter.png" width="16" height="auto" alt="twitter"/> [@kitesurfer](https://twitter.com/kitesurfer?lang=en)

* [Aleksei Romanenko](https://github.com/slimus)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/fvbommel/sortorder v1.1.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-runewidth v0.0.15
github.com/mattn/go-shellwords v1.0.12
github.com/olekukonko/tablewriter v0.0.5
github.com/petergtz/pegomock v2.9.0+incompatible
github.com/rakyll/hey v0.1.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
Expand Down
109 changes: 109 additions & 0 deletions internal/view/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package view

import (
"context"
"errors"
"fmt"
"os"
"sort"
"strconv"
"strings"
Expand All @@ -14,6 +16,7 @@ import (

"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
Expand Down Expand Up @@ -305,6 +308,31 @@ func (b *Browser) TableLoadFailed(err error) {
// ----------------------------------------------------------------------------
// Actions...

func (b *Browser) createCmd(_ *tcell.EventKey) *tcell.EventKey {
tmpFile, err := createTmpYaml()
if err != nil {
b.App().Flash().Err(errors.New("Failed to create temporary resource file: " + err.Error()))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors in go should not be capitalized. Also prefer wrapping the error ie fmt.Errorf("failed ...: %w", err)
Please follow similar logic for here down.

return nil
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()

_, err = tmpFile.WriteString(strings.Join([]string{
"# Please add your resource definitions below. Lines beginning with a '#' will be ignored.",
"# Multiple resources are supported and can be separated by '---'.",
"# When done, save and close the editor, K9s will then `kubectl create` the resource.",
fmt.Sprintf("# The content will also be saved in '%s'", b.App().Config.K9s.ContextScreenDumpDir()),
"# in case you need to recover it.",
"#",
}, "\n"))
if err != nil {
b.App().Flash().Err(errors.New("Failed to write to temporary resource file: " + err.Error()))
return nil
}

return editAndCreateFromFile(b.App(), tmpFile.Name())
}

func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
path := b.GetSelectedItem()
if path == "" {
Expand Down Expand Up @@ -558,6 +586,10 @@ func (b *Browser) refreshActions() {
if !dao.IsK9sMeta(b.meta) {
aa.Add(ui.KeyY, ui.NewKeyAction(yamlAction, b.viewCmd, true))
aa.Add(ui.KeyD, ui.NewKeyAction("Describe", b.describeCmd, true))

if !b.app.Config.K9s.IsReadOnly() {
aa.Add(tcell.KeyCtrlN, ui.NewKeyAction("Create", b.createCmd, true))
}
}
for _, f := range b.bindKeysFn {
f(aa)
Expand Down Expand Up @@ -644,3 +676,80 @@ func (b *Browser) resourceDelete(selections []string, msg string) {
}
dialog.ShowDelete(b.app.Styles.Dialog(), b.app.Content.Pages, msg, okFn, func() {})
}

func editAndCreateFromFile(app *App, filePath string) *tcell.EventKey {
if !edit(app, shellOpts{clear: true, args: []string{filePath}}) {
app.Flash().Errf("Failed to launch editor")
return nil
}

if isEmpty, err := isFileEmpty(filePath); err != nil || isEmpty {
if err != nil {
app.Flash().Err(err)
}
return nil
}

content, err := os.ReadFile(filePath)
if err != nil {
app.Flash().Errf("Failed to create resource from file %s", err)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are reading the file at this stage. The error should reflect that

return nil
}

dumpedFile, err := saveYAML(app.Config.K9s.ContextScreenDumpDir(), "create", string(content))
if err != nil {
app.Flash().Err(err)
return nil
}

return createFromFile(app, dumpedFile)
}

func isFileEmpty(filePath string) (bool, error) {
fileStat, err := os.Stat(filePath)
if err != nil {
return false, errors.New("Failed to get temporary resource file information: " + err.Error())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer error wrapping as mentioned above

}

return fileStat.Size() == 0, nil
}

func createFromFile(app *App, filePath string) *tcell.EventKey {
args := []string{
"create",
"-f",
filePath,
}
res, err := runKu(app, shellOpts{clear: false, args: args})
if err != nil {
res = "status:\n " + err.Error() + "\nmessage:\n" + fmtResults(res)
} else {
res = "message:\n" + fmtResults(res)
}

details := NewDetails(app, "Applied Manifest", filePath, contentYAML, true).Update(res)
if err := app.inject(details, false); err != nil {
app.Flash().Err(err)
return nil
}

return nil
}

func createTmpYaml() (*os.File, error) {
tmpDir, err := config.UserTmpDir()
if err != nil {
return nil, err
}

if err := ensureDir(tmpDir); err != nil {
return nil, err
}

tmpFile, err := os.CreateTemp(tmpDir, "tmp--*.yml")
if err != nil {
return nil, err
}

return tmpFile, nil
}
53 changes: 43 additions & 10 deletions internal/view/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"time"

"github.com/derailed/k9s/internal/render"
"github.com/mattn/go-shellwords"

"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
Expand All @@ -40,6 +41,8 @@ const (

var editorEnvVars = []string{"KUBE_EDITOR", "K9S_EDITOR", "EDITOR"}

var lookPath func(file string) (string, error) = exec.LookPath

type shellOpts struct {
clear, background bool
pipes []string
Expand Down Expand Up @@ -116,26 +119,56 @@ func run(a *App, opts shellOpts) (bool, chan error, chan string) {
}), errChan, statusChan
}

func edit(a *App, opts shellOpts) bool {
var (
bin string
err error
)
func findEditor(opts shellOpts) (shellOpts, error) {
var bin string
var args []string

for _, e := range editorEnvVars {
env := os.Getenv(e)
if env == "" {
var _cmd string
var _args []string

v := os.Getenv(e)
if v == "" {
continue
}
if bin, err = exec.LookPath(env); err == nil {

words, err := shellwords.Parse(v)
if err != nil {
continue
}

switch len(words) {
case 0:
continue
case 1:
_cmd, _args = words[0], nil
default:
_cmd, _args = words[0], words[1:]
}

if bin, err = lookPath(_cmd); err == nil {
args = _args
break
}
}

if bin == "" {
a.Flash().Errf("You must set at least one of those env vars: %s", strings.Join(editorEnvVars, "|"))
return false
return opts, fmt.Errorf("You must set at least one of those env vars: %s", strings.Join(editorEnvVars, "|"))
}

opts.args = append(args, opts.args...)
opts.binary, opts.background = bin, false

return opts, nil
}

func edit(a *App, opts shellOpts) bool {
opts, err := findEditor(opts)
if err != nil {
a.Flash().Err(err)
return false
}

suspended, errChan, _ := run(a, opts)
if !suspended {
a.Flash().Errf("edit command failed")
Expand Down
71 changes: 71 additions & 0 deletions internal/view/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package view

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

var fakeLookPath = func(file string) (string, error) {
return fmt.Sprintf("/usr/bin/%s", file), nil
}

func TestFindEditor(t *testing.T) {
lookPath = fakeLookPath

type result struct {
binary string
args []string
}

uu := map[string]struct {
env map[string]string
e result
err bool
}{
"no-editor": {
env: map[string]string{},
e: result{binary: "", args: nil},
err: true,
},
"vi-by-EDITOR": {
env: map[string]string{"EDITOR": "vi"},
e: result{binary: "vi", args: nil},
},
"vim-with-args-by-EDITOR": {
env: map[string]string{"EDITOR": "vim --wait --some=thing"},
e: result{binary: "vim", args: []string{"--wait", "--some=thing"}},
},
"code-with-args-by-KUBE_EDITOR": {
env: map[string]string{"KUBE_EDITOR": "code -w"},
e: result{binary: "code", args: []string{"-w"}},
},
"code-with-args-by-K9S-EDITOR": {
env: map[string]string{"K9S_EDITOR": "code --wait"},
e: result{binary: "code", args: []string{"--wait"}},
},
}

for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
opts := shellOpts{}

for _, v := range editorEnvVars {
if _, ok := u.env[v]; !ok {
t.Setenv(v, "")
}
}

for k, v := range u.env {
t.Setenv(k, v)
}

got, err := findEditor(opts)
assert.Equal(t, u.err, err != nil)
assert.Contains(t, got.binary, u.e.binary)
assert.Equal(t, u.e.args, got.args)
})
}
}
Loading