Skip to content

Commit 268b48f

Browse files
authored
feat: add webhook for change imagemanager ref which bind dev container (#13)
1 parent c23fcc2 commit 268b48f

File tree

9 files changed

+267
-3
lines changed

9 files changed

+267
-3
lines changed

cmd/devbox/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func main() {
8080
config := ctrl.GetConfigOrDie()
8181
wh := webhook.Webhook{KubeClient: kubernetes.NewForConfigOrDie(config)}
8282
runtime.Must(wh.DeleteDevContainerMutatingWebhook())
83+
runtime.Must(wh.DeleteImageManagerMutatingWebhook())
8384
},
8485
}
8586

pkg/api/server/handlers_api.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package server
33
import (
44
"errors"
55
"fmt"
6-
"github.com/go-resty/resty/v2"
76
"net/http"
87
"os"
98
"path/filepath"
@@ -17,6 +16,7 @@ import (
1716
"github.com/beclab/devbox/pkg/store/db/model"
1817

1918
"github.com/emicklei/go-restful/v3"
19+
"github.com/go-resty/resty/v2"
2020
"github.com/gofiber/fiber/v2"
2121
"github.com/google/uuid"
2222
"gopkg.in/yaml.v2"
@@ -194,6 +194,7 @@ func (h *handlers) bindContainer(ctx *fiber.Ctx) error {
194194
AppId int `json:"appId"`
195195
PodSelector string `json:"podSelector"`
196196
ContainerName string `json:"containerName"`
197+
Image string `json:"image"`
197198
DevEnv *string `json:"devEnv,omitempty"`
198199
DevContainerName string `json:"devContainerName"`
199200
}
@@ -279,6 +280,7 @@ func (h *handlers) bindContainer(ctx *fiber.Ctx) error {
279280
ContainerID: uint(containerId),
280281
PodSelector: postData.PodSelector,
281282
ContainerName: postData.ContainerName,
283+
Image: postData.Image,
282284
}
283285

284286
err = h.db.DB.Create(&appContainer).Error

pkg/api/server/handlers_webhook.go

+45-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package server
33
import (
44
"context"
55
"fmt"
6-
7-
"github.com/beclab/devbox/pkg/webhook"
86

7+
"github.com/beclab/devbox/pkg/webhook"
8+
99
"github.com/gofiber/fiber/v2"
1010
"github.com/google/uuid"
1111
admissionv1 "k8s.io/api/admission/v1"
@@ -87,3 +87,46 @@ func (h *webhooks) mutate(ctx context.Context, req *admissionv1.AdmissionRequest
8787
return resp
8888

8989
}
90+
91+
func (h *webhooks) imageManager(ctx *fiber.Ctx) error {
92+
klog.Infof("Received mutating webhook request: Method=%v, URL=%v", ctx.Method(), ctx.OriginalURL())
93+
admissionRequestBody := ctx.BodyRaw()
94+
if len(admissionRequestBody) == 0 {
95+
klog.Error("Error reading admission request body, body is empty")
96+
return fiber.NewError(fiber.StatusBadRequest, "empty request admission request body")
97+
}
98+
var admissionReq, admissionResp admissionv1.AdmissionReview
99+
proxyUUID := uuid.New()
100+
if _, _, err := webhook.Deserializer.Decode(admissionRequestBody, nil, &admissionReq); err != nil {
101+
klog.Error("Error decoding admission request body, ", err)
102+
admissionResp.Response = h.webhook.AdmissionError(err)
103+
} else {
104+
admissionResp.Response = h.imageManagerMutate(ctx.Context(), admissionReq.Request, proxyUUID)
105+
}
106+
107+
admissionResp.TypeMeta = admissionReq.TypeMeta
108+
admissionResp.Kind = admissionReq.Kind
109+
110+
return ctx.JSON(&admissionResp)
111+
}
112+
113+
func (h *webhooks) imageManagerMutate(ctx context.Context, req *admissionv1.AdmissionRequest, proxyUUID uuid.UUID) *admissionv1.AdmissionResponse {
114+
if req == nil {
115+
klog.Error("nil admission Request")
116+
return h.webhook.AdmissionError(errNilAdmissionRequest)
117+
}
118+
resp := &admissionv1.AdmissionResponse{
119+
Allowed: true,
120+
UID: req.UID,
121+
}
122+
123+
klog.Info("Creating patch for resource ", req.Resource)
124+
patchBytes, err := h.webhook.MutateIm(ctx, req.Object.Raw, proxyUUID)
125+
if err != nil {
126+
return h.webhook.AdmissionError(err)
127+
}
128+
if len(patchBytes) > 0 {
129+
h.webhook.PatchAdmissionResponse(resp, patchBytes)
130+
}
131+
return resp
132+
}

pkg/api/server/server.go

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func NewServer(db *db.DbOperator) *server {
3636
DB: db,
3737
}
3838
utilruntime.Must(webhook.CreateOrUpdateDevContainerMutatingWebhook())
39+
utilruntime.Must(webhook.CreateOrUpdateImageManagerMutatingWebhook())
3940

4041
return &server{
4142
handlers: &handlers{db: db, kubeConfig: config},
@@ -104,6 +105,7 @@ func (s *server) Start() {
104105
// webhooks /webhook
105106
wh := webhookServer.Group("webhook")
106107
wh.Post("/devcontainer", s.webhooks.devcontainer)
108+
wh.Post("/imagemanager", s.webhooks.imageManager)
107109

108110
klog.Info("dev box api server listening on 8088 ")
109111

pkg/store/db/model/dev_app_container.go

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type DevAppContainers struct {
88
ContainerID uint `gorm:"column:container_id" json:"containerId"`
99
PodSelector string `gorm:"type:varchar(50);column:pod_selector" json:"podSelector"`
1010
ContainerName string `gorm:"type:varchar(50);column:container_name" json:"containerName"`
11+
Image string `gorm:"type:varchar(128);column:image" json:"image"`
1112
CreateTime time.Time `gorm:"default:CURRENT_TIMESTAMP;column:create_time" json:"createTime"`
1213
UpdateTime time.Time `gorm:"default:CURRENT_TIMESTAMP;column:update_time" json:"updateTime"`
1314

pkg/store/db/model/dev_container.go

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type DevContainerInfo struct {
1919
PodSelector *string `json:"podSelector,omitempty"`
2020
AppID *int `json:"appId,omitempty"`
2121
ContainerName *string `json:"containerName,omitempty"`
22+
Image *string `json:"image,omitempty"`
2223
AppName *string `json:"appName,omitempty"`
2324
State *string `json:"state,omitempty"`
2425
DevPath *string `json:"devPath,omitempty"`

pkg/store/db/operator.go

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ func createTableIfNotExists() (err error) {
6262
if err != nil {
6363
return err
6464
}
65+
} else {
66+
if !db.Migrator().HasColumn(&model.DevAppContainers{}, "Image") {
67+
err = db.Migrator().AddColumn(&model.DevAppContainers{}, "Image")
68+
if err != nil {
69+
return err
70+
}
71+
}
6572
}
6673
return nil
6774
}

pkg/webhook/funcs.go

+60
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"path/filepath"
89
"sort"
910
"strconv"
@@ -13,14 +14,17 @@ import (
1314
"github.com/beclab/devbox/pkg/development/container"
1415
"github.com/beclab/devbox/pkg/development/envoy"
1516
"github.com/beclab/devbox/pkg/development/helm"
17+
"github.com/beclab/devbox/pkg/store/db"
1618
"github.com/beclab/devbox/pkg/store/db/model"
1719

20+
"github.com/containerd/containerd/reference/docker"
1821
"github.com/google/uuid"
1922
"gorm.io/gorm"
2023
admissionv1 "k8s.io/api/admission/v1"
2124
appsv1 "k8s.io/api/apps/v1"
2225
corev1 "k8s.io/api/core/v1"
2326
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2428
"k8s.io/apimachinery/pkg/labels"
2529
"k8s.io/client-go/kubernetes/scheme"
2630
"k8s.io/klog/v2"
@@ -493,3 +497,59 @@ func (wh *Webhook) getUserHomeDir(ctx context.Context) (string, error) {
493497
}
494498
return filepath.Join(dir, "Home"), nil
495499
}
500+
501+
func (wh *Webhook) MutateIm(ctx context.Context, raw []byte, proxyUUID uuid.UUID) (patch []byte, err error) {
502+
var obj unstructured.Unstructured
503+
if err := json.Unmarshal(raw, &obj); err != nil {
504+
klog.Errorf("Error unmarshaling request to unstructured err=%v", err)
505+
return nil, err
506+
}
507+
appName, _, _ := unstructured.NestedString(obj.Object, "spec", "appName")
508+
originAppName := strings.TrimSuffix(appName, "-dev")
509+
refs, _, _ := unstructured.NestedSlice(obj.Object, "spec", "refs")
510+
511+
sql := `select dc.id,dc.dev_env,dc.name, dc.create_time, dc.update_time, ac.pod_selector,ac.app_id, ac.container_name,ac.image,a.app_name
512+
from dev_apps a
513+
join dev_app_containers ac on a.id = ac.app_id
514+
join dev_containers dc on ac.container_id = dc.id
515+
where app_name = '%s'`
516+
sql = fmt.Sprintf(sql, originAppName)
517+
list := make([]*model.DevContainerInfo, 0)
518+
db := db.NewDbOperator()
519+
err = db.DB.Raw(sql).Scan(&list).Error
520+
if err != nil {
521+
return nil, err
522+
}
523+
klog.Infof("len(list)=%v", len(list))
524+
if len(list) == 0 {
525+
return makePatches(raw, obj.Object, appName)
526+
}
527+
528+
newRefs := make([]interface{}, 0)
529+
for _, r := range refs {
530+
name := r.(map[string]interface{})["name"].(string)
531+
pullPolicy := r.(map[string]interface{})["imagePullPolicy"].(string)
532+
image, _ := docker.ParseDockerRef(*list[0].Image)
533+
534+
devImage, _ := docker.ParseDockerRef(container.DevEnvImage(list[0].DevEnv))
535+
if name == image.String() {
536+
name = devImage.String()
537+
}
538+
newRefs = append(newRefs, map[string]interface{}{
539+
"name": name,
540+
"imagePullPolicy": pullPolicy,
541+
})
542+
}
543+
klog.Infof("newRefs: %#v", newRefs)
544+
err = unstructured.SetNestedSlice(obj.Object, newRefs, "spec", "refs")
545+
if err != nil {
546+
return nil, err
547+
}
548+
549+
patch, err = makePatches(raw, obj.Object, appName)
550+
if err != nil {
551+
klog.Infof("make Patches err=%v", err)
552+
}
553+
klog.Infof("pathc: %v", string(patch))
554+
return patch, err
555+
}

pkg/webhook/setup.go

+147
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const (
2121
defaultCaPath = "/etc/certs/ca.crt"
2222
webhookServiceName = "devbox-server"
2323
devContainerWebhookCfgName = "devcontainer-mutate-webhooks"
24+
imageManagerWebhookCfgName = "imagemanager-mutate-webhooks"
25+
imageManagerWebhookPrefix = "imagemanager-webhook"
2426
mutatingWebhookNamePrefix = "devcontainer-webhook"
2527
helmRelease = "meta.helm.sh/release-name"
2628
helmReleaseNamespace = "meta.helm.sh/release-namespace"
@@ -31,6 +33,8 @@ var (
3133
webhookServiceNamespace = &constants.Namespace
3234
webhookPath = "/webhook/devcontainer"
3335
WebhookPort int32 = 8083
36+
37+
imageManagerWebhookPath = "/webhook/imagemanager"
3438
// WebhookServerListenAddress = webhookServiceName + ":" + strconv.Itoa(int(WebhookPort))
3539

3640
// codecs is the codec factory used by the deserialzer
@@ -202,7 +206,150 @@ func (wh *Webhook) DeleteDevContainerMutatingWebhook() error {
202206
return nil
203207
}
204208

209+
func (wh *Webhook) CreateOrUpdateImageManagerMutatingWebhook() error {
210+
failurePolicy := admissionregv1.Fail
211+
matchPolicy := admissionregv1.Exact
212+
webhookTimeout := int32(30)
213+
214+
caBundle, err := os.ReadFile(defaultCaPath)
215+
if err != nil {
216+
return err
217+
}
218+
219+
mwhLabels := map[string]string{"velero.io/exclude-from-backup": "true"}
220+
mwh := admissionregv1.MutatingWebhookConfiguration{
221+
ObjectMeta: metav1.ObjectMeta{
222+
Name: imageManagerWebhookCfgName,
223+
Labels: mwhLabels,
224+
},
225+
Webhooks: []admissionregv1.MutatingWebhook{},
226+
}
227+
imwh := admissionregv1.MutatingWebhook{
228+
Name: imageManagerWebhookName(),
229+
ClientConfig: admissionregv1.WebhookClientConfig{
230+
CABundle: caBundle,
231+
Service: &admissionregv1.ServiceReference{
232+
Namespace: *webhookServiceNamespace,
233+
Name: webhookServiceName,
234+
Path: &imageManagerWebhookPath,
235+
Port: &WebhookPort,
236+
},
237+
},
238+
FailurePolicy: &failurePolicy,
239+
MatchPolicy: &matchPolicy,
240+
Rules: []admissionregv1.RuleWithOperations{
241+
{
242+
Operations: []admissionregv1.OperationType{admissionregv1.Create},
243+
Rule: admissionregv1.Rule{
244+
APIGroups: []string{"app.bytetrade.io"},
245+
APIVersions: []string{"*"},
246+
Resources: []string{"imagemanagers"},
247+
},
248+
},
249+
},
250+
ObjectSelector: &metav1.LabelSelector{
251+
MatchExpressions: []metav1.LabelSelectorRequirement{
252+
{
253+
Key: constants.DevOwnerLabel,
254+
Operator: metav1.LabelSelectorOpIn,
255+
Values: []string{constants.Owner},
256+
},
257+
},
258+
},
259+
SideEffects: func() *admissionregv1.SideEffectClass {
260+
sideEffect := admissionregv1.SideEffectClassNoneOnDryRun
261+
return &sideEffect
262+
}(),
263+
TimeoutSeconds: &webhookTimeout,
264+
AdmissionReviewVersions: []string{"v1"},
265+
}
266+
mwh.Webhooks = append(mwh.Webhooks, imwh)
267+
if _, err = wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &mwh, metav1.CreateOptions{}); err != nil {
268+
if apierrors.IsAlreadyExists(err) {
269+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
270+
existing, err := wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), mwh.Name, metav1.GetOptions{})
271+
if err != nil {
272+
klog.Error("Error getting MutatingWebhookConfiguration ", err)
273+
return err
274+
}
275+
found := false
276+
for i, w := range existing.Webhooks {
277+
if w.Name == imwh.Name {
278+
found = true
279+
existing.Webhooks[i] = imwh
280+
break
281+
}
282+
}
283+
if !found {
284+
existing.Webhooks = append(existing.Webhooks, imwh)
285+
}
286+
_, err = wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(context.TODO(), existing, metav1.UpdateOptions{})
287+
if err != nil && !apierrors.IsConflict(err) {
288+
klog.Error("Error updating MutatingWebhookConfiguration ", err)
289+
}
290+
return err
291+
})
292+
if err != nil {
293+
klog.Error("Error updating MutatingWebhookConfiguration ", err)
294+
return err
295+
}
296+
} else {
297+
klog.Error("Error creating MutatingWebhookConfiguration ", err)
298+
return err
299+
}
300+
}
301+
klog.Infof("Finished creating MutatingWebhookConfiguration %s", imageManagerWebhookCfgName)
302+
return nil
303+
}
304+
305+
func (wh *Webhook) DeleteImageManagerMutatingWebhook() error {
306+
imwhName := imageManagerWebhookName()
307+
existing, err := wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), imageManagerWebhookCfgName, metav1.GetOptions{})
308+
if err != nil {
309+
if apierrors.IsNotFound(err) {
310+
klog.Info("webhook configuration not found, ", imageManagerWebhookCfgName)
311+
return nil
312+
}
313+
return err
314+
}
315+
for i, w := range existing.Webhooks {
316+
if w.Name == imwhName {
317+
if len(existing.Webhooks) == 1 {
318+
err = wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), imageManagerWebhookCfgName, metav1.DeleteOptions{})
319+
if err != nil {
320+
klog.Info("delete webhook configuration error, ", err)
321+
return err
322+
}
323+
} else {
324+
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
325+
updating, err := wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), imageManagerWebhookCfgName, metav1.GetOptions{})
326+
if err != nil {
327+
if apierrors.IsNotFound(err) {
328+
klog.Info("webhook configuration not found, ", imageManagerWebhookCfgName)
329+
return nil
330+
}
331+
return err
332+
}
333+
updating.Webhooks = append(existing.Webhooks[:i], existing.Webhooks[i+1:]...)
334+
klog.Info("removing the webhook, ", imwhName)
335+
_, err = wh.KubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(context.Background(), updating, metav1.UpdateOptions{})
336+
if !apierrors.IsConflict(err) {
337+
klog.Error("Error updating MutatingWebhookConfiguration ", err)
338+
}
339+
return err
340+
})
341+
}
342+
}
343+
}
344+
klog.Infof("success to clean imagemanager webhook")
345+
return nil
346+
}
347+
205348
func mutatingWebhookName() string {
206349
// should be a domain with at least three segments separated by dots
207350
return mutatingWebhookNamePrefix + "." + constants.Namespace + ".ns"
208351
}
352+
353+
func imageManagerWebhookName() string {
354+
return imageManagerWebhookPrefix + "." + constants.Namespace + ".ns"
355+
}

0 commit comments

Comments
 (0)