Skip to content

Commit 49a8de4

Browse files
committed
fix: add storage class pages
1 parent 542caaf commit 49a8de4

8 files changed

Lines changed: 709 additions & 0 deletions

File tree

controllers/storageclass.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package controllers
2+
3+
import (
4+
"encoding/json"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
storagev1 "k8s.io/api/storage/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/client-go/rest"
10+
11+
"github.com/casosorg/casos/object"
12+
)
13+
14+
const (
15+
defaultStorageClassAnnotation = "storageclass.kubernetes.io/is-default-class"
16+
defaultStorageClassAnnotationBeta = "storageclass.beta.kubernetes.io/is-default-class"
17+
)
18+
19+
type storageClassSummary struct {
20+
Name string `json:"name"`
21+
Provisioner string `json:"provisioner"`
22+
ReclaimPolicy string `json:"reclaimPolicy"`
23+
VolumeBindingMode string `json:"volumeBindingMode"`
24+
AllowVolumeExpansion bool `json:"allowVolumeExpansion"`
25+
Parameters map[string]string `json:"parameters"`
26+
IsDefault bool `json:"isDefault"`
27+
CreatedAt string `json:"createdAt"`
28+
ResourceVersion string `json:"resourceVersion"`
29+
}
30+
31+
func isDefaultStorageClass(sc storagev1.StorageClass) bool {
32+
return sc.Annotations[defaultStorageClassAnnotation] == "true" ||
33+
sc.Annotations[defaultStorageClassAnnotationBeta] == "true"
34+
}
35+
36+
func toScSummary(sc storagev1.StorageClass) storageClassSummary {
37+
reclaimPolicy := ""
38+
if sc.ReclaimPolicy != nil {
39+
reclaimPolicy = string(*sc.ReclaimPolicy)
40+
}
41+
volumeBindingMode := ""
42+
if sc.VolumeBindingMode != nil {
43+
volumeBindingMode = string(*sc.VolumeBindingMode)
44+
}
45+
allowVolumeExpansion := false
46+
if sc.AllowVolumeExpansion != nil {
47+
allowVolumeExpansion = *sc.AllowVolumeExpansion
48+
}
49+
return storageClassSummary{
50+
Name: sc.Name,
51+
Provisioner: sc.Provisioner,
52+
ReclaimPolicy: reclaimPolicy,
53+
VolumeBindingMode: volumeBindingMode,
54+
AllowVolumeExpansion: allowVolumeExpansion,
55+
Parameters: sc.Parameters,
56+
IsDefault: isDefaultStorageClass(sc),
57+
CreatedAt: sc.CreationTimestamp.UTC().Format("2006-01-02 15:04:05"),
58+
ResourceVersion: sc.ResourceVersion,
59+
}
60+
}
61+
62+
// GetStorageClasses
63+
// @router /api/get-storageclasses [get]
64+
func (c *ApiController) GetStorageClasses() {
65+
cfg := getAdminRestConfig()
66+
if cfg == nil {
67+
c.ResponseError("apiserver not ready")
68+
return
69+
}
70+
scs, err := object.GetStorageClasses(cfg)
71+
if err != nil {
72+
c.ResponseError(err.Error())
73+
return
74+
}
75+
result := make([]storageClassSummary, 0, len(scs))
76+
for _, sc := range scs {
77+
result = append(result, toScSummary(sc))
78+
}
79+
c.ResponseOk(result)
80+
}
81+
82+
// GetStorageClass
83+
// @router /api/get-storageclass [get]
84+
func (c *ApiController) GetStorageClass() {
85+
cfg := getAdminRestConfig()
86+
if cfg == nil {
87+
c.ResponseError("apiserver not ready")
88+
return
89+
}
90+
name := c.GetString("name")
91+
sc, err := object.GetStorageClass(cfg, name)
92+
if err != nil {
93+
c.ResponseError(err.Error())
94+
return
95+
}
96+
c.ResponseOk(toScSummary(*sc))
97+
}
98+
99+
type storageClassRequest struct {
100+
Name string `json:"name"`
101+
Provisioner string `json:"provisioner"`
102+
ReclaimPolicy string `json:"reclaimPolicy"`
103+
VolumeBindingMode string `json:"volumeBindingMode"`
104+
AllowVolumeExpansion bool `json:"allowVolumeExpansion"`
105+
Parameters map[string]string `json:"parameters"`
106+
IsDefault bool `json:"isDefault"`
107+
ResourceVersion string `json:"resourceVersion"`
108+
}
109+
110+
func defaultAnnotations(isDefault bool) map[string]string {
111+
if !isDefault {
112+
return nil
113+
}
114+
return map[string]string{
115+
defaultStorageClassAnnotation: "true",
116+
defaultStorageClassAnnotationBeta: "true",
117+
}
118+
}
119+
120+
// clearOtherDefaultStorageClasses unsets the default annotation on every StorageClass
121+
// other than exceptName, so at most one StorageClass is ever marked default.
122+
func clearOtherDefaultStorageClasses(cfg *rest.Config, exceptName string) error {
123+
scs, err := object.GetStorageClasses(cfg)
124+
if err != nil {
125+
return err
126+
}
127+
for _, sc := range scs {
128+
if sc.Name == exceptName || !isDefaultStorageClass(sc) {
129+
continue
130+
}
131+
delete(sc.Annotations, defaultStorageClassAnnotation)
132+
delete(sc.Annotations, defaultStorageClassAnnotationBeta)
133+
if _, err := object.UpdateStorageClass(cfg, &sc); err != nil {
134+
return err
135+
}
136+
}
137+
return nil
138+
}
139+
140+
// AddStorageClass
141+
// @router /api/add-storageclass [post]
142+
func (c *ApiController) AddStorageClass() {
143+
cfg := getAdminRestConfig()
144+
if cfg == nil {
145+
c.ResponseError("apiserver not ready")
146+
return
147+
}
148+
var req storageClassRequest
149+
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
150+
c.ResponseError("invalid request body: " + err.Error())
151+
return
152+
}
153+
reclaimPolicy := corev1.PersistentVolumeReclaimPolicy(req.ReclaimPolicy)
154+
if reclaimPolicy == "" {
155+
reclaimPolicy = corev1.PersistentVolumeReclaimDelete
156+
}
157+
volumeBindingMode := storagev1.VolumeBindingMode(req.VolumeBindingMode)
158+
if volumeBindingMode == "" {
159+
volumeBindingMode = storagev1.VolumeBindingImmediate
160+
}
161+
allowVolumeExpansion := req.AllowVolumeExpansion
162+
sc := &storagev1.StorageClass{
163+
ObjectMeta: metav1.ObjectMeta{
164+
Name: req.Name,
165+
Annotations: defaultAnnotations(req.IsDefault),
166+
},
167+
Provisioner: req.Provisioner,
168+
Parameters: req.Parameters,
169+
ReclaimPolicy: &reclaimPolicy,
170+
VolumeBindingMode: &volumeBindingMode,
171+
AllowVolumeExpansion: &allowVolumeExpansion,
172+
}
173+
created, err := object.AddStorageClass(cfg, sc)
174+
if err != nil {
175+
c.ResponseError(err.Error())
176+
return
177+
}
178+
if req.IsDefault {
179+
if err := clearOtherDefaultStorageClasses(cfg, req.Name); err != nil {
180+
c.ResponseError(err.Error())
181+
return
182+
}
183+
}
184+
c.ResponseOk(toScSummary(*created))
185+
}
186+
187+
// UpdateStorageClass updates the mutable fields of a StorageClass; provisioner,
188+
// parameters and volumeBindingMode are immutable after creation.
189+
// @router /api/update-storageclass [post]
190+
func (c *ApiController) UpdateStorageClass() {
191+
cfg := getAdminRestConfig()
192+
if cfg == nil {
193+
c.ResponseError("apiserver not ready")
194+
return
195+
}
196+
var req storageClassRequest
197+
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
198+
c.ResponseError("invalid request body: " + err.Error())
199+
return
200+
}
201+
existing, err := object.GetStorageClass(cfg, req.Name)
202+
if err != nil {
203+
c.ResponseError(err.Error())
204+
return
205+
}
206+
reclaimPolicy := corev1.PersistentVolumeReclaimPolicy(req.ReclaimPolicy)
207+
if reclaimPolicy == "" {
208+
reclaimPolicy = corev1.PersistentVolumeReclaimDelete
209+
}
210+
allowVolumeExpansion := req.AllowVolumeExpansion
211+
existing.ReclaimPolicy = &reclaimPolicy
212+
existing.AllowVolumeExpansion = &allowVolumeExpansion
213+
if existing.Annotations == nil {
214+
existing.Annotations = map[string]string{}
215+
}
216+
if req.IsDefault {
217+
existing.Annotations[defaultStorageClassAnnotation] = "true"
218+
existing.Annotations[defaultStorageClassAnnotationBeta] = "true"
219+
} else {
220+
delete(existing.Annotations, defaultStorageClassAnnotation)
221+
delete(existing.Annotations, defaultStorageClassAnnotationBeta)
222+
}
223+
existing.ResourceVersion = req.ResourceVersion
224+
updated, err := object.UpdateStorageClass(cfg, existing)
225+
if err != nil {
226+
c.ResponseError(err.Error())
227+
return
228+
}
229+
if req.IsDefault {
230+
if err := clearOtherDefaultStorageClasses(cfg, req.Name); err != nil {
231+
c.ResponseError(err.Error())
232+
return
233+
}
234+
}
235+
c.ResponseOk(toScSummary(*updated))
236+
}
237+
238+
// DeleteStorageClass
239+
// @router /api/delete-storageclass [post]
240+
func (c *ApiController) DeleteStorageClass() {
241+
cfg := getAdminRestConfig()
242+
if cfg == nil {
243+
c.ResponseError("apiserver not ready")
244+
return
245+
}
246+
var req storageClassRequest
247+
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
248+
c.ResponseError("invalid request body: " + err.Error())
249+
return
250+
}
251+
if err := object.DeleteStorageClass(cfg, req.Name); err != nil {
252+
c.ResponseError(err.Error())
253+
return
254+
}
255+
c.ResponseOk()
256+
}

object/storageclass.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package object
2+
3+
import (
4+
"context"
5+
6+
storagev1 "k8s.io/api/storage/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/client-go/rest"
9+
)
10+
11+
func GetStorageClasses(cfg *rest.Config) ([]storagev1.StorageClass, error) {
12+
client, err := newClient(cfg)
13+
if err != nil {
14+
return nil, err
15+
}
16+
list, err := client.StorageV1().StorageClasses().List(context.Background(), metav1.ListOptions{})
17+
if err != nil {
18+
return nil, err
19+
}
20+
return list.Items, nil
21+
}
22+
23+
func GetStorageClass(cfg *rest.Config, name string) (*storagev1.StorageClass, error) {
24+
client, err := newClient(cfg)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return client.StorageV1().StorageClasses().Get(context.Background(), name, metav1.GetOptions{})
29+
}
30+
31+
func AddStorageClass(cfg *rest.Config, sc *storagev1.StorageClass) (*storagev1.StorageClass, error) {
32+
client, err := newClient(cfg)
33+
if err != nil {
34+
return nil, err
35+
}
36+
return client.StorageV1().StorageClasses().Create(context.Background(), sc, metav1.CreateOptions{})
37+
}
38+
39+
func UpdateStorageClass(cfg *rest.Config, sc *storagev1.StorageClass) (*storagev1.StorageClass, error) {
40+
client, err := newClient(cfg)
41+
if err != nil {
42+
return nil, err
43+
}
44+
return client.StorageV1().StorageClasses().Update(context.Background(), sc, metav1.UpdateOptions{})
45+
}
46+
47+
func DeleteStorageClass(cfg *rest.Config, name string) error {
48+
client, err := newClient(cfg)
49+
if err != nil {
50+
return err
51+
}
52+
return client.StorageV1().StorageClasses().Delete(context.Background(), name, metav1.DeleteOptions{})
53+
}

routers/router.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ func InitAPI() {
114114
beego.Router("/api/add-pvc", &controllers.ApiController{}, "POST:AddPersistentVolumeClaim")
115115
beego.Router("/api/delete-pvc", &controllers.ApiController{}, "POST:DeletePersistentVolumeClaim")
116116

117+
beego.Router("/api/get-storageclasses", &controllers.ApiController{}, "GET:GetStorageClasses")
118+
beego.Router("/api/get-storageclass", &controllers.ApiController{}, "GET:GetStorageClass")
119+
beego.Router("/api/add-storageclass", &controllers.ApiController{}, "POST:AddStorageClass")
120+
beego.Router("/api/update-storageclass", &controllers.ApiController{}, "POST:UpdateStorageClass")
121+
beego.Router("/api/delete-storageclass", &controllers.ApiController{}, "POST:DeleteStorageClass")
122+
117123
beego.Router("/api/get-daemonsets", &controllers.ApiController{}, "GET:GetDaemonSets")
118124
beego.Router("/api/get-daemonset", &controllers.ApiController{}, "GET:GetDaemonSet")
119125
beego.Router("/api/add-daemonset", &controllers.ApiController{}, "POST:AddDaemonSet")

web/src/ManagementPage.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import ServiceListPage from "./ServiceListPage";
3434
import ClusterRoleBindingListPage from "./ClusterRoleBindingListPage";
3535
import RoleBindingListPage from "./RoleBindingListPage";
3636
import PvcListPage from "./PvcListPage";
37+
import StorageClassListPage from "./StorageClassListPage";
3738
import IngressListPage from "./IngressListPage";
3839
import DaemonSetListPage from "./DaemonSetListPage";
3940
import StatefulSetListPage from "./StatefulSetListPage";
@@ -224,6 +225,7 @@ function ManagementPage(props) {
224225
Setting.getItem(<Link to="/configmaps">{i18next.t("general:ConfigMaps")}</Link>, "/configmaps"),
225226
Setting.getItem(<Link to="/secrets">{i18next.t("general:Secrets")}</Link>, "/secrets"),
226227
Setting.getItem(<Link to="/pvcs">{i18next.t("general:Persistent Volume Claims")}</Link>, "/pvcs"),
228+
Setting.getItem(<Link to="/storageclasses">{i18next.t("general:Storage Classes")}</Link>, "/storageclasses"),
227229
Setting.getItem(<Link to="/resourcequotas">{i18next.t("general:Resource Quotas")}</Link>, "/resourcequotas"),
228230
Setting.getItem(<Link to="/hpas">{i18next.t("general:Horizontal Pod Autoscaler")}</Link>, "/hpas"),
229231
]),
@@ -276,6 +278,7 @@ function ManagementPage(props) {
276278
<Route exact path="/configmaps" render={(props) => <ConfigMapListPage {...props} />} />
277279
<Route exact path="/secrets" render={(props) => <SecretListPage {...props} />} />
278280
<Route exact path="/pvcs" render={(props) => <PvcListPage {...props} />} />
281+
<Route exact path="/storageclasses" render={(props) => <StorageClassListPage {...props} />} />
279282
<Route exact path="/resourcequotas" render={(props) => <ResourceQuotaListPage {...props} />} />
280283
<Route exact path="/services" render={(props) => <ServiceListPage {...props} />} />
281284
<Route exact path="/ingresses" render={(props) => <IngressListPage {...props} />} />

0 commit comments

Comments
 (0)