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