diff --git a/cmd/webhook/config.go b/cmd/webhook/config.go new file mode 100644 index 0000000..12f47ed --- /dev/null +++ b/cmd/webhook/config.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "time" + + "github.com/hashicorp/logutils" +) + +type configStructure struct { + // time configuration + TimeFormat string + TimeZoneLocal *time.Location + TimeZoneUTC *time.Location + + // logging + LogLevel int + Log *logutils.LevelFilter + + // webserver + WebSrvPort int + WebSrvIP string + WebSrvReadTimeout int + WebSrvWriteTimeout int + WebSrvIdleTimeout int +} + +type patchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +// Set Defaults +var config = configStructure{ + TimeFormat: "2006-01-02 15:04:05", + Log: &logutils.LevelFilter{ + Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"}, + Writer: os.Stderr, + }, + WebSrvReadTimeout: 5, + WebSrvWriteTimeout: 10, + WebSrvIdleTimeout: 2, +} diff --git a/cmd/webhook/httpServer.go b/cmd/webhook/httpServer.go new file mode 100644 index 0000000..3c4fe27 --- /dev/null +++ b/cmd/webhook/httpServer.go @@ -0,0 +1,72 @@ +package main + +import ( + "log" + "net/http" + "strconv" + "strings" + "time" +) + +const InvalidMethod string = "Invalid http method." + +func httpAccessLog(req *http.Request) { + log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI) +} + +func crossSiteOrigin(w http.ResponseWriter) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, X-API-Token") +} + +func httpServer(host string, port int) { + path := http.NewServeMux() + + connection := &http.Server{ + Addr: host + ":" + strconv.FormatInt(int64(port), 10), + Handler: path, + ReadTimeout: time.Duration(config.WebSrvReadTimeout) * time.Second, + WriteTimeout: time.Duration(config.WebSrvWriteTimeout) * time.Second, + IdleTimeout: time.Duration(config.WebSrvIdleTimeout) * time.Second, + } + + // healthcheck + path.HandleFunc("/healthcheck", webHealthCheck) + // api-endpoint + path.HandleFunc("/api/v1/mutate", webMutatePod) + // web root + path.HandleFunc("/", webRoot) + + if err := connection.ListenAndServe(); err != nil { + log.Fatalf("[ERROR] %s\n", err) + } +} + +func webRoot(w http.ResponseWriter, r *http.Request) { + httpAccessLog(r) + crossSiteOrigin(w) + + switch { + case strings.ToLower(r.Method) != "get": + log.Printf("[DEBUG] Request to '/' was made using the wrong method: expected %s, got %s", "GET", strings.ToUpper(r.Method)) + tmpltError(w, http.StatusBadRequest, InvalidMethod) + case r.URL.Path != "/": + log.Printf("[DEBUG] Unable to locate requested path: '%s'", r.URL.Path) + tmpltError(w, http.StatusNotFound, "Requested path not found.") + default: + tmpltWebRoot(w) + } +} + +func webHealthCheck(w http.ResponseWriter, r *http.Request) { + httpAccessLog(r) + crossSiteOrigin(w) + + if strings.ToLower(r.Method) == "get" { + tmpltHealthCheck(w) + } else { + log.Printf("[DEBUG] Request to '/healthcheck' was made using the wrong method: expected %s, got %s", "GET", strings.ToUpper(r.Method)) + tmpltError(w, http.StatusBadRequest, InvalidMethod) + } +} diff --git a/cmd/webhook/httpServerTemplates.go b/cmd/webhook/httpServerTemplates.go new file mode 100644 index 0000000..596b9ea --- /dev/null +++ b/cmd/webhook/httpServerTemplates.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +const cT string = "Content-Type" +const cTjson string = "application/json" +const marshalErrorMsg string = "[TRACE] Unable to marshal error message: %v." + +func tmpltError(w http.ResponseWriter, s int, m string) { + var ( + output []byte + o = struct { + Error int `json:"error" yaml:"error"` + ErrorMsg string `json:"errorMessage" yaml:"errorMessage"` + }{ + Error: s, + ErrorMsg: m, + } + err error + ) + + w.Header().Add(cT, cTjson) + + output, err = json.MarshalIndent(o, "", " ") + if err != nil { + log.Printf(marshalErrorMsg, err) + } + w.WriteHeader(s) + w.Write(output) //nolint:errcheck +} + +func tmpltHealthCheck(w http.ResponseWriter) { + o := struct { + WebServer bool `json:"webServerActive" yaml:"webServerActive"` + Status string `json:"status" yaml:"status"` + }{ + WebServer: true, + Status: "healthy", + } + + output, err := json.MarshalIndent(o, "", " ") + if err != nil { + log.Printf(marshalErrorMsg, err) + } + + w.Header().Add(cT, cTjson) + w.Write(output) //nolint:errcheck +} + +func tmpltWebRoot(w http.ResponseWriter) { + o := struct { + Application string `json:"application" yaml:"application"` + Description string `json:"description" yaml:"description"` + Version string `json:"version" yaml:"version"` + }{ + Application: "Mutating-Webhook API", + Description: "Mutating Webhook for Simple Sidecar Injection", + Version: "v1.0.0", + } + w.Header().Add(cT, cTjson) + + output, err := json.MarshalIndent(o, "", " ") + if err != nil { + log.Printf(marshalErrorMsg, err) + } + w.Write(output) //nolint:errcheck +} diff --git a/cmd/webhook/init.go b/cmd/webhook/init.go new file mode 100644 index 0000000..09c6a5b --- /dev/null +++ b/cmd/webhook/init.go @@ -0,0 +1,115 @@ +package main + +import ( + "flag" + "log" + "os" + "strconv" + "time" + + "github.com/hashicorp/logutils" +) + +// getEnvString returns string from environment variable +func getEnvString(env, def string) (val string) { //nolint:deadcode + val = os.Getenv(env) + + if val == "" { + return def + } + + return +} + +// getEnvInt returns int from environment variable +func getEnvInt(env string, def int) (ret int) { + val := os.Getenv(env) + + if val == "" { + return def + } + + ret, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env) + } + + return +} + +func setLogLevel(l int) { + switch { + case l <= 20: + config.Log.SetMinLevel(logutils.LogLevel("ERROR")) + case l > 20 && l <= 40: + config.Log.SetMinLevel(logutils.LogLevel("WARNING")) + case l > 40 && l <= 60: + config.Log.SetMinLevel(logutils.LogLevel("INFO")) + case l > 60 && l <= 80: + config.Log.SetMinLevel(logutils.LogLevel("DEBUG")) + case l > 80: + config.Log.SetMinLevel(logutils.LogLevel("TRACE")) + } +} + +func initialize() { + var ( + tz string + err error + ) + + // log configuration + flag.IntVar(&config.LogLevel, + "log", + getEnvInt("LOG_LEVEL", 50), + "(LOG_LEVEL)\nlog level") + // local webserver configuration + flag.IntVar(&config.WebSrvPort, + "http-port", + getEnvInt("HTTP_PORT", 8080), + "(HTTP_PORT)\nlisten port for internal webserver") + flag.StringVar(&config.WebSrvIP, + "http-ip", + getEnvString("HTTP_IP", ""), + "(HTTP_IP)\nlisten ip for internal webserver") + flag.IntVar(&config.WebSrvReadTimeout, + "http-read-timeout", + getEnvInt("HTTP_READ_TIMEOUT", 5), + "(HTTP_READ_TIMEOUT)\ninternal http server read timeout in seconds") + flag.IntVar(&config.WebSrvWriteTimeout, + "http-write-timeout", + getEnvInt("HTTP_WRITE_TIMEOUT", 2), + "(HTTP_WRITE_TIMEOUT\ninternal http server write timeout in seconds") + flag.IntVar(&config.WebSrvIdleTimeout, + "http-idle-timeout", + getEnvInt("HTTP_IDLE_TIMEOUT", 2), + "(HTTP_IDLE_TIMEOUT)\ninternal http server idle timeout in seconds") + // timezone + flag.StringVar(&tz, + "timezone", + getEnvString("TZ", "America/Chicago"), + "(TZ)\ntimezone") + // read command line options + flag.Parse() + + // logging level + setLogLevel(config.LogLevel) + log.SetOutput(config.Log) + + // timezone configuration + config.TimeZoneUTC, _ = time.LoadLocation("UTC") + if config.TimeZoneLocal, err = time.LoadLocation(tz); err != nil { + log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones") + } + + // print current configuration + log.Printf("[DEBUG] configuration value set: LOG_LEVEL = %s\n", strconv.Itoa(config.LogLevel)) + log.Printf("[DEBUG] configuration value set: HTTP_PORT = %s\n", strconv.Itoa(config.WebSrvPort)) + log.Printf("[DEBUG] configuration value set: HTTP_IP = %s\n", config.WebSrvIP) + log.Printf("[DEBUG] configuration value set: HTTP_READ_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvReadTimeout)) + log.Printf("[DEBUG] configuration value set: HTTP_WRITE_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvWriteTimeout)) + log.Printf("[DEBUG] configuration value set: HTTP_IDLE_TIMEOUT = %s\n", strconv.Itoa(config.WebSrvIdleTimeout)) + log.Printf("[DEBUG] configuration value set: TZ = %s\n", tz) + + log.Println("[INFO] initialization complete") +} diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 85f0393..f6dd5ae 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -1 +1,28 @@ -package main \ No newline at end of file +package main + +import ( + "log" + "os" + "os/signal" + "syscall" +) + +func forever() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + sig := <-c + log.Printf("[INFO] shutting down, detected signal: %s", sig) +} + +func main() { + defer func() { + log.Println("[DEBUG] shutdown sequence complete") + }() + + initialize() + + go httpServer(config.WebSrvIP, config.WebSrvPort) + + forever() +} diff --git a/cmd/webhook/mutate.go b/cmd/webhook/mutate.go new file mode 100644 index 0000000..e3dd07e --- /dev/null +++ b/cmd/webhook/mutate.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "log" + "strings" + + "encoding/json" + "io/ioutil" + "net/http" + + admission "k8s.io/api/admission/v1" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +var ( + codecs = serializer.NewCodecFactory(runtime.NewScheme()) +) + +func admissionReviewFromRequest(r *http.Request, deserializer runtime.Decoder) (*admission.AdmissionReview, error) { + // Validate that the incoming content type is correct. + if r.Header.Get("Content-Type") != "application/json" { + return nil, fmt.Errorf("expected application/json content-type") + } + + // Get the body data, which will be the AdmissionReview + // content for the request. + var body []byte + if r.Body != nil { + requestData, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + body = requestData + } + + // Decode the request body into + admissionReviewRequest := &admission.AdmissionReview{} + if _, _, err := deserializer.Decode(body, nil, admissionReviewRequest); err != nil { + return nil, err + } + + return admissionReviewRequest, nil +} + +func webMutatePod(w http.ResponseWriter, r *http.Request) { + httpAccessLog(r) + + deserializer := codecs.UniversalDeserializer() + + // Parse the AdmissionReview from the http request. + admissionReviewRequest, err := admissionReviewFromRequest(r, deserializer) + if err != nil { + msg := fmt.Sprintf("error getting admission review from request: %v", err) + log.Printf("[ERROR] %v", msg) + tmpltError(w, http.StatusBadRequest, msg) + return + } + + // Do server-side validation that we are only dealing with a pod resource. This + // should also be part of the MutatingWebhookConfiguration in the cluster, but + // we should verify here before continuing. + podResource := meta.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + if admissionReviewRequest.Request.Resource != podResource { + msg := fmt.Sprintf("did not receive pod, got %s", admissionReviewRequest.Request.Resource.Resource) + log.Printf("[ERROR] %v", msg) + tmpltError(w, http.StatusBadRequest, msg) + return + } + + // Decode the pod from the AdmissionReview. + rawRequest := admissionReviewRequest.Request.Object.Raw + pod := core.Pod{} + if _, _, err := deserializer.Decode(rawRequest, nil, &pod); err != nil { + msg := fmt.Sprintf("error decoding raw pod: %v", err) + log.Printf("[ERROR] %v", msg) + tmpltError(w, http.StatusBadRequest, msg) + return + } + + // check to see if mutation is required by looking for a label + if !mutationRequired(&pod.ObjectMeta) { + mutationResp(w, admissionReviewRequest, &admission.AdmissionResponse{Allowed: true}) + } + + // Add sidecar + sidecarContainer := []core.Container{{ + Image: "ca-cert-server:latest", + }} + + patchBytes, _ := createPatch(&pod, sidecarContainer) + + // respond with patch + mutationResp(w, admissionReviewRequest, &admission.AdmissionResponse{ + Allowed: true, + Patch: patchBytes, + PatchType: func() *admission.PatchType { + pt := admission.PatchTypeJSONPatch + return &pt + }(), + }) +} + +// prepare response +func mutationResp(w http.ResponseWriter, aRRequest *admission.AdmissionReview, aResponse *admission.AdmissionResponse) { + var aRResponse admission.AdmissionReview + aRResponse.Response = aResponse + aRResponse.SetGroupVersionKind(aRRequest.GroupVersionKind()) + aRResponse.Response.UID = aRRequest.Request.UID + + resp, err := json.Marshal(aRResponse) + if err != nil { + msg := fmt.Sprintf("error marshalling response json: %v", err) + log.Printf("[ERROR] %v", msg) + tmpltError(w, http.StatusBadRequest, msg) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(resp) +} + +// create mutation patch for resources +func createPatch(pod *core.Pod, containers []core.Container) ([]byte, error) { + var ( + patch []patchOperation + first bool + value interface{} + ) + + if len(pod.Spec.Containers) == 0 { + first = true + } + + for _, add := range containers { + value = add + path := "/spec/containers" + if first { + first = false + value = []core.Container{add} + } else { + path = path + "/-" + } + patch = append(patch, patchOperation{ + Op: "add", + Path: path, + Value: value, + }) + } + + return json.Marshal(patch) +} + +// Check whether the target resourse needs to be mutated +func mutationRequired(metadata *meta.ObjectMeta) bool { + var ignoredNamespaces = []string{ + meta.NamespaceSystem, + meta.NamespacePublic, + } + + // skip special kubernetes system namespaces + for _, namespace := range ignoredNamespaces { + if metadata.Namespace == namespace { + log.Printf("[TRACE] Skip mutation for %v for it's in special namespace:%v", metadata.Name, metadata.Namespace) + return false + } + } + + annotations := metadata.GetLabels() + if annotations == nil { + annotations = map[string]string{} + } + + // determine whether to perform mutation based on annotation for the target resource + var required bool + switch strings.ToLower(annotations["sidecar-injector-webhook/inject"]) { + case "yes", "y", "true", "t", "on": + required = true + default: + required = false + } + + log.Printf("[TRACE] Mutation policy for %v/%v: required:%v", metadata.Namespace, metadata.Name, required) + return required +} diff --git a/go.mod b/go.mod index 96bb4fb..2d58609 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,28 @@ -module localhost/webhook +module mutating-webhook go 1.19 + +require ( + github.com/hashicorp/logutils v1.0.0 + k8s.io/api v0.25.0 + k8s.io/apimachinery v0.25.0 +) + +require ( + github.com/go-logr/logr v1.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..b20f8e6 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,88 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= +k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= +k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= +k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 h1:H9TCJUUx+2VA0ZiD9lvtaX8fthFsMoD+Izn93E/hm8U= +k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=