Skip to content

Commit 5e6e701

Browse files
committed
feat: Adding new config for workload view
feat: Add config and add some documentation feat: Refactor getValidity feat: Refactor getValidity feat: refactor the get replicas complexity feat: trigger pipeline feat: refactor workload list feat: delete log feat: unblock unit tests (will come back to it) feat: fix unit tests and add k9s.json new schema feat: add comment on the dao/workload feat: remove todo feat: add header on new file feat: add new view with crud for custom gvr and rollback config
1 parent 9b0ffb8 commit 5e6e701

16 files changed

+1029
-100
lines changed

README.md

+92
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,98 @@ k9s:
10561056
10571057
---
10581058
1059+
## Custom Workload View
1060+
1061+
You can customize the workload view with CRDs or any resources you want to see on this view.
1062+
1063+
To do so, you will need to update your config to add a new field `workloadGVRs` following this pattern:
1064+
```
1065+
k9s:
1066+
workloadGVRs:
1067+
- name: "v1/pods"
1068+
- name: "test.com/v1alpha1/myCRD"
1069+
status:
1070+
cellName: "State"
1071+
readiness:
1072+
cellName: "Current"
1073+
# The cellExtraName will be added as `cellName/cellExtraName`
1074+
cellExtraName: "Desired"
1075+
validity:
1076+
replicas:
1077+
cellCurrentName: "Current"
1078+
cellDesiredName: "Desired"
1079+
matchs:
1080+
- cellName: "State"
1081+
cellValue: "Ready"
1082+
- name: "external-secrets.io/v1beta1/externalsecrets"
1083+
status:
1084+
na: true
1085+
validity:
1086+
matchs:
1087+
- cellName: Ready
1088+
cellValue: True
1089+
- cellName: Status
1090+
cellValue: SecretSynced
1091+
```
1092+
The first one (`v1/pods`) will be recognized by k9s and will set it's default values for the readiness, validity and status.
1093+
1094+
The second one (`test.com/v1alpha1/myCRD`) will be an unknown GVR, it will use this configuration to be shown on the workload view.
1095+
1096+
The third one (`external-secrets.io/v1beta1/externalsecrets`) will be an unknown GVR, it will use this configuration to be shown on the workload view, but as the readiness is not set, it will use the default values it. About the status, it's set as `na: true` not applicable (for example the secrets does not need a status).
1097+
1098+
The default values applied for an unknown GVR are if they are not set and if they are not flagged as not applicable are:
1099+
```
1100+
status:
1101+
cellName: "Status"
1102+
validity:
1103+
matchs:
1104+
- cellName: "Ready"
1105+
cellValue: "True"
1106+
readiness:
1107+
cellName: "Ready"
1108+
```
1109+
1110+
The known GVRs from k9s are:
1111+
```
1112+
- v1/pods
1113+
- apps/v1/replicasets
1114+
- v1/serviceaccounts
1115+
- v1/persistentvolumeclaims
1116+
- scheduling.k8s.io/v1/priorityclasses
1117+
- v1/configmaps
1118+
- v1/secrets
1119+
- v1/services
1120+
- apps/v1/daemonsets
1121+
- apps/v1/statefulSets
1122+
```
1123+
1124+
The full structure about the configuration is:
1125+
```
1126+
workloadGVRs:
1127+
- name: string
1128+
status:
1129+
cellName: string
1130+
na: bool
1131+
readiness:
1132+
cellName: string
1133+
cellExtraName: string
1134+
na: bool
1135+
validity:
1136+
matchs:
1137+
- cellName: string
1138+
cellValue: string
1139+
- cellName: string
1140+
cellValue: string
1141+
...
1142+
replicas:
1143+
cellCurrentName: string
1144+
cellDesiredName: string
1145+
cellAllName: string
1146+
na: bool
1147+
```
1148+
1149+
---
1150+
10591151
## Contributors
10601152
10611153
Without the contributions from these fine folks, this project would be a total dud!

internal/config/alias.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type Aliases struct {
3131
// NewAliases return a new alias.
3232
func NewAliases() *Aliases {
3333
return &Aliases{
34-
Alias: make(Alias, 50),
34+
Alias: make(Alias, 56),
3535
}
3636
}
3737

@@ -190,6 +190,8 @@ func (a *Aliases) loadDefaultAliases() {
190190
a.declare("pulses", "pulse", "pu", "hz")
191191
a.declare("xrays", "xray", "x")
192192
a.declare("workloads", "workload", "wk")
193+
// TODO: Find better name
194+
a.declare("workloadgvr", "workloadgvr", "wkg")
193195
}
194196

195197
// Save alias to disk.

internal/config/alias_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func TestAliasesLoad(t *testing.T) {
111111
a := config.NewAliases()
112112

113113
assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml")))
114-
assert.Equal(t, 54, len(a.Alias))
114+
assert.Equal(t, 56, len(a.Alias))
115115
}
116116

117117
func TestAliasesSave(t *testing.T) {

internal/config/config.go

+15
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ func (c *Config) ContextAliasesPath() string {
5353
return AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName)
5454
}
5555

56+
// ContextWorkloadPath returns a context specific workload file spec.
57+
func (c *Config) ContextWorkloadPath() string {
58+
ct, err := c.K9s.ActiveContext()
59+
if err != nil {
60+
return ""
61+
}
62+
63+
return AppContextWorkloadFile(ct.GetClusterName(), c.K9s.activeContextName)
64+
}
65+
66+
// ContextWorkloadDir returns the workload directory (which contains the custom GVRs)
67+
func (c *Config) ContextWorkloadDir() string {
68+
return AppWorkloadsDir()
69+
}
70+
5671
// ContextPluginsPath returns a context specific plugins file spec.
5772
func (c *Config) ContextPluginsPath() (string, error) {
5873
ct, err := c.K9s.ActiveContext()

internal/config/files.go

+10
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,21 @@ func AppContextDir(cluster, context string) string {
200200
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context))
201201
}
202202

203+
// AppWorkloadsDir generates a valid workload folder path
204+
func AppWorkloadsDir() string {
205+
return filepath.Join(AppContextsDir, "workloads")
206+
}
207+
203208
// AppContextAliasesFile generates a valid context specific aliases file path.
204209
func AppContextAliasesFile(cluster, context string) string {
205210
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "aliases.yaml")
206211
}
207212

213+
// AppContextWorkloadFile generates a valid context specific workload file path.
214+
func AppContextWorkloadFile(cluster, context string) string {
215+
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "workloads.yaml")
216+
}
217+
208218
// AppContextPluginsFile generates a valid context specific plugins file path.
209219
func AppContextPluginsFile(cluster, context string) string {
210220
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "plugins.yaml")

internal/config/workload.go

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of K9s
3+
4+
package config
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path"
10+
11+
"github.com/derailed/k9s/internal/client"
12+
"gopkg.in/yaml.v2"
13+
)
14+
15+
var (
16+
// Template represents the template of new workload gvr
17+
Template = []byte(`name: "test.com/v1alpha1/myCRD"
18+
status:
19+
cellName: "Status"
20+
# na: true
21+
readiness:
22+
cellName: "Current"
23+
# The cellExtraName will be shown as cellName/cellExtraName
24+
cellExtraName: "Desired"
25+
# na: true
26+
validity:
27+
replicas:
28+
cellCurrentName: "Current"
29+
cellDesiredName: "Desired"
30+
# cellAllName: "Ready"
31+
matchs:
32+
- cellName: "State"
33+
cellValue: "Ready"`)
34+
)
35+
36+
var (
37+
// defaultGvr represent the default values uses if a custom gvr is set without status, validity or readiness
38+
defaultGvr = WorkloadGVR{
39+
Status: &GVRStatus{CellName: "Status"},
40+
Validity: &GVRValidity{Matchs: []Match{{CellName: "Ready", Value: "True"}}},
41+
Readiness: &GVRReadiness{CellName: "Ready"},
42+
}
43+
44+
// defaultConfigGVRs represents the default configurations
45+
defaultConfigGVRs = map[string]WorkloadGVR{
46+
"apps/v1/deployments": {
47+
Name: "apps/v1/deployments",
48+
Readiness: &GVRReadiness{CellName: "Ready"},
49+
Validity: &GVRValidity{
50+
Replicas: Replicas{CellAllName: "Ready"},
51+
},
52+
},
53+
"apps/v1/daemonsets": {
54+
Name: "apps/v1/daemonsets",
55+
Readiness: &GVRReadiness{CellName: "Ready", CellExtraName: "Desired"},
56+
Validity: &GVRValidity{
57+
Replicas: Replicas{CellDesiredName: "Desired", CellCurrentName: "Ready"},
58+
},
59+
},
60+
"apps/v1/replicasets": {
61+
Name: "apps/v1/replicasets",
62+
Readiness: &GVRReadiness{CellName: "Current", CellExtraName: "Desired"},
63+
Validity: &GVRValidity{
64+
Replicas: Replicas{CellDesiredName: "Desired", CellCurrentName: "Current"},
65+
},
66+
},
67+
"apps/v1/statefulSets": {
68+
Name: "apps/v1/statefulSets",
69+
Status: &GVRStatus{CellName: "Ready"},
70+
Readiness: &GVRReadiness{CellName: "Ready"},
71+
Validity: &GVRValidity{
72+
Replicas: Replicas{CellAllName: "Ready"},
73+
},
74+
},
75+
"v1/pods": {
76+
Name: "v1/pods",
77+
Status: &GVRStatus{CellName: "Status"},
78+
Readiness: &GVRReadiness{CellName: "Ready"},
79+
Validity: &GVRValidity{
80+
Matchs: []Match{
81+
{CellName: "Status", Value: "Running"},
82+
},
83+
Replicas: Replicas{CellAllName: "Ready"},
84+
},
85+
},
86+
}
87+
)
88+
89+
type CellName string
90+
91+
type GVRStatus struct {
92+
NA bool `json:"na" yaml:"na"`
93+
CellName CellName `json:"cellName" yaml:"cellName"`
94+
}
95+
96+
type GVRReadiness struct {
97+
NA bool `json:"na" yaml:"na"`
98+
CellName CellName `json:"cellName" yaml:"cellName"`
99+
CellExtraName CellName `json:"cellExtraName" yaml:"cellExtraName"`
100+
}
101+
102+
type Match struct {
103+
CellName CellName `json:"cellName" yaml:"cellName"`
104+
Value string `json:"cellValue" yaml:"cellValue"`
105+
}
106+
107+
type Replicas struct {
108+
CellCurrentName CellName `json:"cellCurrentName" yaml:"cellCurrentName"`
109+
CellDesiredName CellName `json:"cellDesiredName" yaml:"cellDesiredName"`
110+
CellAllName CellName `json:"cellAllName" yaml:"cellAllName"`
111+
}
112+
113+
type GVRValidity struct {
114+
NA bool `json:"na" yaml:"na"`
115+
Matchs []Match `json:"matchs,omitempty" yaml:"matchs,omitempty"`
116+
Replicas Replicas `json:"replicas" yaml:"replicas"`
117+
}
118+
119+
type WorkloadGVR struct {
120+
Name string `json:"name" yaml:"name"`
121+
Status *GVRStatus `json:"status,omitempty" yaml:"status,omitempty"`
122+
Readiness *GVRReadiness `json:"readiness,omitempty" yaml:"readiness,omitempty"`
123+
Validity *GVRValidity `json:"validity,omitempty" yaml:"validity,omitempty"`
124+
}
125+
126+
// TODO: Find better name (maybe moving it to a new file as well)
127+
type WkC struct {
128+
GVRFilenames []string `yaml:"wkg"`
129+
}
130+
131+
// TODO: Explains the parameters
132+
// NewWorkloadGVRs returns the default GVRs to use if no custom config is set
133+
func NewWorkloadGVRs(workloadPath string, filenames []string) []WorkloadGVR {
134+
workloadGVRs := make([]WorkloadGVR, 0)
135+
for _, gvr := range defaultConfigGVRs {
136+
workloadGVRs = append(workloadGVRs, gvr)
137+
}
138+
139+
// Append custom GVRS
140+
if len(filenames) != 0 {
141+
for _, filename := range filenames {
142+
wkgvr, err := GetWorkloadGVRFromFile(path.Join(workloadPath, fmt.Sprintf("%s.%s", filename, "yaml")))
143+
if err == nil {
144+
workloadGVRs = append(workloadGVRs, wkgvr)
145+
}
146+
}
147+
}
148+
149+
return workloadGVRs
150+
}
151+
152+
// GetWorkloadGVRFromFile returns a gvr from a filepath
153+
func GetWorkloadGVRFromFile(filepath string) (WorkloadGVR, error) {
154+
yamlFile, err := os.ReadFile(filepath)
155+
if err != nil {
156+
return WorkloadGVR{}, err
157+
}
158+
159+
var wkgvr WorkloadGVR
160+
if err = yaml.Unmarshal(yamlFile, &wkgvr); err != nil {
161+
return WorkloadGVR{}, err
162+
}
163+
164+
return wkgvr, nil
165+
}
166+
167+
// GetGVR will return the GVR defined by the WorkloadGVR's name
168+
func (wgvr WorkloadGVR) GetGVR() client.GVR {
169+
return client.NewGVR(wgvr.Name)
170+
}
171+
172+
// ApplyDefault will complete the GVR with missing values
173+
// If it's an existing GVR's name, it will apply their corresponding default values
174+
// If it's an unknown resources without readiness, status or validity it will use the default ones
175+
func (wkgvr *WorkloadGVR) ApplyDefault() {
176+
// Apply default values
177+
existingGvr, ok := defaultConfigGVRs[wkgvr.Name]
178+
if ok {
179+
wkgvr.applyDefaultValues(existingGvr)
180+
} else {
181+
wkgvr.applyDefaultValues(defaultGvr)
182+
}
183+
}
184+
185+
func (wkgvr *WorkloadGVR) applyDefaultValues(defaultGVR WorkloadGVR) {
186+
if wkgvr.Status == nil {
187+
wkgvr.Status = defaultGVR.Status
188+
}
189+
190+
if wkgvr.Readiness == nil {
191+
wkgvr.Readiness = defaultGVR.Readiness
192+
}
193+
194+
if wkgvr.Validity == nil {
195+
wkgvr.Validity = defaultGVR.Validity
196+
}
197+
}

internal/dao/registry.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ func NewMeta() *Meta {
6565
// Customize here for non resource types or types with metrics or logs.
6666
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
6767
m := Accessors{
68-
client.NewGVR("workloads"): &Workload{},
68+
client.NewGVR("workloads"): &Workload{},
69+
// TODO: find better name
70+
client.NewGVR("workloadgvr"): &WorkloadGVR{},
6971
client.NewGVR("contexts"): &Context{},
7072
client.NewGVR("containers"): &Container{},
7173
client.NewGVR("scans"): &ImageScan{},
@@ -214,6 +216,15 @@ func loadK9s(m ResourceMetas) {
214216
ShortNames: []string{"wk"},
215217
Categories: []string{k9sCat},
216218
}
219+
// TODO: find better name
220+
m[client.NewGVR("workloadgvr")] = metav1.APIResource{
221+
Name: "workloadgvr",
222+
Kind: "Workloadgvr",
223+
SingularName: "workloadgvr",
224+
Namespaced: true,
225+
ShortNames: []string{"wkg"},
226+
Categories: []string{k9sCat},
227+
}
217228
m[client.NewGVR("pulses")] = metav1.APIResource{
218229
Name: "pulses",
219230
Kind: "Pulse",

0 commit comments

Comments
 (0)