Skip to content

Commit 0ccce10

Browse files
authored
Merge branch 'kubestellar:dev' into dev
2 parents 8697979 + ff8af73 commit 0ccce10

6 files changed

Lines changed: 340 additions & 1 deletion

File tree

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,13 @@ make fix-lint
422422
```sh
423423
make lint
424424
```
425+
426+
## extending KS-UI API through Plugins
427+
428+
### Static plugins
429+
430+
To write static plugins define your plugin type inside `/backend/plugin/plugins`, you have to implement methods to satisfy the plugin interface basically name,version,Routes methods. you can define the functionality of the plugin as you like. follow the standard of
431+
definining routes for your plugin in this form `/plugins/your-plugin-name/`.after writing all the methods (name,version,routes...) make sure to call `pm.register(your-plugin-name)` so that your routes are send to gin at start time. pm is the plugin manager (defined at `/backend/plugin/plugins/manager.go`) basically a record of all static plugins.
432+
433+
- backup plugin: currently only supporting postgres backend ,takes a snapshot of the wds on
434+
`/plugins/backup-plugin/snapshot`

backend/k8s/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package k8s
22

33
import (
44
"fmt"
5-
"k8s.io/client-go/rest"
65
"os"
76

7+
"k8s.io/client-go/rest"
8+
89
"k8s.io/client-go/dynamic"
910
"k8s.io/client-go/kubernetes"
1011
"k8s.io/client-go/tools/clientcmd"

backend/plugin/plugin.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package plugin
2+
3+
import "github.com/gin-gonic/gin"
4+
5+
// plugin interface defines methods that a KS plugin must implement
6+
type Plugin interface {
7+
// name of the plugin
8+
Name() string
9+
// version of your plugin
10+
Version() string
11+
// plugin enabled or disabled 1 for enabled 0 for disabled
12+
Enabled() int
13+
// routes and http methods to communicate with this plugin to do operations
14+
Routes() []PluginRoutesMeta
15+
}
16+
17+
// Metadata about routes of the plugin
18+
type PluginRoutesMeta struct {
19+
// http method
20+
Method string
21+
// route path
22+
Path string
23+
// route handler
24+
Handler func(c *gin.Context)
25+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package plugins
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/gin-gonic/gin"
9+
"github.com/kubestellar/ui/k8s"
10+
"github.com/kubestellar/ui/log"
11+
"github.com/kubestellar/ui/plugin"
12+
"go.uber.org/zap"
13+
v1 "k8s.io/api/batch/v1"
14+
corev1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/api/resource"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/client-go/kubernetes"
18+
)
19+
20+
var (
21+
pluginName = "backup-plugin"
22+
pluginVersion = "0.0.1"
23+
)
24+
25+
type backupPlugin struct {
26+
storageType string
27+
c *kubernetes.Clientset
28+
}
29+
30+
func (p backupPlugin) Name() string {
31+
return pluginName
32+
}
33+
34+
func (p backupPlugin) Version() string {
35+
return pluginVersion
36+
}
37+
func (p backupPlugin) Enabled() int {
38+
return 1
39+
40+
}
41+
func (p backupPlugin) Routes() []plugin.PluginRoutesMeta {
42+
43+
routes := []plugin.PluginRoutesMeta{}
44+
routes = append(routes, plugin.PluginRoutesMeta{
45+
Method: http.MethodGet,
46+
Path: "/plugins/backup-plugin/",
47+
Handler: rootHandler,
48+
})
49+
routes = append(routes, plugin.PluginRoutesMeta{
50+
Method: http.MethodGet,
51+
Path: "/plugins/backup-plugin/snapshot",
52+
Handler: takeSnapshot,
53+
})
54+
55+
return routes
56+
}
57+
58+
func rootHandler(c *gin.Context) {
59+
c.JSON(http.StatusOK, gin.H{"name": pluginName, "version": pluginVersion})
60+
}
61+
62+
// takes snapshot of the cluster
63+
func takeSnapshot(c *gin.Context) {
64+
err := freeBackupResources(bp.c)
65+
if err != nil {
66+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
67+
return
68+
}
69+
err = createBackupJob(bp.c)
70+
if err != nil {
71+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
72+
return
73+
}
74+
c.JSON(http.StatusOK, nil)
75+
}
76+
77+
var bp backupPlugin
78+
79+
func init() {
80+
//get k8s client
81+
c, _, err := k8s.GetClientSetWithContext("kind-kubeflex")
82+
if err != nil {
83+
// try with k3d
84+
c, _, err = k8s.GetClientSetWithContext("k3d-kubeflex")
85+
if err != nil {
86+
log.LogError("failed to initialized backup plugin", zap.String("error", err.Error()))
87+
return
88+
}
89+
90+
}
91+
//try for k3d if it exists
92+
93+
// currently only supporting postgr for structuredes backend
94+
bp = backupPlugin{
95+
storageType: "postgres",
96+
c: c,
97+
}
98+
// register your with plugin manager otherwise routes wont be sent to gin
99+
Pm.Register(bp)
100+
}
101+
102+
// create job that takes backup
103+
func createBackupJob(c *kubernetes.Clientset) error {
104+
105+
s, err := c.CoreV1().Secrets("kubeflex-system").Get(context.TODO(), "postgres-postgresql", metav1.GetOptions{})
106+
if err != nil {
107+
return err
108+
}
109+
password := string(s.Data["postgres-password"])
110+
err = pvc(c)
111+
if err != nil {
112+
return err
113+
}
114+
// create job
115+
var bl, ttl int32 = 3, 120
116+
j, err := c.BatchV1().Jobs("default").Create(context.TODO(), &v1.Job{
117+
TypeMeta: metav1.TypeMeta{
118+
APIVersion: "batch/v1",
119+
Kind: "job",
120+
},
121+
ObjectMeta: metav1.ObjectMeta{
122+
Name: "pg-job-ks",
123+
Namespace: "default",
124+
},
125+
Spec: v1.JobSpec{
126+
Template: corev1.PodTemplateSpec{
127+
Spec: corev1.PodSpec{
128+
Containers: []corev1.Container{
129+
corev1.Container{
130+
Name: "pg-jobc",
131+
Image: "postgres:16",
132+
Command: []string{"/bin/sh", "-c"},
133+
Args: []string{"pg_dumpall -U $user -h $host -f /mnt/backup-vol/pgdump.sql && ls /mnt/backup-vol/"},
134+
Env: []corev1.EnvVar{
135+
corev1.EnvVar{
136+
Name: "PGPASSWORD",
137+
Value: password,
138+
},
139+
corev1.EnvVar{
140+
Name: "host",
141+
Value: "postgres-postgresql.kubeflex-system.svc.cluster.local",
142+
},
143+
corev1.EnvVar{
144+
Name: "user",
145+
Value: "postgres",
146+
},
147+
},
148+
VolumeMounts: []corev1.VolumeMount{
149+
corev1.VolumeMount{
150+
Name: "backup-vol",
151+
MountPath: "/mnt/backup-vol",
152+
},
153+
},
154+
},
155+
},
156+
Volumes: []corev1.Volume{
157+
corev1.Volume{
158+
Name: "backup-vol",
159+
VolumeSource: corev1.VolumeSource{
160+
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
161+
ClaimName: "backup-vol-claim",
162+
},
163+
},
164+
},
165+
},
166+
RestartPolicy: corev1.RestartPolicyNever,
167+
},
168+
},
169+
BackoffLimit: &bl,
170+
TTLSecondsAfterFinished: &ttl,
171+
},
172+
}, metav1.CreateOptions{})
173+
174+
if err != nil {
175+
return err
176+
}
177+
log.LogInfo("Created backup job", zap.String("name", j.Name))
178+
return nil
179+
180+
}
181+
182+
func pvc(c *kubernetes.Clientset) error {
183+
storageClass := "standard"
184+
pvc, err := c.CoreV1().PersistentVolumeClaims("default").Create(context.TODO(), &corev1.PersistentVolumeClaim{
185+
ObjectMeta: metav1.ObjectMeta{
186+
Name: "backup-vol-claim",
187+
Namespace: "default",
188+
},
189+
Spec: corev1.PersistentVolumeClaimSpec{
190+
Resources: corev1.VolumeResourceRequirements{
191+
Requests: corev1.ResourceList{
192+
corev1.ResourceStorage: resource.MustParse("5Gi"),
193+
},
194+
},
195+
AccessModes: []corev1.PersistentVolumeAccessMode{
196+
corev1.ReadWriteOnce,
197+
},
198+
StorageClassName: &storageClass,
199+
},
200+
}, metav1.CreateOptions{})
201+
if err != nil {
202+
return err
203+
}
204+
log.LogInfo("created a pvc", zap.String("name", pvc.Name))
205+
return nil
206+
207+
}
208+
209+
func freeBackupResources(c *kubernetes.Clientset) error {
210+
// check if the resource exist
211+
_, err := c.CoreV1().PersistentVolumeClaims("default").Get(context.TODO(), "backup-vol-claim", metav1.GetOptions{})
212+
if err != nil {
213+
if strings.Contains(err.Error(), "not found") {
214+
return nil
215+
}
216+
return err
217+
}
218+
err = c.CoreV1().PersistentVolumeClaims("default").Delete(context.TODO(), "backup-vol-claim", *metav1.NewDeleteOptions(0))
219+
if err != nil {
220+
return err
221+
}
222+
return err
223+
}

backend/plugin/plugins/manager.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package plugins
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"sync"
7+
8+
"github.com/gin-gonic/gin"
9+
"github.com/kubestellar/ui/log"
10+
"github.com/kubestellar/ui/plugin"
11+
"go.uber.org/zap"
12+
)
13+
14+
// this file contains the plugin Manager implementation for KS
15+
// a centralized manager that handles our plugins
16+
17+
type pluginManager struct {
18+
plugins map[string]plugin.Plugin
19+
mx sync.Mutex
20+
}
21+
22+
// returns all the routes if there are any for the gin engine
23+
func (pm *pluginManager) SetupPluginsRoutes(e *gin.Engine) {
24+
pm.mx.Lock()
25+
defer pm.mx.Unlock()
26+
log.LogInfo("setting up plugin route...")
27+
for _, p := range pm.plugins {
28+
log.LogInfo(fmt.Sprintf("routes for Plugin--->%s", p.Name()))
29+
for _, r := range p.Routes() {
30+
31+
switch r.Method {
32+
case http.MethodGet:
33+
e.GET(r.Path, r.Handler)
34+
log.LogInfo("",
35+
zap.String("method", http.MethodGet),
36+
zap.String("path", r.Path))
37+
38+
case http.MethodPost:
39+
e.POST(r.Path, r.Handler)
40+
log.LogInfo("",
41+
zap.String("method", http.MethodPost),
42+
zap.String("path", r.Path),
43+
)
44+
case http.MethodDelete:
45+
e.DELETE(r.Path, r.Handler)
46+
log.LogInfo("",
47+
zap.String("method", http.MethodDelete),
48+
zap.String("path", r.Path),
49+
)
50+
case http.MethodPatch:
51+
e.PATCH(r.Path, r.Handler)
52+
log.LogInfo("",
53+
zap.String("method", http.MethodPatch),
54+
zap.String("path", r.Path))
55+
56+
}
57+
}
58+
}
59+
}
60+
61+
// registers a plugin to plugin Manager
62+
func (pm *pluginManager) Register(p plugin.Plugin) {
63+
pm.mx.Lock()
64+
defer pm.mx.Unlock()
65+
pm.plugins[p.Name()] = p
66+
log.LogInfo("registered a new plugin", zap.String("NAME", p.Name()))
67+
}
68+
69+
// deregisters a plugin to plugin manager
70+
func (pm *pluginManager) Deregister(p plugin.Plugin) {
71+
pm.mx.Lock()
72+
defer pm.mx.Unlock()
73+
delete(pm.plugins, p.Name())
74+
log.LogInfo("deregistered plugin", zap.String("NAME", p.Name()))
75+
}
76+
77+
var Pm *pluginManager = &pluginManager{plugins: map[string]plugin.Plugin{}}

backend/routes/setup.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package routes
22

33
import (
44
"github.com/gin-gonic/gin"
5+
"github.com/kubestellar/ui/plugin/plugins"
56
)
67

78
func SetupRoutes(router *gin.Engine) {
@@ -18,6 +19,8 @@ func SetupRoutes(router *gin.Engine) {
1819
setupHelmRoutes(router)
1920
setupGitHubRoutes(router)
2021
setupDeploymentHistoryRoutes(router)
22+
plugins.Pm.SetupPluginsRoutes(router)
23+
2124
setupAuthRoutes(router)
2225
setupArtifactHubRoutes(router)
2326
}

0 commit comments

Comments
 (0)