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  }