Skip to content

Feat: add customizable workload GVRs #3031

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

Closed
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,20 @@ k9s:

---

## Custom Workload View

You can customize the workload view with CRDs or any resources you want to see on this view.

To do so, you can go to the `workloadGVR` view, you'll be able to see all your custom GVRs. You can also create, edit, delete them.

You can also describe by pressing `d` or simulate them by pressing `enter`.

You can create new one from this view, this will ask you for a custom GVR name and will set default values (to comment or uncomment).

There is a way to add a custom GVR to you cluster context or to delete them, they will be added on top of the default workloads GVRS.

---

## Contributors

Without the contributions from these fine folks, this project would be a total dud!
Expand Down
3 changes: 2 additions & 1 deletion internal/config/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Aliases struct {
// NewAliases return a new alias.
func NewAliases() *Aliases {
return &Aliases{
Alias: make(Alias, 50),
Alias: make(Alias, 56),
}
}

Expand Down Expand Up @@ -190,6 +190,7 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("pulses", "pulse", "pu", "hz")
a.declare("xrays", "xray", "x")
a.declare("workloads", "workload", "wk")
a.declare("workloadgvrs", "workloadgvr", "wkg")
}

// Save alias to disk.
Expand Down
2 changes: 1 addition & 1 deletion internal/config/alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestAliasesLoad(t *testing.T) {
a := config.NewAliases()

assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml")))
assert.Equal(t, 54, len(a.Alias))
assert.Equal(t, 57, len(a.Alias))
}

func TestAliasesSave(t *testing.T) {
Expand Down
15 changes: 15 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ func (c *Config) ContextAliasesPath() string {
return AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName)
}

// ContextWorkloadPath returns a context specific workload file spec.
func (c *Config) ContextWorkloadPath() string {
ct, err := c.K9s.ActiveContext()
if err != nil {
return ""
}

return AppContextWorkloadFile(ct.GetClusterName(), c.K9s.activeContextName)
}

// ContextWorkloadDir returns the workload directory (which contains the custom GVRs)
func (c *Config) ContextWorkloadDir() string {
return AppWorkloadsDir()
}

// ContextPluginsPath returns a context specific plugins file spec.
func (c *Config) ContextPluginsPath() (string, error) {
ct, err := c.K9s.ActiveContext()
Expand Down
11 changes: 11 additions & 0 deletions internal/config/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"io/fs"
"os"
"path"
"path/filepath"

"github.com/derailed/k9s/internal/config/data"
Expand Down Expand Up @@ -200,11 +201,21 @@ func AppContextDir(cluster, context string) string {
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context))
}

// AppWorkloadsDir generates a valid workload folder path
func AppWorkloadsDir() string {
return path.Join(AppContextsDir, "workloads")
}

// AppContextAliasesFile generates a valid context specific aliases file path.
func AppContextAliasesFile(cluster, context string) string {
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "aliases.yaml")
}

// AppContextWorkloadFile generates a valid context specific workload file path.
func AppContextWorkloadFile(cluster, context string) string {
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "workloads.yaml")
}

// AppContextPluginsFile generates a valid context specific plugins file path.
func AppContextPluginsFile(cluster, context string) string {
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "plugins.yaml")
Expand Down
201 changes: 201 additions & 0 deletions internal/config/workload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package config

import (
"errors"
"fmt"
"os"
"path"

"github.com/derailed/k9s/internal/client"
"gopkg.in/yaml.v2"
)

var (
// Template represents the template of a new workload gvr
Template = []byte(`name: "test.com/v1alpha1/myCRD"
status:
cellName: "Status"
# na: true
readiness:
cellName: "Current"
# The cellExtraName will be shown as cellName/cellExtraName
cellExtraName: "Desired"
# na: true
validity:
replicas:
cellCurrentName: "Current"
cellDesiredName: "Desired"
# cellAllName: "Ready"
matchs:
- cellName: "State"
cellValue: "Ready"`)
)

var (
// defaultGvr represent the default values uses if a custom gvr is set without status, validity or readiness
defaultGvr = WorkloadGVR{
Status: &GVRStatus{CellName: "Status"},
Validity: &GVRValidity{Matchs: []Match{{CellName: "Ready", Value: "True"}}},
Readiness: &GVRReadiness{CellName: "Ready"},
}

// defaultConfigGVRs represents the default configurations
defaultConfigGVRs = map[string]WorkloadGVR{
"apps/v1/deployments": {
Name: "apps/v1/deployments",
Readiness: &GVRReadiness{CellName: "Ready"},
Validity: &GVRValidity{
Replicas: Replicas{CellAllName: "Ready"},
},
},
"apps/v1/daemonsets": {
Name: "apps/v1/daemonsets",
Readiness: &GVRReadiness{CellName: "Ready", CellExtraName: "Desired"},
Validity: &GVRValidity{
Replicas: Replicas{CellDesiredName: "Desired", CellCurrentName: "Ready"},
},
},
"apps/v1/replicasets": {
Name: "apps/v1/replicasets",
Readiness: &GVRReadiness{CellName: "Current", CellExtraName: "Desired"},
Validity: &GVRValidity{
Replicas: Replicas{CellDesiredName: "Desired", CellCurrentName: "Current"},
},
},
"apps/v1/statefulSets": {
Name: "apps/v1/statefulSets",
Status: &GVRStatus{CellName: "Ready"},
Readiness: &GVRReadiness{CellName: "Ready"},
Validity: &GVRValidity{
Replicas: Replicas{CellAllName: "Ready"},
},
},
"v1/pods": {
Name: "v1/pods",
Status: &GVRStatus{CellName: "Status"},
Readiness: &GVRReadiness{CellName: "Ready"},
Validity: &GVRValidity{
Matchs: []Match{
{CellName: "Status", Value: "Running"},
},
Replicas: Replicas{CellAllName: "Ready"},
},
},
}
)

type CellName string

type GVRStatus struct {
NA bool `json:"na" yaml:"na"`
CellName CellName `json:"cellName" yaml:"cellName"`
}

type GVRReadiness struct {
NA bool `json:"na" yaml:"na"`
CellName CellName `json:"cellName" yaml:"cellName"`
CellExtraName CellName `json:"cellExtraName" yaml:"cellExtraName"`
}

type Match struct {
CellName CellName `json:"cellName" yaml:"cellName"`
Value string `json:"cellValue" yaml:"cellValue"`
}

type Replicas struct {
CellCurrentName CellName `json:"cellCurrentName" yaml:"cellCurrentName"`
CellDesiredName CellName `json:"cellDesiredName" yaml:"cellDesiredName"`
CellAllName CellName `json:"cellAllName" yaml:"cellAllName"`
}

type GVRValidity struct {
NA bool `json:"na" yaml:"na"`
Matchs []Match `json:"matchs,omitempty" yaml:"matchs,omitempty"`
Replicas Replicas `json:"replicas" yaml:"replicas"`
}

type WorkloadGVR struct {
Name string `json:"name" yaml:"name"`
Status *GVRStatus `json:"status,omitempty" yaml:"status,omitempty"`
Readiness *GVRReadiness `json:"readiness,omitempty" yaml:"readiness,omitempty"`
Validity *GVRValidity `json:"validity,omitempty" yaml:"validity,omitempty"`
}

type WorkloadConfig struct {
GVRFilenames []string `yaml:"wkg"`
}

// NewWorkloadGVRs returns the default GVRs to use if no custom config is set
// The workloadDir represent the directory of the custom workloads, the gvrNames are the custom gvrs names
func NewWorkloadGVRs(workloadDir string, gvrNames []string) ([]WorkloadGVR, error) {
workloadGVRs := make([]WorkloadGVR, 0)
for _, gvr := range defaultConfigGVRs {
workloadGVRs = append(workloadGVRs, gvr)
}

var errs error

// Append custom GVRS
if len(gvrNames) != 0 {
for _, filename := range gvrNames {
wkgvr, err := GetWorkloadGVRFromFile(path.Join(workloadDir, fmt.Sprintf("%s.%s", filename, "yaml")))
if err != nil {
errs = errors.Join(errs, err)
continue
}
workloadGVRs = append(workloadGVRs, wkgvr)
}
}

return workloadGVRs, errs
}

// GetWorkloadGVRFromFile returns a gvr from a filepath
func GetWorkloadGVRFromFile(filepath string) (WorkloadGVR, error) {
yamlFile, err := os.ReadFile(filepath)
if err != nil {
return WorkloadGVR{}, err
}

var wkgvr WorkloadGVR
if err = yaml.Unmarshal(yamlFile, &wkgvr); err != nil {
return WorkloadGVR{}, err
}

return wkgvr, nil
}

// GetGVR will return the GVR defined by the WorkloadGVR's name
func (wgvr WorkloadGVR) GetGVR() client.GVR {
return client.NewGVR(wgvr.Name)
}

// ApplyDefault will complete the GVR with missing values
// If it's an existing GVR's name, it will apply their corresponding default values
// If it's an unknown resources without readiness, status or validity it will use the default ones
func (wkgvr *WorkloadGVR) ApplyDefault() {
// Apply default values
existingGvr, ok := defaultConfigGVRs[wkgvr.Name]
if ok {
wkgvr.applyDefaultValues(existingGvr)
} else {
wkgvr.applyDefaultValues(defaultGvr)
}
}

func (wkgvr *WorkloadGVR) applyDefaultValues(defaultGVR WorkloadGVR) {
if wkgvr.Status == nil {
wkgvr.Status = defaultGVR.Status
}

if wkgvr.Readiness == nil {
wkgvr.Readiness = defaultGVR.Readiness
}

if wkgvr.Validity == nil {
wkgvr.Validity = defaultGVR.Validity
}
}
9 changes: 9 additions & 0 deletions internal/dao/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func NewMeta() *Meta {
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
m := Accessors{
client.NewGVR("workloads"): &Workload{},
client.NewGVR("workloadgvrs"): &WorkloadGVR{},
client.NewGVR("contexts"): &Context{},
client.NewGVR("containers"): &Container{},
client.NewGVR("scans"): &ImageScan{},
Expand Down Expand Up @@ -214,6 +215,14 @@ func loadK9s(m ResourceMetas) {
ShortNames: []string{"wk"},
Categories: []string{k9sCat},
}
m[client.NewGVR("workloadgvrs")] = metav1.APIResource{
Name: "workloadgvrs",
Kind: "Workloadgvr",
SingularName: "workloadgvr",
Namespaced: true,
ShortNames: []string{"wkg"},
Categories: []string{k9sCat},
}
m[client.NewGVR("pulses")] = metav1.APIResource{
Name: "pulses",
Kind: "Pulse",
Expand Down
Loading