github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/build/admission.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 "crypto/tls" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 27 "github.com/sirupsen/logrus" 28 29 admissionapi "k8s.io/api/admission/v1beta1" 30 admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 31 "k8s.io/apimachinery/pkg/api/equality" 32 meta "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/serializer" 35 prowjobv1 "k8s.io/test-infra/prow/apis/prowjobs/v1" 36 37 prowjobscheme "k8s.io/test-infra/prow/client/clientset/versioned/scheme" 38 ) 39 40 var ( 41 vscheme = runtime.NewScheme() 42 codecs = serializer.NewCodecFactory(vscheme) 43 ) 44 45 func init() { 46 prowjobscheme.AddToScheme(vscheme) 47 admissionapi.AddToScheme(vscheme) 48 admissionregistrationv1beta1.AddToScheme(vscheme) 49 } 50 51 // runServer starts an http server on :8443, responding to /validate requests 52 func runServer(cert, privateKey string) { 53 http.HandleFunc("/validate", handle) 54 s := http.Server{ 55 Addr: ":8443", 56 TLSConfig: &tls.Config{ 57 ClientAuth: tls.NoClientCert, 58 }, 59 } 60 logrus.Error(s.ListenAndServeTLS(cert, privateKey)) 61 } 62 63 const contentTypeJSON = "application/json" 64 65 // readRequest extracts the request from the AdmissionReview reader 66 func readRequest(r io.Reader, contentType string) (*admissionapi.AdmissionRequest, error) { 67 if contentType != contentTypeJSON { 68 return nil, fmt.Errorf("Content-Type=%s, expected %s", contentType, contentTypeJSON) 69 } 70 71 // Can we read the body? 72 if r == nil { 73 return nil, fmt.Errorf("no body") 74 } 75 body, err := ioutil.ReadAll(r) 76 if err != nil { 77 return nil, fmt.Errorf("read body: %v", err) 78 } 79 80 // Can we convert the body into an AdmissionReview? 81 var ar admissionapi.AdmissionReview 82 deserializer := codecs.UniversalDeserializer() 83 if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { 84 return nil, fmt.Errorf("decode body: %v", err) 85 } 86 return ar.Request, nil 87 } 88 89 // handle reads the request and writes the response 90 func handle(w http.ResponseWriter, r *http.Request) { 91 req, err := readRequest(r.Body, r.Header.Get("Content-Type")) 92 if err != nil { 93 logrus.WithError(err).Error("read") 94 } 95 96 if err := writeResponse(*req, w, onlyUpdateStatus); err != nil { 97 logrus.WithError(err).Error("write") 98 } 99 } 100 101 type decider func(admissionapi.AdmissionRequest) (*admissionapi.AdmissionResponse, error) 102 103 // writeResponse gets the response from onlyUpdateStatus and writes it to w. 104 func writeResponse(ar admissionapi.AdmissionRequest, w io.Writer, decide decider) error { 105 response, err := decide(ar) 106 if err != nil { 107 logrus.WithError(err).Error("failed decision") 108 response = &admissionapi.AdmissionResponse{ 109 Result: &meta.Status{ 110 Message: err.Error(), 111 }, 112 } 113 } 114 var result admissionapi.AdmissionReview 115 result.Response = response 116 result.Response.UID = ar.UID 117 out, err := json.Marshal(result) 118 if err != nil { 119 return fmt.Errorf("encode response: %v", err) 120 } 121 if _, err := w.Write(out); err != nil { 122 return fmt.Errorf("write response: %v", err) 123 } 124 return nil 125 } 126 127 var ( 128 allow = admissionapi.AdmissionResponse{ 129 Allowed: true, 130 } 131 reject = admissionapi.AdmissionResponse{ 132 Result: &meta.Status{ 133 Reason: meta.StatusReasonForbidden, 134 Message: "ProwJobs may only update status", 135 }, 136 } 137 ) 138 139 // onlyUpdateStatus returns the response to the request 140 func onlyUpdateStatus(req admissionapi.AdmissionRequest) (*admissionapi.AdmissionResponse, error) { 141 logger := logrus.WithFields(logrus.Fields{ 142 "resource": req.Resource, 143 "subresource": req.SubResource, 144 "name": req.Name, 145 "namespace": req.Namespace, 146 "operation": req.Operation, 147 }) 148 149 // Does this only update status? 150 if req.SubResource == "status" { 151 logrus.Info("accept status update") 152 return &allow, nil 153 } 154 155 // Otherwise, do the specs match? 156 var new prowjobv1.ProwJob 157 if _, _, err := codecs.UniversalDeserializer().Decode(req.Object.Raw, nil, &new); err != nil { 158 return nil, fmt.Errorf("decode new: %v", err) 159 } 160 var old prowjobv1.ProwJob 161 if _, _, err := codecs.UniversalDeserializer().Decode(req.OldObject.Raw, nil, &old); err != nil { 162 return nil, fmt.Errorf("decode old: %v", err) 163 } 164 if equality.Semantic.DeepEqual(old.Spec, new.Spec) { 165 logrus.Info("accept update with equivalent spec") 166 return &allow, nil // yes 167 } 168 logger.Info("reject") // no 169 return &reject, nil 170 }