-
Notifications
You must be signed in to change notification settings - Fork 1
/
admission.go
254 lines (225 loc) · 8.24 KB
/
admission.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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/*
Copyright 2016 The Kubernetes Authors.
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 imagepolicy contains an admission controller that configures a webhook to which policy
// decisions are delegated.
package imagepolicy
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/golang/glog"
"k8s.io/api/imagepolicy/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
// install the clientgo image policy API for use with api registry
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
)
// PluginName indicates name of admission plugin.
const PluginName = "ImagePolicyWebhook"
var (
groupVersions = []schema.GroupVersion{v1alpha1.SchemeGroupVersion}
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
newImagePolicyWebhook, err := NewImagePolicyWebhook(config)
if err != nil {
return nil, err
}
return newImagePolicyWebhook, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*admission.Handler
webhook *webhook.GenericWebhook
responseCache *cache.LRUExpireCache
allowTTL time.Duration
denyTTL time.Duration
retryBackoff time.Duration
defaultAllow bool
}
var _ admission.ValidationInterface = &Plugin{}
func (a *Plugin) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration {
if status.Allowed {
return a.allowTTL
}
return a.denyTTL
}
// Filter out annotations that don't match *.image-policy.k8s.io/*
func (a *Plugin) filterAnnotations(allAnnotations map[string]string) map[string]string {
annotations := make(map[string]string)
for k, v := range allAnnotations {
if strings.Contains(k, ".image-policy.k8s.io/") {
annotations[k] = v
}
}
return annotations
}
// Function to call on webhook failure; behavior determined by defaultAllow flag
func (a *Plugin) webhookError(pod *api.Pod, attributes admission.Attributes, err error) error {
if err != nil {
glog.V(2).Infof("error contacting webhook backend: %s", err)
if a.defaultAllow {
annotations := pod.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[api.ImagePolicyFailedOpenKey] = "true"
pod.ObjectMeta.SetAnnotations(annotations)
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
return nil
}
glog.V(2).Infof("resource not allowed due to webhook backend failure ")
return admission.NewForbidden(attributes, err)
}
return nil
}
// Validate makes an admission decision based on the request attributes
func (a *Plugin) Validate(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if attributes.GetSubresource() != "" || attributes.GetResource().GroupResource() != api.Resource("pods") {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
// Build list of ImageReviewContainerSpec
var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec
containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
containers = append(containers, pod.Spec.Containers...)
containers = append(containers, pod.Spec.InitContainers...)
for _, c := range containers {
imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
Image: c.Image,
})
}
imageReview := v1alpha1.ImageReview{
Spec: v1alpha1.ImageReviewSpec{
Containers: imageReviewContainerSpecs,
Annotations: a.filterAnnotations(pod.Annotations),
Namespace: attributes.GetNamespace(),
},
}
if err := a.admitPod(pod, attributes, &imageReview); err != nil {
return admission.NewForbidden(attributes, err)
}
return nil
}
func (a *Plugin) admitPod(pod *api.Pod, attributes admission.Attributes, review *v1alpha1.ImageReview) error {
cacheKey, err := json.Marshal(review.Spec)
if err != nil {
return err
}
if entry, ok := a.responseCache.Get(string(cacheKey)); ok {
review.Status = entry.(v1alpha1.ImageReviewStatus)
} else {
result := a.webhook.WithExponentialBackoff(func() rest.Result {
return a.webhook.RestClient.Post().Body(review).Do()
})
if err := result.Error(); err != nil {
return a.webhookError(pod, attributes, err)
}
var statusCode int
if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
return a.webhookError(pod, attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
}
if err := result.Into(review); err != nil {
return a.webhookError(pod, attributes, err)
}
a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status))
}
if !review.Status.Allowed {
if len(review.Status.Reason) > 0 {
return fmt.Errorf("image policy webhook backend denied one or more images: %s", review.Status.Reason)
}
return errors.New("one or more images rejected by webhook backend")
}
return nil
}
// NewImagePolicyWebhook a new ImagePolicyWebhook plugin from the provided config file.
// The config file is specified by --admission-control-config-file and has the
// following format for a webhook:
//
// {
// "imagePolicy": {
// "kubeConfigFile": "path/to/kubeconfig/for/backend",
// "allowTTL": 30, # time in s to cache approval
// "denyTTL": 30, # time in s to cache denial
// "retryBackoff": 500, # time in ms to wait between retries
// "defaultAllow": true # determines behavior if the webhook backend fails
// }
// }
//
// The config file may be json or yaml.
//
// The kubeconfig property refers to another file in the kubeconfig format which
// specifies how to connect to the webhook backend.
//
// The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer.
//
// # clusters refers to the remote service.
// clusters:
// - name: name-of-remote-imagepolicy-service
// cluster:
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
// server: https://images.example.com/policy # URL of remote service to query. Must use 'https'.
//
// # users refers to the API server's webhook configuration.
// users:
// - name: name-of-api-server
// user:
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
// client-key: /path/to/key.pem # key matching the cert
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
if configFile == nil {
return nil, fmt.Errorf("no config specified")
}
// TODO: move this to a versioned configuration file format
var config AdmissionConfig
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
err := d.Decode(&config)
if err != nil {
return nil, err
}
whConfig := config.ImagePolicyWebhook
if err := normalizeWebhookConfig(&whConfig); err != nil {
return nil, err
}
gw, err := webhook.NewGenericWebhook(legacyscheme.Scheme, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
if err != nil {
return nil, err
}
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
webhook: gw,
responseCache: cache.NewLRUExpireCache(1024),
allowTTL: whConfig.AllowTTL,
denyTTL: whConfig.DenyTTL,
defaultAllow: whConfig.DefaultAllow,
}, nil
}