istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/webhooks/validation/server/server.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package server 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "net/http" 23 24 multierror "github.com/hashicorp/go-multierror" 25 admissionv1 "k8s.io/api/admission/v1" 26 kubeApiAdmissionv1beta1 "k8s.io/api/admission/v1beta1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/runtime/serializer" 30 31 "istio.io/istio/pilot/pkg/config/kube/crd" 32 "istio.io/istio/pkg/config/constants" 33 "istio.io/istio/pkg/config/schema/collection" 34 "istio.io/istio/pkg/config/schema/resource" 35 "istio.io/istio/pkg/config/validation" 36 "istio.io/istio/pkg/kube" 37 "istio.io/istio/pkg/log" 38 ) 39 40 var scope = log.RegisterScope("validationServer", "validation webhook server") 41 42 var ( 43 runtimeScheme = runtime.NewScheme() 44 codecs = serializer.NewCodecFactory(runtimeScheme) 45 deserializer = codecs.UniversalDeserializer() 46 47 // Expect AdmissionRequest to only include these top-level field names 48 validFields = map[string]bool{ 49 "apiVersion": true, 50 "kind": true, 51 "metadata": true, 52 "spec": true, 53 "status": true, 54 } 55 ) 56 57 func init() { 58 _ = admissionv1.AddToScheme(runtimeScheme) 59 _ = kubeApiAdmissionv1beta1.AddToScheme(runtimeScheme) 60 } 61 62 // Options contains the configuration for the Istio Pilot validation 63 // admission controller. 64 type Options struct { 65 // Schemas provides a description of all configuration resources. 66 Schemas collection.Schemas 67 68 // DomainSuffix is the DNS domain suffix for Pilot CRD resources, 69 // e.g. cluster.local. 70 DomainSuffix string 71 72 // Port where the webhook is served. the number should be greater than 1024 for non-root 73 // user, because non-root user cannot bind port number less than 1024 74 // Mainly used for testing. Webhook server is started by Istiod. 75 Port uint 76 77 // Use an existing mux instead of creating our own. 78 Mux *http.ServeMux 79 } 80 81 // String produces a stringified version of the arguments for debugging. 82 func (o Options) String() string { 83 buf := &bytes.Buffer{} 84 85 _, _ = fmt.Fprintf(buf, "DomainSuffix: %s\n", o.DomainSuffix) 86 _, _ = fmt.Fprintf(buf, "Port: %d\n", o.Port) 87 88 return buf.String() 89 } 90 91 // DefaultArgs allocates an Options struct initialized with Webhook's default configuration. 92 func DefaultArgs() Options { 93 return Options{ 94 Port: 9443, 95 } 96 } 97 98 // Webhook implements the validating admission webhook for validating Istio configuration. 99 type Webhook struct { 100 // pilot 101 schemas collection.Schemas 102 domainSuffix string 103 } 104 105 // New creates a new instance of the admission webhook server. 106 func New(o Options) (*Webhook, error) { 107 if o.Mux == nil { 108 scope.Error("mux not set correctly") 109 return nil, errors.New("expected mux to be passed, but was not passed") 110 } 111 wh := &Webhook{ 112 schemas: o.Schemas, 113 domainSuffix: o.DomainSuffix, 114 } 115 116 o.Mux.HandleFunc("/validate", wh.serveValidate) 117 o.Mux.HandleFunc("/validate/", wh.serveValidate) 118 119 return wh, nil 120 } 121 122 func toAdmissionResponse(err error) *kube.AdmissionResponse { 123 return &kube.AdmissionResponse{Result: &metav1.Status{Message: err.Error()}} 124 } 125 126 type admitFunc func(*kube.AdmissionRequest) *kube.AdmissionResponse 127 128 func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { 129 var body []byte 130 if r.Body != nil { 131 if data, err := kube.HTTPConfigReader(r); err == nil { 132 body = data 133 } else { 134 http.Error(w, err.Error(), http.StatusBadRequest) 135 return 136 } 137 } 138 if len(body) == 0 { 139 reportValidationHTTPError(http.StatusBadRequest) 140 http.Error(w, "no body found", http.StatusBadRequest) 141 return 142 } 143 144 // verify the content type is accurate 145 contentType := r.Header.Get("Content-Type") 146 if contentType != "application/json" { 147 reportValidationHTTPError(http.StatusUnsupportedMediaType) 148 http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType) 149 return 150 } 151 152 var reviewResponse *kube.AdmissionResponse 153 var obj runtime.Object 154 var ar *kube.AdmissionReview 155 if out, _, err := deserializer.Decode(body, nil, obj); err != nil { 156 reviewResponse = toAdmissionResponse(fmt.Errorf("could not decode body: %v", err)) 157 } else { 158 ar, err = kube.AdmissionReviewKubeToAdapter(out) 159 if err != nil { 160 reviewResponse = toAdmissionResponse(fmt.Errorf("could not decode object: %v", err)) 161 } else { 162 reviewResponse = admit(ar.Request) 163 } 164 } 165 166 response := kube.AdmissionReview{} 167 response.Response = reviewResponse 168 var responseKube runtime.Object 169 var apiVersion string 170 if ar != nil { 171 apiVersion = ar.APIVersion 172 response.TypeMeta = ar.TypeMeta 173 if response.Response != nil { 174 if ar.Request != nil { 175 response.Response.UID = ar.Request.UID 176 } 177 } 178 } 179 responseKube = kube.AdmissionReviewAdapterToKube(&response, apiVersion) 180 resp, err := json.Marshal(responseKube) 181 if err != nil { 182 reportValidationHTTPError(http.StatusInternalServerError) 183 http.Error(w, fmt.Sprintf("could encode response: %v", err), http.StatusInternalServerError) 184 return 185 } 186 if _, err := w.Write(resp); err != nil { 187 reportValidationHTTPError(http.StatusInternalServerError) 188 http.Error(w, fmt.Sprintf("could write response: %v", err), http.StatusInternalServerError) 189 } 190 } 191 192 func (wh *Webhook) serveValidate(w http.ResponseWriter, r *http.Request) { 193 serve(w, r, wh.validate) 194 } 195 196 func (wh *Webhook) validate(request *kube.AdmissionRequest) *kube.AdmissionResponse { 197 isDryRun := request.DryRun != nil && *request.DryRun 198 addDryRunMessageIfNeeded := func(errStr string) error { 199 err := fmt.Errorf("%s", errStr) 200 if isDryRun { 201 err = fmt.Errorf("%s (dry run)", err) 202 } 203 return err 204 } 205 switch request.Operation { 206 case kube.Create, kube.Update: 207 default: 208 scope.Warnf("Unsupported webhook operation %v", addDryRunMessageIfNeeded(request.Operation)) 209 reportValidationFailed(request, reasonUnsupportedOperation, isDryRun) 210 return &kube.AdmissionResponse{Allowed: true} 211 } 212 213 var obj crd.IstioKind 214 if err := json.Unmarshal(request.Object.Raw, &obj); err != nil { 215 scope.Infof("cannot decode configuration: %v", addDryRunMessageIfNeeded(err.Error())) 216 reportValidationFailed(request, reasonYamlDecodeError, isDryRun) 217 return toAdmissionResponse(fmt.Errorf("cannot decode configuration: %v", err)) 218 } 219 220 gvk := obj.GroupVersionKind() 221 222 s, exists := wh.schemas.FindByGroupVersionAliasesKind(resource.FromKubernetesGVK(&gvk)) 223 if !exists { 224 scope.Infof("unrecognized type %v", addDryRunMessageIfNeeded(obj.GroupVersionKind().String())) 225 reportValidationFailed(request, reasonUnknownType, isDryRun) 226 return toAdmissionResponse(fmt.Errorf("unrecognized type %v", obj.GroupVersionKind())) 227 } 228 229 out, err := crd.ConvertObject(s, &obj, wh.domainSuffix) 230 if err != nil { 231 scope.Infof("error decoding configuration: %v", addDryRunMessageIfNeeded(err.Error())) 232 reportValidationFailed(request, reasonCRDConversionError, isDryRun) 233 return toAdmissionResponse(fmt.Errorf("error decoding configuration: %v", err)) 234 } 235 236 warnings, err := s.ValidateConfig(*out) 237 if err != nil { 238 if _, f := out.Annotations[constants.AlwaysReject]; !f { 239 // Hide error message if it was intentionally rejected (by our own internal call) 240 scope.Infof("configuration is invalid: %v", addDryRunMessageIfNeeded(err.Error())) 241 } 242 reportValidationFailed(request, reasonInvalidConfig, isDryRun) 243 return toAdmissionResponse(fmt.Errorf("configuration is invalid: %v", err)) 244 } 245 246 if reason, err := checkFields(request.Object.Raw, request.Kind.Kind, request.Namespace, obj.Name); err != nil { 247 reportValidationFailed(request, reason, isDryRun) 248 return toAdmissionResponse(err) 249 } 250 251 reportValidationPass(request) 252 return &kube.AdmissionResponse{Allowed: true, Warnings: toKubeWarnings(warnings)} 253 } 254 255 func toKubeWarnings(warn validation.Warning) []string { 256 if warn == nil { 257 return nil 258 } 259 me, ok := warn.(*multierror.Error) 260 if ok { 261 res := []string{} 262 for _, e := range me.Errors { 263 res = append(res, e.Error()) 264 } 265 return res 266 } 267 return []string{warn.Error()} 268 } 269 270 func checkFields(raw []byte, kind string, namespace string, name string) (string, error) { 271 trial := make(map[string]json.RawMessage) 272 if err := json.Unmarshal(raw, &trial); err != nil { 273 scope.Infof("cannot decode configuration fields: %v", err) 274 return reasonYamlDecodeError, fmt.Errorf("cannot decode configuration fields: %v", err) 275 } 276 277 for key := range trial { 278 if _, ok := validFields[key]; !ok { 279 scope.Infof("unknown field %q on %s resource %s/%s", 280 key, kind, namespace, name) 281 return reasonInvalidConfig, fmt.Errorf("unknown field %q on %s resource %s/%s", 282 key, kind, namespace, name) 283 } 284 } 285 286 return "", nil 287 } 288 289 // validatePort checks that the network port is in range 290 func validatePort(port int) error { 291 if 1 <= port && port <= 65535 { 292 return nil 293 } 294 return fmt.Errorf("port number %d must be in the range 1..65535", port) 295 } 296 297 // Validate tests if the Options has valid params. 298 func (o Options) Validate() error { 299 var errs *multierror.Error 300 if err := validatePort(int(o.Port)); err != nil { 301 errs = multierror.Append(errs, err) 302 } 303 return errs.ErrorOrNil() 304 }