Skip to content

Commit cb38fba

Browse files
committed
working ver
1 parent 9ab0a3c commit cb38fba

13 files changed

Lines changed: 119 additions & 205 deletions

admission/cocoonset_validator.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"k8s.io/apimachinery/pkg/util/validation"
1212

1313
cocoonv1 "github.com/cocoonstack/cocoon-common/apis/v1"
14+
commonadmission "github.com/cocoonstack/cocoon-common/k8s/admission"
1415
"github.com/cocoonstack/cocoon-webhook/metrics"
1516
)
1617

@@ -23,23 +24,23 @@ func (s *Server) validateCocoonSet(ctx context.Context, review *admissionv1.Admi
2324
req := review.Request
2425

2526
if req.Operation != admissionv1.Create && req.Operation != admissionv1.Update {
26-
return allowResponse()
27+
return commonadmission.Allow()
2728
}
2829

2930
var cs cocoonv1.CocoonSet
3031
if err := json.Unmarshal(req.Object.Raw, &cs); err != nil {
3132
logger.Warnf(ctx, "decode cocoonset %s/%s: %v", req.Namespace, req.Name, err)
32-
return denyResponse(fmt.Sprintf("decode CocoonSet: %v", err))
33+
return commonadmission.Deny(fmt.Sprintf("decode CocoonSet: %v", err))
3334
}
3435

3536
if errs := validateCocoonSetSpec(&cs); len(errs) > 0 {
3637
msg := "cocoon-webhook: invalid CocoonSet spec: " + strings.Join(errs, "; ")
3738
logger.Warnf(ctx, "validate %s/%s DENY: %s", req.Namespace, req.Name, msg)
3839
metrics.RecordAdmission(metrics.HandlerCocoonSetValid, metrics.DecisionDeny)
39-
return denyResponse(msg)
40+
return commonadmission.Deny(msg)
4041
}
4142
metrics.RecordAdmission(metrics.HandlerCocoonSetValid, metrics.DecisionAllow)
42-
return allowResponse()
43+
return commonadmission.Allow()
4344
}
4445

4546
// validateCocoonSetSpec returns the list of human-readable error

admission/pod_mutator.go

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ package admission
33
import (
44
"context"
55
"encoding/json"
6-
"fmt"
7-
"strings"
86

97
"github.com/projecteru2/core/log"
108
admissionv1 "k8s.io/api/admission/v1"
119
corev1 "k8s.io/api/core/v1"
1210

11+
commonadmission "github.com/cocoonstack/cocoon-common/k8s/admission"
1312
"github.com/cocoonstack/cocoon-common/meta"
1413
"github.com/cocoonstack/cocoon-webhook/affinity"
1514
"github.com/cocoonstack/cocoon-webhook/metrics"
@@ -26,33 +25,33 @@ func (s *Server) mutatePod(ctx context.Context, review *admissionv1.AdmissionRev
2625

2726
if req.Kind.Kind != "Pod" {
2827
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionAllow)
29-
return allowResponse()
28+
return commonadmission.Allow()
3029
}
3130

3231
var pod corev1.Pod
3332
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
3433
logger.Warnf(ctx, "decode pod %s/%s: %v", req.Namespace, req.Name, err)
3534
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionError)
36-
return allowResponse()
35+
return commonadmission.Allow()
3736
}
3837

3938
if !meta.HasCocoonToleration(pod.Spec.Tolerations) {
4039
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionAllow)
41-
return allowResponse()
40+
return commonadmission.Allow()
4241
}
4342

4443
if meta.IsOwnedByCocoonSet(pod.OwnerReferences) {
4544
// CocoonSet-managed pods come pre-annotated by the operator.
4645
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionAllow)
47-
return allowResponse()
46+
return commonadmission.Allow()
4847
}
4948

5049
if pod.Spec.NodeName != "" {
5150
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionAllow)
52-
return allowResponse()
51+
return commonadmission.Allow()
5352
}
5453

55-
pool := podNodePool(&pod)
54+
pool := meta.PodNodePool(&pod)
5655
name := podDisplayName(&pod, req)
5756
res, err := s.store.Reserve(ctx, affinity.ReserveRequest{
5857
Pool: pool,
@@ -65,15 +64,15 @@ func (s *Server) mutatePod(ctx context.Context, review *admissionv1.AdmissionRev
6564
// unreachable: log loudly and let the pod through unmutated.
6665
logger.Errorf(ctx, err, "reserve affinity for pod %s/%s", req.Namespace, name)
6766
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionAffinityFailed)
68-
return allowResponse()
67+
return commonadmission.Allow()
6968
}
7069
metrics.RecordReservation(pool)
7170

7271
patch, err := buildMutatePatch(&pod, res)
7372
if err != nil {
7473
logger.Errorf(ctx, err, "build mutate patch for pod %s/%s", req.Namespace, name)
7574
metrics.RecordAdmission(metrics.HandlerMutate, metrics.DecisionError)
76-
return allowResponse()
75+
return commonadmission.Allow()
7776
}
7877

7978
logger.Infof(ctx, "mutate %s/%s: vm=%s node=%s", req.Namespace, name, res.VMName, res.Node)
@@ -101,63 +100,28 @@ func podDisplayName(pod *corev1.Pod, req *admissionv1.AdmissionRequest) string {
101100
return pod.GenerateName + "<unnamed>"
102101
}
103102

104-
// podNodePool returns the cocoon pool the pod requests. Resolution
105-
// order: nodeSelector[cocoonstack.io/pool] -> labels[cocoonstack.io/pool]
106-
// -> annotations[cocoonstack.io/pool] -> default.
107-
func podNodePool(pod *corev1.Pod) string {
108-
if v := pod.Spec.NodeSelector[meta.LabelNodePool]; v != "" {
109-
return v
110-
}
111-
if v := pod.Labels[meta.LabelNodePool]; v != "" {
112-
return v
113-
}
114-
if v := pod.Annotations[meta.LabelNodePool]; v != "" {
115-
return v
116-
}
117-
return meta.DefaultNodePool
118-
}
119-
120-
// jsonPatchOp is a single RFC 6902 patch operation.
121-
type jsonPatchOp struct {
122-
Op string `json:"op"`
123-
Path string `json:"path"`
124-
Value any `json:"value,omitempty"`
125-
}
126-
127103
// buildMutatePatch produces an RFC 6902 JSON patch that writes the
128104
// VM name annotation and (when present) pins spec.nodeName.
129105
func buildMutatePatch(pod *corev1.Pod, res affinity.Reservation) ([]byte, error) {
130-
var ops []jsonPatchOp
106+
var ops []commonadmission.JSONPatchOp
131107
if pod.Annotations == nil {
132-
ops = append(ops, jsonPatchOp{
108+
ops = append(ops, commonadmission.JSONPatchOp{
133109
Op: "add",
134110
Path: "/metadata/annotations",
135111
Value: map[string]string{},
136112
})
137113
}
138-
ops = append(ops, jsonPatchOp{
114+
ops = append(ops, commonadmission.JSONPatchOp{
139115
Op: "add",
140-
Path: "/metadata/annotations/" + escapeJSONPointer(meta.AnnotationVMName),
116+
Path: "/metadata/annotations/" + commonadmission.EscapeJSONPointer(meta.AnnotationVMName),
141117
Value: res.VMName,
142118
})
143119
if res.Node != "" {
144-
ops = append(ops, jsonPatchOp{
120+
ops = append(ops, commonadmission.JSONPatchOp{
145121
Op: "add",
146122
Path: "/spec/nodeName",
147123
Value: res.Node,
148124
})
149125
}
150-
out, err := json.Marshal(ops)
151-
if err != nil {
152-
return nil, fmt.Errorf("marshal patch: %w", err)
153-
}
154-
return out, nil
155-
}
156-
157-
// escapeJSONPointer escapes the two characters that are reserved in
158-
// RFC 6901 JSON Pointer paths.
159-
func escapeJSONPointer(s string) string {
160-
s = strings.ReplaceAll(s, "~", "~0")
161-
s = strings.ReplaceAll(s, "/", "~1")
162-
return s
126+
return commonadmission.MarshalPatch(ops)
163127
}

admission/pod_mutator_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/client-go/kubernetes/fake"
1313

14+
commonadmission "github.com/cocoonstack/cocoon-common/k8s/admission"
1415
"github.com/cocoonstack/cocoon-common/meta"
1516
"github.com/cocoonstack/cocoon-webhook/affinity"
1617
)
@@ -57,7 +58,7 @@ func TestPodNodePoolPrecedence(t *testing.T) {
5758
for _, c := range cases {
5859
t.Run(c.name, func(t *testing.T) {
5960
pod := c.pod
60-
if got := podNodePool(&pod); got != c.want {
61+
if got := meta.PodNodePool(&pod); got != c.want {
6162
t.Errorf("got %q, want %q", got, c.want)
6263
}
6364
})
@@ -71,7 +72,7 @@ func TestEscapeJSONPointer(t *testing.T) {
7172
"plain": "plain",
7273
}
7374
for in, want := range cases {
74-
if got := escapeJSONPointer(in); got != want {
75+
if got := commonadmission.EscapeJSONPointer(in); got != want {
7576
t.Errorf("escape %q = %q, want %q", in, got, want)
7677
}
7778
}
@@ -197,7 +198,7 @@ func newTestServer(t *testing.T) *Server {
197198
t.Helper()
198199
client := fake.NewSimpleClientset()
199200
store := affinity.NewConfigMapStore(client, fixedNodePicker("node-test"))
200-
return NewServer(client, store)
201+
return NewServer(store)
201202
}
202203

203204
func buildPodReview(t *testing.T, pod *corev1.Pod) *admissionv1.AdmissionReview {

admission/response.go

Lines changed: 0 additions & 23 deletions
This file was deleted.

admission/server.go

Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
package admission
22

33
import (
4-
"context"
5-
"encoding/json"
6-
"io"
74
"net/http"
85

9-
"github.com/projecteru2/core/log"
10-
admissionv1 "k8s.io/api/admission/v1"
11-
"k8s.io/client-go/kubernetes"
12-
6+
commonadmission "github.com/cocoonstack/cocoon-common/k8s/admission"
137
"github.com/cocoonstack/cocoon-webhook/affinity"
148
)
159

16-
const maxAdmissionBody = 10 << 20 // 10 MiB upper bound on request body size.
17-
1810
// Server hosts the admission webhook HTTP handlers. Dependencies are
1911
// injected so each handler stays trivially testable.
2012
type Server struct {
21-
clientset kubernetes.Interface
22-
store affinity.Store
13+
store affinity.Store
2314
}
2415

2516
// NewServer constructs a Server with the supplied dependencies.
26-
func NewServer(clientset kubernetes.Interface, store affinity.Store) *Server {
27-
return &Server{clientset: clientset, store: store}
17+
func NewServer(store affinity.Store) *Server {
18+
return &Server{store: store}
2819
}
2920

3021
// Routes returns the HTTP handler exposing every webhook endpoint.
22+
// Decode / dispatch / encode for the admission endpoints lives in
23+
// cocoon-common/k8s/admission; the per-endpoint handlers live in
24+
// this package.
3125
func (s *Server) Routes() http.Handler {
3226
mux := http.NewServeMux()
3327
mux.HandleFunc("/mutate", s.handleMutate)
@@ -49,53 +43,13 @@ func (s *Server) handleReadyz(w http.ResponseWriter, _ *http.Request) {
4943
}
5044

5145
func (s *Server) handleMutate(w http.ResponseWriter, r *http.Request) {
52-
s.serveAdmission(w, r, s.mutatePod)
46+
commonadmission.Serve(w, r, 0, s.mutatePod)
5347
}
5448

5549
func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) {
56-
s.serveAdmission(w, r, s.validateWorkload)
50+
commonadmission.Serve(w, r, 0, s.validateWorkload)
5751
}
5852

5953
func (s *Server) handleValidateCocoonSet(w http.ResponseWriter, r *http.Request) {
60-
s.serveAdmission(w, r, s.validateCocoonSet)
61-
}
62-
63-
// serveAdmission decodes an AdmissionReview, dispatches it to the
64-
// supplied admission function, copies the request UID onto the
65-
// response (required by the API server), and writes the response.
66-
func (s *Server) serveAdmission(
67-
w http.ResponseWriter,
68-
r *http.Request,
69-
admit func(context.Context, *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse,
70-
) {
71-
logger := log.WithFunc("serveAdmission")
72-
review, err := decodeAdmissionReview(r)
73-
if err != nil {
74-
logger.Warnf(r.Context(), "decode admission review: %v", err)
75-
http.Error(w, "decode admission review", http.StatusBadRequest)
76-
return
77-
}
78-
resp := admit(r.Context(), review)
79-
if resp == nil {
80-
resp = allowResponse()
81-
}
82-
resp.UID = review.Request.UID
83-
review.Response = resp
84-
85-
out, err := json.Marshal(review)
86-
if err != nil {
87-
logger.Error(r.Context(), err, "marshal admission review")
88-
http.Error(w, "encode response", http.StatusInternalServerError)
89-
return
90-
}
91-
w.Header().Set("Content-Type", "application/json")
92-
_, _ = w.Write(out) //nolint:gosec // marshaled JSON API response, not rendered as HTML
93-
}
94-
95-
func decodeAdmissionReview(r *http.Request) (*admissionv1.AdmissionReview, error) {
96-
var review admissionv1.AdmissionReview
97-
if err := json.NewDecoder(io.LimitReader(r.Body, maxAdmissionBody)).Decode(&review); err != nil {
98-
return nil, err
99-
}
100-
return &review, nil
54+
commonadmission.Serve(w, r, 0, s.validateCocoonSet)
10155
}

admission/workload_validator.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
corev1 "k8s.io/api/core/v1"
1212
"k8s.io/utils/ptr"
1313

14+
commonadmission "github.com/cocoonstack/cocoon-common/k8s/admission"
1415
"github.com/cocoonstack/cocoon-common/meta"
1516
"github.com/cocoonstack/cocoon-webhook/metrics"
1617
)
@@ -32,15 +33,15 @@ type scalable interface {
3233
func (s *Server) validateWorkload(ctx context.Context, review *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
3334
req := review.Request
3435
if req.Operation != admissionv1.Update {
35-
return allowResponse()
36+
return commonadmission.Allow()
3637
}
3738
switch req.Kind.Kind {
3839
case "Deployment":
3940
return validateScaleDown[appsv1.Deployment](ctx, req)
4041
case "StatefulSet":
4142
return validateScaleDown[appsv1.StatefulSet](ctx, req)
4243
default:
43-
return allowResponse()
44+
return commonadmission.Allow()
4445
}
4546
}
4647

@@ -51,15 +52,15 @@ func validateScaleDown[T scalable](ctx context.Context, req *admissionv1.Admissi
5152
var oldObj, newObj T
5253
if err := json.Unmarshal(req.OldObject.Raw, &oldObj); err != nil {
5354
logger.Warnf(ctx, "decode old %s %s/%s: %v", req.Kind.Kind, req.Namespace, req.Name, err)
54-
return allowResponse()
55+
return commonadmission.Allow()
5556
}
5657
if err := json.Unmarshal(req.Object.Raw, &newObj); err != nil {
5758
logger.Warnf(ctx, "decode new %s %s/%s: %v", req.Kind.Kind, req.Namespace, req.Name, err)
58-
return allowResponse()
59+
return commonadmission.Allow()
5960
}
6061

6162
if !meta.HasCocoonToleration(workloadTolerations(&newObj)) {
62-
return allowResponse()
63+
return commonadmission.Allow()
6364
}
6465
return checkScaleDown(ctx, req, workloadReplicas(&oldObj), workloadReplicas(&newObj))
6566
}
@@ -94,13 +95,13 @@ func workloadTolerations[T scalable](obj *T) []corev1.Toleration {
9495
func checkScaleDown(ctx context.Context, req *admissionv1.AdmissionRequest, oldReplicas, newReplicas int32) *admissionv1.AdmissionResponse {
9596
if newReplicas >= oldReplicas {
9697
metrics.RecordAdmission(metrics.HandlerValidate, metrics.DecisionAllow)
97-
return allowResponse()
98+
return commonadmission.Allow()
9899
}
99100
msg := fmt.Sprintf(
100101
"cocoon-webhook: scale-down blocked for cocoon %s %s/%s (%d -> %d). "+
101102
"Use a CocoonHibernation CR to suspend individual agents.",
102103
req.Kind.Kind, req.Namespace, req.Name, oldReplicas, newReplicas)
103104
log.WithFunc("checkScaleDown").Warn(ctx, msg)
104105
metrics.RecordAdmission(metrics.HandlerValidate, metrics.DecisionDeny)
105-
return denyResponse(msg)
106+
return commonadmission.Deny(msg)
106107
}

0 commit comments

Comments
 (0)