github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/caasadmission/handler_test.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package caasadmission 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "net/http/httptest" 13 "strings" 14 15 "github.com/juju/loggo" 16 jc "github.com/juju/testing/checkers" 17 gc "gopkg.in/check.v1" 18 admission "k8s.io/api/admission/v1beta1" 19 authentication "k8s.io/api/authentication/v1" 20 core "k8s.io/api/core/v1" 21 meta "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/runtime" 23 "k8s.io/apimachinery/pkg/runtime/schema" 24 "k8s.io/apimachinery/pkg/types" 25 26 providerconst "github.com/juju/juju/caas/kubernetes/provider/constants" 27 providerutils "github.com/juju/juju/caas/kubernetes/provider/utils" 28 rbacmappertest "github.com/juju/juju/worker/caasrbacmapper/test" 29 ) 30 31 type HandlerSuite struct { 32 logger Logger 33 } 34 35 var _ = gc.Suite(&HandlerSuite{}) 36 37 func (h *HandlerSuite) SetUpTest(c *gc.C) { 38 h.logger = loggo.Logger{} 39 } 40 41 func (h *HandlerSuite) TestCompareGroupVersionKind(c *gc.C) { 42 tests := []struct { 43 A *schema.GroupVersionKind 44 B *schema.GroupVersionKind 45 ShouldMatch bool 46 }{ 47 { 48 A: &schema.GroupVersionKind{ 49 Group: admission.SchemeGroupVersion.Group, 50 Version: admission.SchemeGroupVersion.Version, 51 Kind: "AdmissionReview", 52 }, 53 B: &schema.GroupVersionKind{ 54 Group: admission.SchemeGroupVersion.Group, 55 Version: admission.SchemeGroupVersion.Version, 56 Kind: "AdmissionReview", 57 }, 58 ShouldMatch: true, 59 }, 60 { 61 A: &schema.GroupVersionKind{ 62 Group: admission.SchemeGroupVersion.Group, 63 Version: admission.SchemeGroupVersion.Version, 64 Kind: "AdmissionReview", 65 }, 66 B: &schema.GroupVersionKind{ 67 Group: admission.SchemeGroupVersion.Group, 68 Version: admission.SchemeGroupVersion.Version, 69 Kind: "Junk", 70 }, 71 ShouldMatch: false, 72 }, 73 { 74 A: &schema.GroupVersionKind{ 75 Group: admission.SchemeGroupVersion.Group, 76 Version: admission.SchemeGroupVersion.Version, 77 Kind: "AdmissionReview", 78 }, 79 B: nil, 80 ShouldMatch: false, 81 }, 82 } 83 84 for _, test := range tests { 85 c.Assert(compareGroupVersionKind(test.A, test.B), gc.Equals, test.ShouldMatch) 86 } 87 } 88 89 func (h *HandlerSuite) TestEmptyBodyFails(c *gc.C) { 90 req := httptest.NewRequest(http.MethodPost, "/", nil) 91 recorder := httptest.NewRecorder() 92 93 admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req) 94 95 c.Assert(recorder.Code, gc.Equals, http.StatusBadRequest) 96 } 97 98 func (h *HandlerSuite) TestUnknownContentType(c *gc.C) { 99 req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("junk")) 100 req.Header.Set("junk", "junk") 101 recorder := httptest.NewRecorder() 102 103 admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req) 104 105 c.Assert(recorder.Code, gc.Equals, http.StatusUnsupportedMediaType) 106 } 107 108 func (h *HandlerSuite) TestUnknownServiceAccount(c *gc.C) { 109 inReview := &admission.AdmissionReview{ 110 Request: &admission.AdmissionRequest{ 111 UID: types.UID("test"), 112 UserInfo: authentication.UserInfo{ 113 UID: "juju-tst-sa", 114 }, 115 }, 116 } 117 118 body, err := json.Marshal(inReview) 119 c.Assert(err, gc.IsNil) 120 121 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 122 req.Header.Set(HeaderContentType, ExpectedContentType) 123 recorder := httptest.NewRecorder() 124 125 admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req) 126 c.Assert(recorder.Code, gc.Equals, http.StatusOK) 127 c.Assert(recorder.Body, gc.NotNil) 128 129 outReview := admission.AdmissionReview{} 130 err = json.Unmarshal(recorder.Body.Bytes(), &outReview) 131 c.Assert(err, jc.ErrorIsNil) 132 133 c.Assert(outReview.Response.Allowed, jc.IsTrue) 134 c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID) 135 } 136 137 func (h *HandlerSuite) TestRBACMapperFailure(c *gc.C) { 138 inReview := &admission.AdmissionReview{ 139 Request: &admission.AdmissionRequest{ 140 UID: types.UID("test"), 141 UserInfo: authentication.UserInfo{ 142 UID: "juju-tst-sa", 143 }, 144 }, 145 } 146 147 body, err := json.Marshal(inReview) 148 c.Assert(err, gc.IsNil) 149 150 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 151 req.Header.Set(HeaderContentType, ExpectedContentType) 152 recorder := httptest.NewRecorder() 153 154 rbacMapper := rbacmappertest.Mapper{ 155 AppNameForServiceAccountFunc: func(_ types.UID) (string, error) { 156 return "", errors.New("test error") 157 }, 158 } 159 160 admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req) 161 c.Assert(recorder.Code, gc.Equals, http.StatusInternalServerError) 162 } 163 164 func (h *HandlerSuite) TestPatchLabelsAdd(c *gc.C) { 165 pod := core.Pod{ 166 ObjectMeta: meta.ObjectMeta{ 167 Name: "pod", 168 }, 169 } 170 podBytes, err := json.Marshal(&pod) 171 c.Assert(err, jc.ErrorIsNil) 172 173 inReview := &admission.AdmissionReview{ 174 Request: &admission.AdmissionRequest{ 175 UID: types.UID("test"), 176 UserInfo: authentication.UserInfo{ 177 UID: "juju-tst-sa", 178 }, 179 Object: runtime.RawExtension{ 180 Raw: podBytes, 181 }, 182 }, 183 } 184 185 body, err := json.Marshal(inReview) 186 c.Assert(err, jc.ErrorIsNil) 187 188 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 189 req.Header.Set(HeaderContentType, ExpectedContentType) 190 recorder := httptest.NewRecorder() 191 192 appName := "test-app" 193 rbacMapper := rbacmappertest.Mapper{ 194 AppNameForServiceAccountFunc: func(_ types.UID) (string, error) { 195 return appName, nil 196 }, 197 } 198 199 admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req) 200 c.Assert(recorder.Code, gc.Equals, http.StatusOK) 201 c.Assert(recorder.Body, gc.NotNil) 202 203 outReview := admission.AdmissionReview{} 204 err = json.Unmarshal(recorder.Body.Bytes(), &outReview) 205 c.Assert(err, jc.ErrorIsNil) 206 207 c.Assert(outReview.Response.Allowed, jc.IsTrue) 208 c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID) 209 210 patchOperations := []patchOperation{} 211 err = json.Unmarshal(outReview.Response.Patch, &patchOperations) 212 c.Assert(err, jc.ErrorIsNil) 213 214 c.Assert(len(patchOperations), gc.Equals, 2) 215 c.Assert(patchOperations[0].Op, gc.Equals, "add") 216 c.Assert(patchOperations[0].Path, gc.Equals, "/metadata/labels") 217 218 expectedLabels := providerutils.LabelForKeyValue( 219 providerconst.LabelJujuAppCreatedBy, appName) 220 221 for k, v := range expectedLabels { 222 found := false 223 for _, patchOp := range patchOperations[1:] { 224 if patchOp.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) { 225 c.Assert(patchOp.Op, gc.Equals, "add") 226 c.Assert(patchOp.Value, jc.DeepEquals, v) 227 found = true 228 break 229 } 230 } 231 c.Assert(found, jc.IsTrue) 232 } 233 234 for k, v := range expectedLabels { 235 found := false 236 for _, op := range patchOperations { 237 c.Assert(op.Op, gc.Equals, addOp) 238 if op.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) && 239 op.Value.(string) == v { 240 found = true 241 break 242 } 243 continue 244 } 245 c.Assert(found, jc.IsTrue) 246 } 247 } 248 249 func (h *HandlerSuite) TestPatchLabelsReplace(c *gc.C) { 250 pod := core.Pod{ 251 ObjectMeta: meta.ObjectMeta{ 252 Name: "pod", 253 Labels: providerutils.LabelForKeyValue( 254 providerconst.LabelJujuAppCreatedBy, "replace-app", 255 ), 256 }, 257 } 258 podBytes, err := json.Marshal(&pod) 259 c.Assert(err, jc.ErrorIsNil) 260 261 inReview := &admission.AdmissionReview{ 262 Request: &admission.AdmissionRequest{ 263 UID: types.UID("test"), 264 UserInfo: authentication.UserInfo{ 265 UID: "juju-tst-sa", 266 }, 267 Object: runtime.RawExtension{ 268 Raw: podBytes, 269 }, 270 }, 271 } 272 273 body, err := json.Marshal(inReview) 274 c.Assert(err, jc.ErrorIsNil) 275 276 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 277 req.Header.Set(HeaderContentType, ExpectedContentType) 278 recorder := httptest.NewRecorder() 279 280 appName := "test-app" 281 rbacMapper := rbacmappertest.Mapper{ 282 AppNameForServiceAccountFunc: func(_ types.UID) (string, error) { 283 return appName, nil 284 }, 285 } 286 287 admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req) 288 c.Assert(recorder.Code, gc.Equals, http.StatusOK) 289 c.Assert(recorder.Body, gc.NotNil) 290 291 outReview := admission.AdmissionReview{} 292 err = json.Unmarshal(recorder.Body.Bytes(), &outReview) 293 c.Assert(err, jc.ErrorIsNil) 294 295 c.Assert(outReview.Response.Allowed, jc.IsTrue) 296 c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID) 297 298 patchOperations := []patchOperation{} 299 err = json.Unmarshal(outReview.Response.Patch, &patchOperations) 300 c.Assert(err, jc.ErrorIsNil) 301 c.Assert(len(patchOperations), gc.Equals, 1) 302 303 expectedLabels := providerutils.LabelForKeyValue( 304 providerconst.LabelJujuAppCreatedBy, appName) 305 for k, v := range expectedLabels { 306 found := false 307 for _, patchOp := range patchOperations { 308 if patchOp.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) { 309 c.Assert(patchOp.Op, gc.Equals, "replace") 310 c.Assert(patchOp.Value, jc.DeepEquals, v) 311 found = true 312 break 313 } 314 } 315 c.Assert(found, jc.IsTrue) 316 } 317 318 for k, v := range expectedLabels { 319 found := false 320 for _, op := range patchOperations { 321 c.Assert(op.Op, gc.Equals, replaceOp) 322 if op.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) && 323 op.Value.(string) == v { 324 found = true 325 break 326 } 327 continue 328 } 329 c.Assert(found, jc.IsTrue) 330 } 331 } 332 333 func (h *HandlerSuite) TestSelfSubjectAccessReviewIgnore(c *gc.C) { 334 inReview := &admission.AdmissionReview{ 335 Request: &admission.AdmissionRequest{ 336 Kind: meta.GroupVersionKind{ 337 Group: "authorization.k8s.io", 338 Kind: "SelfSubjectAccessReview", 339 Version: "v1", 340 }, 341 UID: types.UID("test"), 342 UserInfo: authentication.UserInfo{ 343 UID: "juju-tst-sa", 344 }, 345 }, 346 } 347 348 body, err := json.Marshal(inReview) 349 c.Assert(err, jc.ErrorIsNil) 350 351 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 352 req.Header.Set(HeaderContentType, ExpectedContentType) 353 recorder := httptest.NewRecorder() 354 355 appName := "test-app" 356 rbacMapper := rbacmappertest.Mapper{ 357 AppNameForServiceAccountFunc: func(_ types.UID) (string, error) { 358 return appName, nil 359 }, 360 } 361 362 admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req) 363 c.Assert(recorder.Code, gc.Equals, http.StatusOK) 364 c.Assert(recorder.Body, gc.NotNil) 365 366 outReview := admission.AdmissionReview{} 367 err = json.Unmarshal(recorder.Body.Bytes(), &outReview) 368 c.Assert(err, jc.ErrorIsNil) 369 370 c.Assert(outReview.Response.Allowed, jc.IsTrue) 371 c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID) 372 373 c.Assert(len(outReview.Response.Patch), gc.Equals, 0) 374 }