/
lb_service_admission_handler.go
183 lines (154 loc) · 6.11 KB
/
lb_service_admission_handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
Copyright 2024 DigitalOcean
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package do
import (
"context"
"fmt"
"net/http"
"github.com/digitalocean/godo"
"github.com/go-logr/logr"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
// LBServiceAdmissionHandler validates service type LB.
type LBServiceAdmissionHandler struct {
log *logr.Logger
godoClient *godo.Client
decoder *admission.Decoder
region string
clusterID string
vpcID string
}
// NewLBServiceAdmissionHandler returns a configured instance of LBServiceHandler.
func NewLBServiceAdmissionHandler(log *logr.Logger, godoClient *godo.Client) *LBServiceAdmissionHandler {
return &LBServiceAdmissionHandler{
log: log,
godoClient: godoClient,
decoder: admission.NewDecoder(runtime.NewScheme()),
}
}
// Handle handles admissions requests for load balancer services.
func (h *LBServiceAdmissionHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
resp := h.handle(ctx, req)
logFields := []any{"object_name", req.Name, "object_namespace", req.Namespace, "object_kind", req.Kind.String()}
if resp.Allowed {
h.log.V(2).Info("allowing admission request", logFields...)
} else {
h.log.Info("rejecting admission request", append(logFields, "reason", resp.Result.Message)...)
}
return resp
}
func (h *LBServiceAdmissionHandler) handle(ctx context.Context, req admission.Request) admission.Response {
var svc corev1.Service
err := h.decoder.Decode(req, &svc)
if err != nil {
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("failed to decode admission request: %s", err))
}
if svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
return admission.Allowed("allowing service that is not a load balancer")
}
if svc.DeletionTimestamp != nil {
return admission.Allowed("allowing service that is being deleted")
}
lbID := svc.Annotations[annDOLoadBalancerID]
lbReq, err := h.buildLoadBalancerRequest(&svc)
if err != nil {
return admission.Denied(fmt.Sprintf("failed to build DO API request: %s", err))
}
var resp admission.Response
switch {
case req.OldObject.Raw != nil:
resp = h.validateUpdate(ctx, req, lbID, lbReq)
default:
resp = h.validateCreate(ctx, lbReq)
}
return resp
}
func (h *LBServiceAdmissionHandler) validateUpdate(ctx context.Context, req admission.Request, lbID string, lbReq *godo.LoadBalancerRequest) admission.Response {
var oldSvc corev1.Service
if err := h.decoder.DecodeRaw(req.OldObject, &oldSvc); err != nil {
return admission.Errored(http.StatusBadRequest, fmt.Errorf("failed to decode old object: %s", err))
}
// We ignore errors when building the old service's godo request because
// it is allowed to be wrong. In cases where it errors, it can potentially
// get fixed after the update.
oldReq, _ := h.buildLoadBalancerRequest(&oldSvc)
if cmp.Equal(oldReq, lbReq) {
return admission.Allowed("new service has irrelevant changes")
}
// We prefer the new LB ID if it is set. If not, we fallback to the old
// service's LB ID. If the old and new entities don't have an LB id, we
// fallback to the creation validation.
reqLbID := lbID
if reqLbID == "" {
reqLbID = oldSvc.Annotations[annDOLoadBalancerID]
}
if reqLbID == "" {
return h.validateCreate(ctx, lbReq)
}
_, resp, err := h.godoClient.LoadBalancers.Update(ctx, reqLbID, lbReq)
return h.mapGodoRespToAdmissionResp(resp, err)
}
func (h *LBServiceAdmissionHandler) validateCreate(ctx context.Context, lbReq *godo.LoadBalancerRequest) admission.Response {
_, resp, err := h.godoClient.LoadBalancers.Create(ctx, lbReq)
return h.mapGodoRespToAdmissionResp(resp, err)
}
func (h *LBServiceAdmissionHandler) buildLoadBalancerRequest(svc *corev1.Service) (*godo.LoadBalancerRequest, error) {
lbReq, err := buildLoadBalancerRequest(svc)
if err != nil {
return nil, fmt.Errorf("failed to build base load balancer request: %s", err)
}
lbReq.ValidateOnly = true
lbReq.Region = h.region
lbReq.VPCUUID = h.vpcID
if h.clusterID != "" {
lbReq.Tags = []string{buildK8sTag(h.clusterID)}
}
return lbReq, nil
}
// mapGodoRespToAdmissionResp converts godo responses to admission responses. The returned admission response
// has to be permissive enough to allow validation requests when unexpected errors happen. Webhook definitions
// with a failure policy set to `Ignore` will reject `admission.Errored(...)` and `admission.Denied(...)` responses.
func (h *LBServiceAdmissionHandler) mapGodoRespToAdmissionResp(resp *godo.Response, err error) admission.Response {
switch {
case err == nil:
return admission.Allowed("valid load balancer definition")
case resp == nil:
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("failed to get a response from DO API: %s", err))
case resp.StatusCode < 400:
return admission.Allowed("valid load balancer definition")
case resp.StatusCode < 500:
return admission.Denied(fmt.Sprintf("invalid load balancer definition: %s", err))
default:
return admission.Allowed(fmt.Sprintf("received unexpected status code (%d) from DO API, allowing to prevent blocking: %s", resp.StatusCode, err))
}
}
// WithRegion sets the region field of the handler.
func (a *LBServiceAdmissionHandler) WithRegion() error {
region, err := dropletRegion(a.godoClient.Regions)
if err != nil {
return err
}
a.region = region
return nil
}
// WithVPCID sets the vpcID field of the handler.
func (a *LBServiceAdmissionHandler) WithVPCID(vpcID string) {
a.vpcID = vpcID
}
// WithClusterID sets the clusterID field of the handler.
func (a *LBServiceAdmissionHandler) WithClusterID(clusterID string) {
a.clusterID = clusterID
}