github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/admission/admission_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "reflect" 25 "testing" 26 27 admissionapi "k8s.io/api/admission/v1beta1" 28 meta "k8s.io/apimachinery/pkg/apis/meta/v1" 29 30 prowjobv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 31 ) 32 33 func TestOnlyUpdateStatus(t *testing.T) { 34 cases := []struct { 35 name string 36 sub string 37 old prowjobv1.ProwJob 38 new prowjobv1.ProwJob 39 expected *admissionapi.AdmissionResponse 40 }{{ 41 name: "allow status updates", 42 sub: "status", 43 expected: &allow, 44 }, { 45 name: "reject different specs", 46 old: prowjobv1.ProwJob{ 47 Spec: prowjobv1.ProwJobSpec{ 48 MaxConcurrency: 1, 49 }, 50 }, 51 new: prowjobv1.ProwJob{ 52 Spec: prowjobv1.ProwJobSpec{ 53 MaxConcurrency: 2, 54 }, 55 }, 56 expected: &reject, 57 }, { 58 59 name: "allow changes with same spec", 60 old: prowjobv1.ProwJob{ 61 Status: prowjobv1.ProwJobStatus{ 62 State: prowjobv1.PendingState, 63 }, 64 Spec: prowjobv1.ProwJobSpec{ 65 MaxConcurrency: 2, 66 }, 67 }, 68 new: prowjobv1.ProwJob{ 69 Status: prowjobv1.ProwJobStatus{ 70 State: prowjobv1.SuccessState, 71 }, 72 Spec: prowjobv1.ProwJobSpec{ 73 MaxConcurrency: 2, 74 }, 75 }, 76 expected: &allow, 77 }, { 78 name: "allow changes with no changes", 79 old: prowjobv1.ProwJob{ 80 Spec: prowjobv1.ProwJobSpec{ 81 MaxConcurrency: 2, 82 }, 83 }, 84 new: prowjobv1.ProwJob{ 85 Spec: prowjobv1.ProwJobSpec{ 86 MaxConcurrency: 2, 87 }, 88 }, 89 expected: &allow, 90 }} 91 92 for _, tc := range cases { 93 t.Run(tc.name, func(t *testing.T) { 94 var req admissionapi.AdmissionRequest 95 var err error 96 req.SubResource = tc.sub 97 req.Object.Raw, err = json.Marshal(tc.new) 98 if err != nil { 99 t.Fatalf("encode new: %v", err) 100 } 101 req.OldObject.Raw, err = json.Marshal(tc.old) 102 if err != nil { 103 t.Fatalf("encode old: %v", err) 104 } 105 actual, err := onlyUpdateStatus(req) 106 switch { 107 case tc.expected == nil: 108 if err == nil { 109 t.Errorf("failed to receive an exception") 110 } 111 case err != nil: 112 t.Errorf("unexpected error: %v", err) 113 case !reflect.DeepEqual(actual, tc.expected): 114 t.Errorf("actual %#v != expected %#v", actual, tc.expected) 115 } 116 }) 117 } 118 } 119 120 func TestWriteResponse(t *testing.T) { 121 cases := []struct { 122 name string 123 req admissionapi.AdmissionRequest 124 resp *admissionapi.AdmissionResponse 125 respErr error 126 writeErr bool 127 expected *admissionapi.AdmissionReview 128 }{ 129 { 130 name: "include request UID in output", 131 req: admissionapi.AdmissionRequest{ 132 UID: "123", 133 }, 134 resp: &admissionapi.AdmissionResponse{}, 135 expected: &admissionapi.AdmissionReview{ 136 Response: &admissionapi.AdmissionResponse{ 137 UID: "123", 138 }, 139 }, 140 }, 141 { 142 name: "include response in output", 143 resp: &admissionapi.AdmissionResponse{ 144 Allowed: true, 145 Result: &meta.Status{ 146 Reason: meta.StatusReasonForbidden, 147 Message: "yo", 148 }, 149 }, 150 expected: &admissionapi.AdmissionReview{ 151 Response: &admissionapi.AdmissionResponse{ 152 Allowed: true, 153 Result: &meta.Status{ 154 Reason: meta.StatusReasonForbidden, 155 Message: "yo", 156 }, 157 }, 158 }, 159 }, 160 { 161 name: "create response when decision fails", 162 respErr: errors.New("hey there"), 163 expected: &admissionapi.AdmissionReview{ 164 Response: &admissionapi.AdmissionResponse{ 165 Result: &meta.Status{ 166 Message: errors.New("hey there").Error(), 167 }, 168 }, 169 }, 170 }, 171 { 172 name: "error when writing fails", 173 resp: &admissionapi.AdmissionResponse{ 174 Allowed: true, 175 }, 176 writeErr: true, 177 }, 178 } 179 180 for _, tc := range cases { 181 t.Run(tc.name, func(t *testing.T) { 182 fw := fakeWriter{ 183 w: bytes.Buffer{}, 184 err: tc.writeErr, 185 } 186 decide := func(ar admissionapi.AdmissionRequest) (*admissionapi.AdmissionResponse, error) { 187 if !reflect.DeepEqual(ar, tc.req) { 188 return nil, fmt.Errorf("request %#v != expected %#v", ar, tc.req) 189 } 190 if tc.respErr != nil { 191 return nil, tc.respErr 192 } 193 return tc.resp, nil 194 } 195 err := writeResponse(tc.req, &fw, decide) 196 switch { 197 case err != nil: 198 if tc.expected != nil { 199 t.Errorf("unexpected error: %v", err) 200 } 201 case tc.expected == nil: 202 t.Error("failed to receive error") 203 default: 204 expected, err := json.Marshal(*tc.expected) 205 if err != nil { 206 t.Fatalf("marhsal expected: %v", err) 207 } 208 if buf := fw.w.Bytes(); !reflect.DeepEqual(expected, buf) { 209 t.Errorf("actual %s != expected %s", buf, expected) 210 } 211 } 212 }) 213 } 214 } 215 216 type fakeWriter struct { 217 w bytes.Buffer 218 err bool 219 } 220 221 func (w *fakeWriter) Write(p []byte) (int, error) { 222 if w.err { 223 return 0, errors.New("injected write error") 224 } 225 return w.w.Write(p) 226 } 227 228 type fakeReader struct { 229 bytes bytes.Buffer 230 err bool 231 } 232 233 func (r *fakeReader) Read(p []byte) (int, error) { 234 if r.err { 235 return 0, errors.New("injected read error") 236 } 237 return r.bytes.Read(p) 238 } 239 240 func TestReadRequest(t *testing.T) { 241 cases := []struct { 242 name string 243 ct string 244 data *admissionapi.AdmissionReview 245 bytes []byte 246 readErr bool 247 expected *admissionapi.AdmissionRequest 248 }{ 249 { 250 name: "error on bad content type", 251 ct: "text/html", 252 }, 253 { 254 name: "error on empty body", 255 }, 256 { 257 name: "error on read error", 258 readErr: true, 259 }, 260 { 261 name: "error on decode error", 262 bytes: []byte("this is not valid json"), 263 }, 264 { 265 name: "return decoded review", 266 data: &admissionapi.AdmissionReview{ 267 Request: &admissionapi.AdmissionRequest{ 268 SubResource: "status", 269 }, 270 }, 271 expected: &admissionapi.AdmissionRequest{ 272 SubResource: "status", 273 }, 274 }, 275 } 276 277 for _, tc := range cases { 278 t.Run(tc.name, func(t *testing.T) { 279 fr := &fakeReader{ 280 bytes: *bytes.NewBuffer(tc.bytes), 281 err: tc.readErr, 282 } 283 if tc.data != nil { 284 if err := codecs.LegacyCodec(admissionapi.SchemeGroupVersion).Encode(tc.data, &fr.bytes); err != nil { 285 t.Fatalf("failed encoding: %v", err) 286 } 287 } 288 if len(tc.ct) == 0 { 289 tc.ct = contentTypeJSON 290 } 291 actual, err := readRequest(fr, tc.ct) 292 switch { 293 case actual == nil: 294 if tc.expected != nil { 295 t.Errorf("unexpected error: %v", err) 296 } 297 case tc.expected == nil: 298 t.Error("failed to receive error") 299 case !reflect.DeepEqual(actual, tc.expected): 300 t.Errorf("return %#v != expected %#v", actual, tc.expected) 301 } 302 }) 303 } 304 }