From 15516536a4dd46d63fd97ce80dbd07ccccc22761 Mon Sep 17 00:00:00 2001 From: jayson Date: Sun, 22 Dec 2024 20:25:13 +0800 Subject: [PATCH] Adding a webhook to externalproxy --- Dockerfile | 2 +- Makefile | 10 +- PROJECT | 7 + README.md | 35 +- api/networking/types.go | 16 + .../v1alpha1/externalproxy_types.go | 188 +++++-- .../v1alpha1/zz_generated.deepcopy.go | 55 +- charts/mobius-manager/.helmignore | 23 - charts/mobius-manager/Chart.yaml | 24 - charts/mobius-manager/templates/NOTES.txt | 1 - charts/mobius-manager/templates/_helpers.tpl | 62 --- charts/mobius-manager/templates/crd.yaml | 435 --------------- .../mobius-manager/templates/deployment.yaml | 66 --- charts/mobius-manager/templates/rbac.yaml | 122 ----- .../templates/serviceaccount.yaml | 13 - charts/mobius-manager/values.yaml | 86 --- cmd/main.go | 8 + config/certmanager/certificate.yaml | 57 ++ config/certmanager/kustomization.yaml | 5 + config/certmanager/kustomizeconfig.yaml | 8 + ...networking.laboys.org_externalproxies.yaml | 183 ++++--- config/default/kustomization.yaml | 6 +- config/default/manager_webhook_patch.yaml | 26 + .../network-policy/allow-webhook-traffic.yaml | 26 + config/network-policy/kustomization.yaml | 2 + config/rbac/role.yaml | 11 - config/webhook/kustomization.yaml | 6 + config/webhook/kustomizeconfig.yaml | 22 + config/webhook/manifests.yaml | 52 ++ config/webhook/service.yaml | 15 + go.mod | 97 ++-- go.sum | 217 ++++---- .../networking/externalproxy_controller.go | 1 + .../externalproxy_controller_test.go | 3 +- ...vent.go => externalproxy_event_handler.go} | 15 +- .../networking/externalproxy_status.go | 16 + internal/controller/networking/utils.go | 24 +- internal/expectations/expectations.go | 16 + internal/expectations/expectations_test.go | 16 + internal/fieldindex/fieldindex.go | 16 + internal/fieldindex/fieldindex_test.go | 31 +- internal/patch/patch.go | 7 +- internal/patch/patch_test.go | 73 +-- internal/sync/singleton/singleton.go | 3 +- .../v1alpha1/externalproxy_webhook.go | 499 ++++++++++++++++++ .../v1alpha1/externalproxy_webhook_test.go | 75 +++ .../networking/v1alpha1/k8s_validation.go | 194 +++++++ .../networking/v1alpha1/webhook_suite_test.go | 170 ++++++ pkg/must/json.go | 15 - pkg/must/json_test.go | 28 - test/utils/utils.go | 6 +- 51 files changed, 1761 insertions(+), 1333 deletions(-) delete mode 100644 charts/mobius-manager/.helmignore delete mode 100644 charts/mobius-manager/Chart.yaml delete mode 100644 charts/mobius-manager/templates/NOTES.txt delete mode 100644 charts/mobius-manager/templates/_helpers.tpl delete mode 100644 charts/mobius-manager/templates/crd.yaml delete mode 100644 charts/mobius-manager/templates/deployment.yaml delete mode 100644 charts/mobius-manager/templates/rbac.yaml delete mode 100644 charts/mobius-manager/templates/serviceaccount.yaml delete mode 100644 charts/mobius-manager/values.yaml create mode 100644 config/certmanager/certificate.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/network-policy/allow-webhook-traffic.yaml create mode 100644 config/network-policy/kustomization.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml rename internal/controller/networking/{externalproxy_watch_event.go => externalproxy_event_handler.go} (80%) create mode 100644 internal/webhook/networking/v1alpha1/externalproxy_webhook.go create mode 100644 internal/webhook/networking/v1alpha1/externalproxy_webhook_test.go create mode 100644 internal/webhook/networking/v1alpha1/k8s_validation.go create mode 100644 internal/webhook/networking/v1alpha1/webhook_suite_test.go delete mode 100644 pkg/must/json.go delete mode 100644 pkg/must/json_test.go diff --git a/Dockerfile b/Dockerfile index c6d568d..5f2ef9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.22 AS builder +FROM golang:1.23 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index e559eb9..d3b7f3a 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. +generate: controller-gen manifests ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt @@ -166,10 +166,10 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) ## Tool Versions -KUSTOMIZE_VERSION ?= v5.4.1 -CONTROLLER_TOOLS_VERSION ?= v0.15.0 -ENVTEST_VERSION ?= release-0.18 -GOLANGCI_LINT_VERSION ?= v1.57.2 +KUSTOMIZE_VERSION ?= v5.5.0 +CONTROLLER_TOOLS_VERSION ?= v0.16.4 +ENVTEST_VERSION ?= release-0.19 +GOLANGCI_LINT_VERSION ?= v1.61.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. diff --git a/PROJECT b/PROJECT index 73c574f..c408811 100644 --- a/PROJECT +++ b/PROJECT @@ -5,6 +5,9 @@ domain: laboys.org layout: - go.kubebuilder.io/v4 +multigroup: true +plugins: + helm.kubebuilder.io/v1-alpha: {} projectName: mobius repo: github.com/wjiec/mobius resources: @@ -17,4 +20,8 @@ resources: kind: ExternalProxy path: github.com/wjiec/mobius/api/networking/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/README.md b/README.md index 513f879..ccc18fd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ # mobius [![Go Report Card](https://goreportcard.com/badge/github.com/wjiec/mobius)](https://goreportcard.com/report/github.com/wjiec/mobius) [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) +[![CI](https://github.com/wjiec/mobius/actions/workflows/ci.yaml/badge.svg)](https://github.com/wjiec/mobius/actions/workflows/ci.yaml) ## Introduction Mobius aims to better orchestrate services in a personal Homelab through kubernetes. +**Note: At the moment mobius is only aiming to serve my own Homelab needs and is in the +development phase, so the design and implementation logic will be changed very frequently. +If you are using the project or are interested in it, any suggestions or questions are welcome +(of course if you have a good idea and are also willing to provide PR, that's also very welcome!).** + ## Getting Started @@ -18,14 +24,20 @@ If you have Helm, you can deploy the mobius with the following command: ```bash helm upgrade --install mobius-manager mobius-manager \ --repo https://wjiec.github.io/mobius \ - --namespace mobius-manager --create-namespace + --namespace mobius-system --create-namespace ``` -It will install the mobius in the mobius-manager namespace, creating that namespace if it doesn't already exist. +It will install the mobius in the mobius-system namespace, creating that namespace if it doesn't already exist. + +### ExternalProxy + +As the name suggests, ExternalProxy creates a unified abstraction for out-of-cluster services through Kubernetes, allowing +us to create services or Ingresses for these out-of-cluster services as if they were in-cluster resources. -### Configure a ExternalProxy +For example, I have some standalone services on my Homelab (TrueNas, openwrt) or something like that, and I'd like to +provide HTTPS ingress for those services via cert-manager or use names within the cluster to access specific services. -Create this manifests locally and update something to your own. +You can refer to the configuration below and update something to your own. ```yaml apiVersion: networking.laboys.org/v1alpha1 kind: ExternalProxy @@ -38,24 +50,13 @@ spec: ports: - name: http port: 80 - service: - type: ClusterIP - ports: - - name: http - port: 80 ingress: rules: - host: openwrt.home.lab - http: - paths: - - pathType: ImplementationSpecific - backend: - port: - name: http tls: - hosts: - - openwrt.home.lab - secretName: star-home-lab + - openwrt.home.lab + secretName: star-domain-com-tls ``` diff --git a/api/networking/types.go b/api/networking/types.go index bd63a0f..d5bea52 100644 --- a/api/networking/types.go +++ b/api/networking/types.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package networking const ( diff --git a/api/networking/v1alpha1/externalproxy_types.go b/api/networking/v1alpha1/externalproxy_types.go index da21004..09f9574 100644 --- a/api/networking/v1alpha1/externalproxy_types.go +++ b/api/networking/v1alpha1/externalproxy_types.go @@ -24,46 +24,123 @@ import ( // ExternalProxySpec defines the desired state of ExternalProxy type ExternalProxySpec struct { + // Backend defines the endpoint to which the traffic will be forwarded to. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems:=1 Backends []ExternalProxyBackend `json:"backends"` - Service ExternalProxyService `json:"service,omitempty"` - Ingress *ExternalProxyIngress `json:"ingress,omitempty"` + + // The service is a generalized description of the destination endpoint of the + // traffic, containing the ingress port number of the traffic and the corresponding + // backend port number or name. + // + // If the content of the service is not defined, a Service of type ClusterIP is automatically + // generated based on the configuration in Backends. + // + // If Service is configured manually, the names of all defined ports must be unique and match + // the port names in Backends, otherwise forwarding will not be possible. The name can be omitted + // if there is only one port. + // + // You can configure only the name, metadata, and type of the Service. Leaving the port information + // blank will be automatically generated by the controller. + // + // +optional + // +kubebuilder:validation:Optional + Service ExternalProxyService `json:"service,omitempty"` + + // Optional Ingress configuration that declares how the proxied service + // will be accessed externally via HTTP(s). + // + // +optional + // +kubebuilder:validation:Optional + Ingress *ExternalProxyIngress `json:"ingress,omitempty"` } +// ExternalProxyBackend describes the backend address of the given proxy as well as the port. type ExternalProxyBackend struct { - Addresses []corev1.EndpointAddress `json:"addresses"` - Ports []corev1.EndpointPort `json:"ports"` + // Addresses which offer the related ports that are marked as ready. These endpoints + // should be considered safe for load balancers and clients to utilize. + // + // +listType=atomic + // +kubebuilder:validation:Required + Addresses []ExternalProxyBackendAddress `json:"addresses"` + + // The available port number on the related addresses. + // + // +listType=atomic + // +kubebuilder:validation:Required + Ports []corev1.EndpointPort `json:"ports"` } +// ExternalProxyBackendAddress describes single IP address. +type ExternalProxyBackendAddress struct { + // The IP of this endpoint. + // May not be loopback (127.0.0.0/8 or ::1), link-local (169.254.0.0/16 or fe80::/10), + // or link-local multicast (224.0.0.0/24 or ff02::/16). + // + // +kubebuilder:validation:Required + IP string `json:"ip" protobuf:"bytes,1,opt,name=ip"` +} + +// ExternalProxyService describes the saved software proxies in the cluster and the port configuration. type ExternalProxyService struct { - // Standard object's metadata. + // Only metadata such as name, label and annotations are allowed to be configured, and + // these will be copied to the created Service. No other fields are allowed and will be + // rejected during validation. + // + // If you do not specify a name, the name of the ExternalProxy will be used as the + // name of the Service by default. + // + // +optional + // +kubebuilder:validation:Optional // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless metav1.ObjectMeta `json:"metadata,omitempty"` - // The name of the Service, if empty the name of the ExternalProxy is used. - Name *string `json:"name,omitempty"` - - // type determines how the Service is exposed. Defaults to ClusterIP. - Type corev1.ServiceType `json:"type"` + // type determines how the Service is exposed. Defaults to ClusterIP. Valid + // options are ExternalName, ClusterIP, NodePort, and LoadBalancer. + // + // +optional + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum:=ClusterIP;NodePort;LoadBalancer + Type corev1.ServiceType `json:"type,omitempty"` // The list of ports that are exposed by this service. + // + // +listType=map + // +listMapKey=port + // +listMapKey=protocol Ports []corev1.ServicePort `json:"ports"` } +// ExternalProxyIngress describes how external traffic is accessing the service to be proxied via HTTP(s). type ExternalProxyIngress struct { - // Standard object's metadata. + // Only metadata such as name, label and annotations are allowed to be configured, and + // these will be copied to the created Ingress. No other fields are allowed and will be + // rejected during validation. + // + // +optional + // +kubebuilder:validation:Optional // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless metav1.ObjectMeta `json:"metadata,omitempty"` - // ingressClassName is the name of an IngressClass cluster resource. + // ingressClassName is the name of an IngressClass cluster resource. Ingress + // controller implementations use this field to know whether they should be + // serving this Ingress resource, by a transitive connection + // (controller -> IngressClass -> Ingress resource). + // + // +optional + // +kubebuilder:validation:Optional IngressClassName *string `json:"ingressClassName,omitempty"` // defaultBackend is the backend that should handle requests that don't // match any rule. If Rules are not specified, DefaultBackend must be specified. // If DefaultBackend is not set, the handling of requests that do not match any // of the rules will be up to the Ingress controller. + // // +optional + // +kubebuilder:validation:Optional DefaultBackend *ExternalProxyIngressBackend `json:"defaultBackend,omitempty"` // tls represents the TLS configuration. Currently, the Ingress only supports a @@ -71,37 +148,33 @@ type ExternalProxyIngress struct { // they will be multiplexed on the same port according to the hostname specified // through the SNI TLS extension, if the ingress controller fulfilling the // ingress supports SNI. + // + // +optional + // +kubebuilder:validation:Optional TLS []networkingv1.IngressTLS `json:"tls,omitempty"` // rules is a list of host rules used to configure the Ingress. If unspecified, // or no rule matches, all traffic is sent to the default backend. - Rules []ExternalProxyIngressRule `json:"rules,omitempty"` -} - -type ExternalProxyIngressMetadata struct { - // Map of string keys and values that can be used to organize and categorize - // (scope and select) objects. May match selectors of replication controllers - // and services. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels - // +optional - Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"` - - // Annotations is an unstructured key value map stored with a resource that may be - // set by external tools to store and retrieve arbitrary metadata. They are not - // queryable and should be preserved when modifying objects. - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + // // +optional - Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` + // +listType=atomic + // +kubebuilder:validation:Optional + Rules []ExternalProxyIngressRule `json:"rules,omitempty"` } // ExternalProxyIngressRule represents the rules mapping the paths under a specified -// host to the related backend services. Incoming requests are first evaluated for a -// host match, then routed to the backend associated with the matching IngressRuleValue. +// host to the related backend proxy services. Incoming requests are first evaluated +// for a host match, then routed to the backend associated with the matching +// ExternalProxyIngressHttpRuleValue. type ExternalProxyIngressRule struct { // host is the fully qualified domain name of a network host, as defined by RFC 3986. // // Incoming requests are matched against the host before the IngressRuleValue. If the - // host is unspecified, the Ingress routes all traffic based on the specified IngressRuleValue. + // host is unspecified, the Ingress routes all traffic based on the specified + // ExternalProxyIngressHttpRuleValue. + // + // +optional + // +kubebuilder:validation:Optional Host string `json:"host,omitempty"` // http represents a rule to route requests for this ExternalProxyIngressRule. @@ -115,30 +188,45 @@ type ExternalProxyIngressRule struct { // ExternalProxyIngressHttpRuleValue is a list of http selectors pointing to backends. type ExternalProxyIngressHttpRuleValue struct { // paths is a collection of paths that map requests to backends. + // + // +listType=atomic Paths []ExternalProxyIngressHttpPath `json:"paths"` } -// ExternalProxyIngressHttpPath associates a path with a backend. Incoming urls matching -// the path are forwarded to the backend. +// ExternalProxyIngressHttpPath associates a path with proxied service. Incoming urls +// matching the path are forwarded to the corresponding backend. type ExternalProxyIngressHttpPath struct { // path is matched against the path of an incoming request. Currently, it can // contain characters disallowed from the conventional "path" part of a URL // as defined by RFC 3986. Paths must begin with a '/' and must be present // when using PathType with value "Exact" or "Prefix". + // + // +optional + // +kubebuilder:validation:Optional Path string `json:"path,omitempty"` - // pathType determines the interpretation of the path matching. + // pathType determines the interpretation of the path matching. Valid + // options are Exact, Prefix and ImplementationSpecific. + // + // +kubebuilder:validation:Enum:=Exact;Prefix;ImplementationSpecific PathType *networkingv1.PathType `json:"pathType"` - // backend defines the referenced service endpoint to which the traffic + // backend defines the referenced service port to which the traffic // will be forwarded to. + // + // This field can be ignored if only one port is configured in the service. + // + // +optional + // +kubebuilder:validation:Optional Backend *ExternalProxyIngressBackend `json:"backend,omitempty"` } // ExternalProxyIngressBackend describes all endpoints for a given service and port. type ExternalProxyIngressBackend struct { - // port of the referenced service. A port name or port number - // is required for a ExternalProxyServiceBackendPort. + // port of the referenced service. A port name or port number is required. + // + // +optional + // +kubebuilder:validation:Optional Port ExternalProxyServiceBackendPort `json:"port,omitempty"` } @@ -146,26 +234,46 @@ type ExternalProxyIngressBackend struct { type ExternalProxyServiceBackendPort struct { // name is the name of the port on the Service. // This is a mutually exclusive setting with "Number". + // // +optional + // +kubebuilder:validation:Optional Name string `json:"name,omitempty"` // number is the numerical port number (e.g. 80) on the Service. // This is a mutually exclusive setting with "Name". + // // +optional + // +kubebuilder:validation:Optional Number int32 `json:"number,omitempty"` } // ExternalProxyStatus defines the observed state of ExternalProxy type ExternalProxyStatus struct { - Ready bool `json:"ready"` - ServiceName string `json:"serviceName"` - ObservedGeneration int64 `json:"observedGeneration"` + // Ready Indicates whether the current ExternalProxy, including its subordinate + // Services, and Ingress resources are ready. + // + // +optional + // +kubebuilder:validation:Optional + Ready bool `json:"ready"` + + // ServiceName is the name of the Service object that was finalized for use. + // + // +optional + // +kubebuilder:validation:Optional + ServiceName string `json:"serviceName"` + + // ObservedGeneration is the last generation the controller observed. + // + // +optional + // +kubebuilder:validation:Optional + ObservedGeneration int64 `json:"observedGeneration"` } // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=".status.ready" // +kubebuilder:printcolumn:name="Service",type=string,JSONPath=".status.serviceName" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:resource:categories=mobius,shortName=mep // +kubebuilder:subresource:status // ExternalProxy is the Schema for the externalproxies API diff --git a/api/networking/v1alpha1/zz_generated.deepcopy.go b/api/networking/v1alpha1/zz_generated.deepcopy.go index 6f36af8..cd138cc 100644 --- a/api/networking/v1alpha1/zz_generated.deepcopy.go +++ b/api/networking/v1alpha1/zz_generated.deepcopy.go @@ -58,10 +58,8 @@ func (in *ExternalProxyBackend) DeepCopyInto(out *ExternalProxyBackend) { *out = *in if in.Addresses != nil { in, out := &in.Addresses, &out.Addresses - *out = make([]v1.EndpointAddress, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + *out = make([]ExternalProxyBackendAddress, len(*in)) + copy(*out, *in) } if in.Ports != nil { in, out := &in.Ports, &out.Ports @@ -82,6 +80,21 @@ func (in *ExternalProxyBackend) DeepCopy() *ExternalProxyBackend { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalProxyBackendAddress) DeepCopyInto(out *ExternalProxyBackendAddress) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProxyBackendAddress. +func (in *ExternalProxyBackendAddress) DeepCopy() *ExternalProxyBackendAddress { + if in == nil { + return nil + } + out := new(ExternalProxyBackendAddress) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalProxyIngress) DeepCopyInto(out *ExternalProxyIngress) { *out = *in @@ -185,35 +198,6 @@ func (in *ExternalProxyIngressHttpRuleValue) DeepCopy() *ExternalProxyIngressHtt return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExternalProxyIngressMetadata) DeepCopyInto(out *ExternalProxyIngressMetadata) { - *out = *in - if in.Labels != nil { - in, out := &in.Labels, &out.Labels - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Annotations != nil { - in, out := &in.Annotations, &out.Annotations - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProxyIngressMetadata. -func (in *ExternalProxyIngressMetadata) DeepCopy() *ExternalProxyIngressMetadata { - if in == nil { - return nil - } - out := new(ExternalProxyIngressMetadata) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalProxyIngressRule) DeepCopyInto(out *ExternalProxyIngressRule) { *out = *in @@ -270,11 +254,6 @@ func (in *ExternalProxyList) DeepCopyObject() runtime.Object { func (in *ExternalProxyService) DeepCopyInto(out *ExternalProxyService) { *out = *in in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.Name != nil { - in, out := &in.Name, &out.Name - *out = new(string) - **out = **in - } if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make([]v1.ServicePort, len(*in)) diff --git a/charts/mobius-manager/.helmignore b/charts/mobius-manager/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/charts/mobius-manager/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/mobius-manager/Chart.yaml b/charts/mobius-manager/Chart.yaml deleted file mode 100644 index ccbeca4..0000000 --- a/charts/mobius-manager/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: mobius-manager -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "v0.1.0" diff --git a/charts/mobius-manager/templates/NOTES.txt b/charts/mobius-manager/templates/NOTES.txt deleted file mode 100644 index 9163fa0..0000000 --- a/charts/mobius-manager/templates/NOTES.txt +++ /dev/null @@ -1 +0,0 @@ -Have a nice day :) diff --git a/charts/mobius-manager/templates/_helpers.tpl b/charts/mobius-manager/templates/_helpers.tpl deleted file mode 100644 index 88a7021..0000000 --- a/charts/mobius-manager/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "mobius-manager.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "mobius-manager.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "mobius-manager.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "mobius-manager.labels" -}} -helm.sh/chart: {{ include "mobius-manager.chart" . }} -{{ include "mobius-manager.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "mobius-manager.selectorLabels" -}} -app.kubernetes.io/name: {{ include "mobius-manager.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "mobius-manager.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "mobius-manager.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/charts/mobius-manager/templates/crd.yaml b/charts/mobius-manager/templates/crd.yaml deleted file mode 100644 index c9e9a14..0000000 --- a/charts/mobius-manager/templates/crd.yaml +++ /dev/null @@ -1,435 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.15.0 - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} - name: externalproxies.networking.laboys.org -spec: - group: networking.laboys.org - names: - kind: ExternalProxy - listKind: ExternalProxyList - plural: externalproxies - singular: externalproxy - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.ready - name: Ready - type: boolean - - jsonPath: .status.serviceName - name: Service - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: ExternalProxy is the Schema for the externalproxies API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ExternalProxySpec defines the desired state of ExternalProxy - properties: - backends: - items: - properties: - addresses: - items: - description: EndpointAddress is a tuple that describes single - IP address. - properties: - hostname: - description: The Hostname of this endpoint - type: string - ip: - description: |- - The IP of this endpoint. - May not be loopback (127.0.0.0/8 or ::1), link-local (169.254.0.0/16 or fe80::/10), - or link-local multicast (224.0.0.0/24 or ff02::/16). - type: string - nodeName: - description: 'Optional: Node hosting this endpoint. This - can be used to determine endpoints local to a node.' - type: string - targetRef: - description: Reference to object providing the endpoint. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - TODO: this design is not final and this field is subject to change in the future. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - required: - - ip - type: object - x-kubernetes-map-type: atomic - type: array - ports: - items: - description: EndpointPort is a tuple that describes a single - port. - properties: - appProtocol: - description: |- - The application protocol for this port. - This is used as a hint for implementations to offer richer behavior for protocols that they understand. - This field follows standard Kubernetes label syntax. - Valid values are either: - - - * Un-prefixed protocol names - reserved for IANA standard service names (as per - RFC-6335 and https://www.iana.org/assignments/service-names). - - - * Kubernetes-defined prefixed names: - * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- - * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 - * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - - - * Other protocols should use implementation-defined prefixed names such as - mycompany.com/my-custom-protocol. - type: string - name: - description: |- - The name of this port. This must match the 'name' field in the - corresponding ServicePort. - Must be a DNS_LABEL. - Optional only if one port is defined. - type: string - port: - description: The port number of the endpoint. - format: int32 - type: integer - protocol: - default: TCP - description: |- - The IP protocol for this port. - Must be UDP, TCP, or SCTP. - Default is TCP. - type: string - required: - - port - type: object - x-kubernetes-map-type: atomic - type: array - required: - - addresses - - ports - type: object - type: array - ingress: - properties: - defaultBackend: - description: |- - defaultBackend is the backend that should handle requests that don't - match any rule. If Rules are not specified, DefaultBackend must be specified. - If DefaultBackend is not set, the handling of requests that do not match any - of the rules will be up to the Ingress controller. - properties: - port: - description: |- - port of the referenced service. A port name or port number - is required for a ExternalProxyServiceBackendPort. - properties: - name: - description: |- - name is the name of the port on the Service. - This is a mutually exclusive setting with "Number". - type: string - number: - description: |- - number is the numerical port number (e.g. 80) on the Service. - This is a mutually exclusive setting with "Name". - format: int32 - type: integer - type: object - type: object - ingressClassName: - description: ingressClassName is the name of an IngressClass cluster - resource. - type: string - metadata: - description: Standard object's metadata. - x-kubernetes-preserve-unknown-fields: true - rules: - description: |- - rules is a list of host rules used to configure the Ingress. If unspecified, - or no rule matches, all traffic is sent to the default backend. - items: - description: |- - ExternalProxyIngressRule represents the rules mapping the paths under a specified - host to the related backend services. Incoming requests are first evaluated for a - host match, then routed to the backend associated with the matching IngressRuleValue. - properties: - host: - description: |- - host is the fully qualified domain name of a network host, as defined by RFC 3986. - - - Incoming requests are matched against the host before the IngressRuleValue. If the - host is unspecified, the Ingress routes all traffic based on the specified IngressRuleValue. - type: string - http: - description: |- - http represents a rule to route requests for this ExternalProxyIngressRule. - - - If unspecified, the rule defaults to a http catch-all. Whether that sends - just traffic matching the host to the default backend or all traffic to the - default backend, is left to the controller fulfilling the Ingress. - properties: - paths: - description: paths is a collection of paths that map - requests to backends. - items: - description: |- - ExternalProxyIngressHttpPath associates a path with a backend. Incoming urls matching - the path are forwarded to the backend. - properties: - backend: - description: |- - backend defines the referenced service endpoint to which the traffic - will be forwarded to. - properties: - port: - description: |- - port of the referenced service. A port name or port number - is required for a ExternalProxyServiceBackendPort. - properties: - name: - description: |- - name is the name of the port on the Service. - This is a mutually exclusive setting with "Number". - type: string - number: - description: |- - number is the numerical port number (e.g. 80) on the Service. - This is a mutually exclusive setting with "Name". - format: int32 - type: integer - type: object - type: object - path: - description: |- - path is matched against the path of an incoming request. Currently, it can - contain characters disallowed from the conventional "path" part of a URL - as defined by RFC 3986. Paths must begin with a '/' and must be present - when using PathType with value "Exact" or "Prefix". - type: string - pathType: - description: pathType determines the interpretation - of the path matching. - type: string - required: - - pathType - type: object - type: array - required: - - paths - type: object - type: object - type: array - tls: - description: |- - tls represents the TLS configuration. Currently, the Ingress only supports a - single TLS port, 443. If multiple members of this list specify different hosts, - they will be multiplexed on the same port according to the hostname specified - through the SNI TLS extension, if the ingress controller fulfilling the - ingress supports SNI. - items: - description: IngressTLS describes the transport layer security - associated with an ingress. - properties: - hosts: - description: |- - hosts is a list of hosts included in the TLS certificate. The values in - this list must match the name/s used in the tlsSecret. Defaults to the - wildcard host setting for the loadbalancer controller fulfilling this - Ingress, if left unspecified. - items: - type: string - type: array - x-kubernetes-list-type: atomic - secretName: - description: |- - secretName is the name of the secret used to terminate TLS traffic on - port 443. Field is left optional to allow TLS routing based on SNI - hostname alone. If the SNI host in a listener conflicts with the "Host" - header field used by an IngressRule, the SNI host is used for termination - and value of the "Host" header is used for routing. - type: string - type: object - type: array - type: object - service: - properties: - metadata: - description: Standard object's metadata. - x-kubernetes-preserve-unknown-fields: true - name: - description: The name of the Service, if empty the name of the - ExternalProxy is used. - type: string - ports: - description: The list of ports that are exposed by this service. - items: - description: ServicePort contains information on service's port. - properties: - appProtocol: - description: |- - The application protocol for this port. - This is used as a hint for implementations to offer richer behavior for protocols that they understand. - This field follows standard Kubernetes label syntax. - Valid values are either: - - - * Un-prefixed protocol names - reserved for IANA standard service names (as per - RFC-6335 and https://www.iana.org/assignments/service-names). - - - * Kubernetes-defined prefixed names: - * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- - * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 - * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - - - * Other protocols should use implementation-defined prefixed names such as - mycompany.com/my-custom-protocol. - type: string - name: - description: |- - The name of this port within the service. This must be a DNS_LABEL. - All ports within a ServiceSpec must have unique names. When considering - the endpoints for a Service, this must match the 'name' field in the - EndpointPort. - Optional if only one ServicePort is defined on this service. - type: string - nodePort: - description: |- - The port on each node on which this service is exposed when type is - NodePort or LoadBalancer. Usually assigned by the system. If a value is - specified, in-range, and not in use it will be used, otherwise the - operation will fail. If not specified, a port will be allocated if this - Service requires one. If this field is specified when creating a - Service which does not need it, creation will fail. This field will be - wiped when updating a Service to no longer need it (e.g. changing type - from NodePort to ClusterIP). - More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport - format: int32 - type: integer - port: - description: The port that will be exposed by this service. - format: int32 - type: integer - protocol: - default: TCP - description: |- - The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". - Default is TCP. - type: string - targetPort: - anyOf: - - type: integer - - type: string - description: |- - Number or name of the port to access on the pods targeted by the service. - Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. - If this is a string, it will be looked up as a named port in the - target Pod's container ports. If this is not specified, the value - of the 'port' field is used (an identity map). - This field is ignored for services with clusterIP=None, and should be - omitted or set equal to the 'port' field. - More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service - x-kubernetes-int-or-string: true - required: - - port - type: object - type: array - type: - description: type determines how the Service is exposed. Defaults - to ClusterIP. - type: string - required: - - ports - - type - type: object - required: - - backends - type: object - status: - description: ExternalProxyStatus defines the observed state of ExternalProxy - properties: - observedGeneration: - format: int64 - type: integer - ready: - type: boolean - serviceName: - type: string - required: - - observedGeneration - - ready - - serviceName - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charts/mobius-manager/templates/deployment.yaml b/charts/mobius-manager/templates/deployment.yaml deleted file mode 100644 index 34ae869..0000000 --- a/charts/mobius-manager/templates/deployment.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "mobius-manager.fullname" . }} - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "mobius-manager.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "mobius-manager.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "mobius-manager.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: healthy - containerPort: {{ .Values.healthy.port }} - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe | nindent 12 }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/mobius-manager/templates/rbac.yaml b/charts/mobius-manager/templates/rbac.yaml deleted file mode 100644 index 41886ba..0000000 --- a/charts/mobius-manager/templates/rbac.yaml +++ /dev/null @@ -1,122 +0,0 @@ -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ .Values.rbac.name }}-edit - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} -rules: - - apiGroups: - - networking.laboys.org - resources: - - externalproxies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - networking.laboys.org - resources: - - externalproxies/status - verbs: - - get ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ .Values.rbac.name }}-view - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} -rules: - - apiGroups: - - networking.laboys.org - resources: - - externalproxies - verbs: - - get - - list - - watch - - apiGroups: - - networking.laboys.org - resources: - - externalproxies/status - verbs: - - get ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ .Values.rbac.name }}-controller - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} -rules: - - apiGroups: - - "" - resources: - - endpoints - - services - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - networking.laboys.org - resources: - - externalproxies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - networking.laboys.org - resources: - - externalproxies/finalizers - verbs: - - update - - apiGroups: - - networking.laboys.org - resources: - - externalproxies/status - verbs: - - get - - patch - - update ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ .Values.rbac.name }}-controller - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} -roleRef: - apiGroup: "" - kind: ClusterRole - name: {{ .Values.rbac.name }}-controller -subjects: - - apiGroup: "" - kind: ServiceAccount - name: {{ include "mobius-manager.fullname" . }} - namespace: {{ .Release.Namespace }} diff --git a/charts/mobius-manager/templates/serviceaccount.yaml b/charts/mobius-manager/templates/serviceaccount.yaml deleted file mode 100644 index 579f17b..0000000 --- a/charts/mobius-manager/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "mobius-manager.serviceAccountName" . }} - labels: - {{- include "mobius-manager.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/charts/mobius-manager/values.yaml b/charts/mobius-manager/values.yaml deleted file mode 100644 index 931df14..0000000 --- a/charts/mobius-manager/values.yaml +++ /dev/null @@ -1,86 +0,0 @@ -# Default values for mobius-manager. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: wjiec/mobius-manager - pullPolicy: Always - # Overrides the image tag whose default is the chart appVersion. - tag: "" - -healthy: - port: 8081 - -rbac: - name: mobius-manager - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "mobius-manager" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -livenessProbe: - httpGet: - path: /healthz - port: healthy -readinessProbe: - httpGet: - path: /readyz - port: healthy - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/cmd/main.go b/cmd/main.go index 16a3e26..8df8fc4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,7 @@ import ( networkingv1alpha1 "github.com/wjiec/mobius/api/networking/v1alpha1" networkingcontroller "github.com/wjiec/mobius/internal/controller/networking" "github.com/wjiec/mobius/internal/fieldindex" + webhooknetworkingv1alpha1 "github.com/wjiec/mobius/internal/webhook/networking/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -112,6 +113,13 @@ func main() { os.Exit(1) } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhooknetworkingv1alpha1.SetupExternalProxyWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ExternalProxy") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 0000000..71520e1 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,57 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: mobius + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: mobius + app.kubernetes.io/part-of: mobius + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + dnsNames: + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: metrics-certs + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: mobius + app.kubernetes.io/part-of: mobius + app.kubernetes.io/managed-by: kustomize + name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + dnsNames: + - controller-manager-metrics-service.system.svc + - controller-manager-metrics-service.system.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: metrics-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 0000000..bebea5a --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 0000000..cf6f89e --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/config/crd/bases/networking.laboys.org_externalproxies.yaml b/config/crd/bases/networking.laboys.org_externalproxies.yaml index 3b92a27..94ca4b5 100644 --- a/config/crd/bases/networking.laboys.org_externalproxies.yaml +++ b/config/crd/bases/networking.laboys.org_externalproxies.yaml @@ -3,14 +3,18 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: externalproxies.networking.laboys.org spec: group: networking.laboys.org names: + categories: + - mobius kind: ExternalProxy listKind: ExternalProxyList plural: externalproxies + shortNames: + - mep singular: externalproxy scope: Namespaced versions: @@ -50,76 +54,33 @@ spec: description: ExternalProxySpec defines the desired state of ExternalProxy properties: backends: + description: Backend defines the endpoint to which the traffic will + be forwarded to. items: + description: ExternalProxyBackend describes the backend address + of the given proxy as well as the port. properties: addresses: + description: |- + Addresses which offer the related ports that are marked as ready. These endpoints + should be considered safe for load balancers and clients to utilize. items: - description: EndpointAddress is a tuple that describes single + description: ExternalProxyBackendAddress describes single IP address. properties: - hostname: - description: The Hostname of this endpoint - type: string ip: description: |- The IP of this endpoint. May not be loopback (127.0.0.0/8 or ::1), link-local (169.254.0.0/16 or fe80::/10), or link-local multicast (224.0.0.0/24 or ff02::/16). type: string - nodeName: - description: 'Optional: Node hosting this endpoint. This - can be used to determine endpoints local to a node.' - type: string - targetRef: - description: Reference to object providing the endpoint. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - TODO: this design is not final and this field is subject to change in the future. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic required: - ip type: object - x-kubernetes-map-type: atomic type: array + x-kubernetes-list-type: atomic ports: + description: The available port number on the related addresses. items: description: EndpointPort is a tuple that describes a single port. @@ -131,17 +92,14 @@ spec: This field follows standard Kubernetes label syntax. Valid values are either: - * Un-prefixed protocol names - reserved for IANA standard service names (as per RFC-6335 and https://www.iana.org/assignments/service-names). - * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - * Other protocols should use implementation-defined prefixed names such as mycompany.com/my-custom-protocol. type: string @@ -157,7 +115,6 @@ spec: format: int32 type: integer protocol: - default: TCP description: |- The IP protocol for this port. Must be UDP, TCP, or SCTP. @@ -168,12 +125,17 @@ spec: type: object x-kubernetes-map-type: atomic type: array + x-kubernetes-list-type: atomic required: - addresses - ports type: object + minItems: 1 type: array ingress: + description: |- + Optional Ingress configuration that declares how the proxied service + will be accessed externally via HTTP(s). properties: defaultBackend: description: |- @@ -183,9 +145,8 @@ spec: of the rules will be up to the Ingress controller. properties: port: - description: |- - port of the referenced service. A port name or port number - is required for a ExternalProxyServiceBackendPort. + description: port of the referenced service. A port name or + port number is required. properties: name: description: |- @@ -201,11 +162,17 @@ spec: type: object type: object ingressClassName: - description: ingressClassName is the name of an IngressClass cluster - resource. + description: |- + ingressClassName is the name of an IngressClass cluster resource. Ingress + controller implementations use this field to know whether they should be + serving this Ingress resource, by a transitive connection + (controller -> IngressClass -> Ingress resource). type: string metadata: - description: Standard object's metadata. + description: |- + Only metadata such as name, label and annotations are allowed to be configured, and + these will be copied to the created Ingress. No other fields are allowed and will be + rejected during validation. x-kubernetes-preserve-unknown-fields: true rules: description: |- @@ -214,22 +181,22 @@ spec: items: description: |- ExternalProxyIngressRule represents the rules mapping the paths under a specified - host to the related backend services. Incoming requests are first evaluated for a - host match, then routed to the backend associated with the matching IngressRuleValue. + host to the related backend proxy services. Incoming requests are first evaluated + for a host match, then routed to the backend associated with the matching + ExternalProxyIngressHttpRuleValue. properties: host: description: |- host is the fully qualified domain name of a network host, as defined by RFC 3986. - Incoming requests are matched against the host before the IngressRuleValue. If the - host is unspecified, the Ingress routes all traffic based on the specified IngressRuleValue. + host is unspecified, the Ingress routes all traffic based on the specified + ExternalProxyIngressHttpRuleValue. type: string http: description: |- http represents a rule to route requests for this ExternalProxyIngressRule. - If unspecified, the rule defaults to a http catch-all. Whether that sends just traffic matching the host to the default backend or all traffic to the default backend, is left to the controller fulfilling the Ingress. @@ -239,18 +206,19 @@ spec: requests to backends. items: description: |- - ExternalProxyIngressHttpPath associates a path with a backend. Incoming urls matching - the path are forwarded to the backend. + ExternalProxyIngressHttpPath associates a path with proxied service. Incoming urls + matching the path are forwarded to the corresponding backend. properties: backend: description: |- - backend defines the referenced service endpoint to which the traffic + backend defines the referenced service port to which the traffic will be forwarded to. + + This field can be ignored if only one port is configured in the service. properties: port: - description: |- - port of the referenced service. A port name or port number - is required for a ExternalProxyServiceBackendPort. + description: port of the referenced service. + A port name or port number is required. properties: name: description: |- @@ -273,18 +241,25 @@ spec: when using PathType with value "Exact" or "Prefix". type: string pathType: - description: pathType determines the interpretation - of the path matching. + description: |- + pathType determines the interpretation of the path matching. Valid + options are Exact, Prefix and ImplementationSpecific. + enum: + - Exact + - Prefix + - ImplementationSpecific type: string required: - pathType type: object type: array + x-kubernetes-list-type: atomic required: - paths type: object type: object type: array + x-kubernetes-list-type: atomic tls: description: |- tls represents the TLS configuration. Currently, the Ingress only supports a @@ -318,14 +293,30 @@ spec: type: array type: object service: + description: |- + The service is a generalized description of the destination endpoint of the + traffic, containing the ingress port number of the traffic and the corresponding + backend port number or name. + + If the content of the service is not defined, a Service of type ClusterIP is automatically + generated based on the configuration in Backends. + + If Service is configured manually, the names of all defined ports must be unique and match + the port names in Backends, otherwise forwarding will not be possible. The name can be omitted + if there is only one port. + + You can configure only the name, metadata, and type of the Service. Leaving the port information + blank will be automatically generated by the controller. properties: metadata: - description: Standard object's metadata. + description: |- + Only metadata such as name, label and annotations are allowed to be configured, and + these will be copied to the created Service. No other fields are allowed and will be + rejected during validation. + + If you do not specify a name, the name of the ExternalProxy will be used as the + name of the Service by default. x-kubernetes-preserve-unknown-fields: true - name: - description: The name of the Service, if empty the name of the - ExternalProxy is used. - type: string ports: description: The list of ports that are exposed by this service. items: @@ -338,17 +329,14 @@ spec: This field follows standard Kubernetes label syntax. Valid values are either: - * Un-prefixed protocol names - reserved for IANA standard service names (as per RFC-6335 and https://www.iana.org/assignments/service-names). - * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 - * Other protocols should use implementation-defined prefixed names such as mycompany.com/my-custom-protocol. type: string @@ -401,13 +389,21 @@ spec: - port type: object type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map type: - description: type determines how the Service is exposed. Defaults - to ClusterIP. + description: |- + type determines how the Service is exposed. Defaults to ClusterIP. Valid + options are ExternalName, ClusterIP, NodePort, and LoadBalancer. + enum: + - ClusterIP + - NodePort + - LoadBalancer type: string required: - ports - - type type: object required: - backends @@ -416,16 +412,19 @@ spec: description: ExternalProxyStatus defines the observed state of ExternalProxy properties: observedGeneration: + description: ObservedGeneration is the last generation the controller + observed. format: int64 type: integer ready: + description: |- + Ready Indicates whether the current ExternalProxy, including its subordinate + Services, and Ingress resources are ready. type: boolean serviceName: + description: ServiceName is the name of the Service object that was + finalized for use. type: string - required: - - observedGeneration - - ready - - serviceName type: object type: object served: true diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 4c90009..d7a3c10 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -20,7 +20,7 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -29,7 +29,7 @@ resources: #- metrics_service.yaml # Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager -#patches: +patches: # [METRICS] The following patch will enable the metrics endpoint. Ensure that you also protect this endpoint. # More info: https://book.kubebuilder.io/reference/metrics # If you want to expose the metric endpoint of your controller-manager uncomment the following line. @@ -39,7 +39,7 @@ resources: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- path: manager_webhook_patch.yaml +- path: manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000..3576203 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + app.kubernetes.io/name: mobius + app.kubernetes.io/managed-by: kustomize +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 0000000..7ab5bcb --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: mobius + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml new file mode 100644 index 0000000..a67bd68 --- /dev/null +++ b/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-webhook-traffic.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1b273a4..bbbbd1f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,17 +8,6 @@ rules: - "" resources: - endpoints - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - "" - resources: - services verbs: - create diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 0000000..9cf2613 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 0000000..206316e --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 0000000..6571d6d --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-networking-laboys-org-v1alpha1-externalproxy + failurePolicy: Fail + name: mexternalproxy-v1alpha1.kb.io + rules: + - apiGroups: + - networking.laboys.org + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - externalproxies + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-networking-laboys-org-v1alpha1-externalproxy + failurePolicy: Fail + name: vexternalproxy-v1alpha1.kb.io + rules: + - apiGroups: + - networking.laboys.org + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - externalproxies + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 0000000..488324c --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: mobius + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/go.mod b/go.mod index 817cf7a..e2aeaa1 100644 --- a/go.mod +++ b/go.mod @@ -1,78 +1,79 @@ module github.com/wjiec/mobius -go 1.22.0 +go 1.23 -toolchain go1.22.5 +toolchain go1.23.3 require ( - github.com/go-logr/logr v1.4.1 + github.com/go-logr/logr v1.4.2 github.com/hashicorp/go-multierror v1.1.1 - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.32.0 - github.com/stretchr/testify v1.8.4 - k8s.io/api v0.30.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 - k8s.io/klog/v2 v2.120.1 - k8s.io/utils v0.0.0-20230726121419-3b25d923346b - sigs.k8s.io/controller-runtime v0.18.2 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + github.com/samber/lo v1.47.0 + github.com/stretchr/testify v1.9.0 + k8s.io/api v0.31.0 + k8s.io/apimachinery v0.31.0 + k8s.io/client-go v0.31.0 + k8s.io/klog/v2 v2.130.1 + k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 + sigs.k8s.io/controller-runtime v0.19.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.16.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.18.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.27.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.30.0 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect + k8s.io/kube-openapi v0.0.0-20241127205056-99599406b04f // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.3 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7328e34..fb4e542 100644 --- a/go.sum +++ b/go.sum @@ -1,78 +1,74 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/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/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 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= @@ -80,36 +76,35 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= -github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -121,84 +116,76 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 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/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= 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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 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= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= -k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs= -k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y= -k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= -k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= -k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.18.2 h1:RqVW6Kpeaji67CY5nPEfRz6ZfFMk0lWQlNrLqlNpx+Q= -sigs.k8s.io/controller-runtime v0.18.2/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241127205056-99599406b04f h1:nLHvOvs1CZ+FAEwR4EqLeRLfbtWQNlIu5g393Hq/1UM= +k8s.io/kube-openapi v0.0.0-20241127205056-99599406b04f/go.mod h1:iZjdMQzunI7O/sUrf/5WRX1gvaAIam32lKx9+paoLbU= +k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= +k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= +sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.4.3 h1:sCP7Vv3xx/CWIuTPVN38lUPx0uw0lcLfzaiDa8Ja01A= +sigs.k8s.io/structured-merge-diff/v4 v4.4.3/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/controller/networking/externalproxy_controller.go b/internal/controller/networking/externalproxy_controller.go index 8ece0f6..c79ce65 100644 --- a/internal/controller/networking/externalproxy_controller.go +++ b/internal/controller/networking/externalproxy_controller.go @@ -148,6 +148,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } newStatus := &ExternalProxyStatus{ + // Should be ready or not based on the state of Ingress and Service Ready: true, ServiceName: getServiceName(&instance), ObservedGeneration: instance.Generation, diff --git a/internal/controller/networking/externalproxy_controller_test.go b/internal/controller/networking/externalproxy_controller_test.go index 1f6f9c8..2b62c18 100644 --- a/internal/controller/networking/externalproxy_controller_test.go +++ b/internal/controller/networking/externalproxy_controller_test.go @@ -55,7 +55,7 @@ var _ = Describe("ExternalProxy Controller", func() { Spec: networkingv1alpha1.ExternalProxySpec{ Backends: []networkingv1alpha1.ExternalProxyBackend{ { - Addresses: []corev1.EndpointAddress{ + Addresses: []networkingv1alpha1.ExternalProxyBackendAddress{ {IP: "192.168.1.1"}, }, Ports: []corev1.EndpointPort{ @@ -64,7 +64,6 @@ var _ = Describe("ExternalProxy Controller", func() { }, }, Service: networkingv1alpha1.ExternalProxyService{ - Name: ptr.To("example"), Ports: []corev1.ServicePort{ {Name: "http", Port: 80}, }, diff --git a/internal/controller/networking/externalproxy_watch_event.go b/internal/controller/networking/externalproxy_event_handler.go similarity index 80% rename from internal/controller/networking/externalproxy_watch_event.go rename to internal/controller/networking/externalproxy_event_handler.go index 6f18c66..b41f3de 100644 --- a/internal/controller/networking/externalproxy_watch_event.go +++ b/internal/controller/networking/externalproxy_event_handler.go @@ -22,6 +22,7 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" @@ -32,14 +33,14 @@ import ( var ( // initialingRateLimiter calculates the delay duration for existing resources // triggered Create event when the Informer cache has just synced. - initialingRateLimiter = workqueue.NewItemExponentialFailureRateLimiter(3*time.Second, 30*time.Second) + initialingRateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[ctrl.Request](3*time.Second, 30*time.Second) ) type watchEventHandler[T client.Object] struct { expectations expectations.ControllerExpectations } -func (w *watchEventHandler[T]) Create(ctx context.Context, evt event.TypedCreateEvent[client.Object], q workqueue.RateLimitingInterface) { +func (w *watchEventHandler[T]) Create(ctx context.Context, evt event.TypedCreateEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { logger := log.FromContext(ctx) if evt.Object.GetDeletionTimestamp() != nil { w.Delete(ctx, event.TypedDeleteEvent[client.Object]{Object: evt.Object}, q) @@ -57,22 +58,22 @@ func (w *watchEventHandler[T]) Create(ctx context.Context, evt event.TypedCreate if isSatisfied { // If the expectation is satisfied, it should be an existing Pod and the Informer // cache should have just synced. - q.AddAfter(*req, initialingRateLimiter.When(req)) + q.AddAfter(*req, initialingRateLimiter.When(*req)) } else { // Otherwise, add it immediately and reset the rate limiter - initialingRateLimiter.Forget(req) + initialingRateLimiter.Forget(*req) q.Add(*req) } } -func (w *watchEventHandler[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[client.Object], q workqueue.RateLimitingInterface) { +func (w *watchEventHandler[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { if evt.ObjectNew.GetDeletionTimestamp() != nil { w.Delete(ctx, event.TypedDeleteEvent[client.Object]{Object: evt.ObjectNew}, q) return } } -func (w *watchEventHandler[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[client.Object], q workqueue.RateLimitingInterface) { +func (w *watchEventHandler[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { logger := log.FromContext(ctx) if _, ok := evt.Object.(T); !ok { logger.Error(nil, "Skipped deletion event", "deleteStateUnknown", evt.DeleteStateUnknown, "obj", evt.Object) @@ -88,5 +89,5 @@ func (w *watchEventHandler[T]) Delete(ctx context.Context, evt event.TypedDelete q.Add(*req) } -func (w *watchEventHandler[T]) Generic(context.Context, event.TypedGenericEvent[client.Object], workqueue.RateLimitingInterface) { +func (w *watchEventHandler[T]) Generic(context.Context, event.TypedGenericEvent[client.Object], workqueue.TypedRateLimitingInterface[ctrl.Request]) { } diff --git a/internal/controller/networking/externalproxy_status.go b/internal/controller/networking/externalproxy_status.go index b8d193d..2252044 100644 --- a/internal/controller/networking/externalproxy_status.go +++ b/internal/controller/networking/externalproxy_status.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package networking import ( diff --git a/internal/controller/networking/utils.go b/internal/controller/networking/utils.go index a7b09e6..5305b9e 100644 --- a/internal/controller/networking/utils.go +++ b/internal/controller/networking/utils.go @@ -52,9 +52,10 @@ var ( // If the service name is specified in the ExternalProxy spec, it returns that name. // Otherwise, it returns the name of the ExternalProxy object. func getServiceName(ep *ExternalProxy) string { - if ep.Spec.Service.Name != nil { - return *ep.Spec.Service.Name + if ep.Spec.Service.Name != "" { + return ep.Spec.Service.Name } + return ep.Name } @@ -102,8 +103,15 @@ func newEndpoints(_ context.Context, instance *ExternalProxy) *corev1.Endpoints } for _, backend := range instance.Spec.Backends { + addresses := make([]corev1.EndpointAddress, 0, len(backend.Addresses)) + for _, address := range backend.Addresses { + addresses = append(addresses, corev1.EndpointAddress{ + IP: address.IP, + }) + } + endpoints.Subsets = append(endpoints.Subsets, corev1.EndpointSubset{ - Addresses: backend.Addresses, + Addresses: addresses, Ports: backend.Ports, }) } @@ -191,13 +199,13 @@ func extractExternalProxyRevision[T client.Object](object T) int64 { // // It uses the generation of the controller object as the revision value. func applyExternalProxyRevision[R client.Object](controller *ExternalProxy, object R) R { - annotation := object.GetAnnotations() - if annotation == nil { - annotation = make(map[string]string) + annotations := object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) } - annotation[networking.ExternalProxyRevisionAnnotationKey] = strconv.FormatInt(controller.Generation, 10) - object.SetAnnotations(annotation) + annotations[networking.ExternalProxyRevisionAnnotationKey] = strconv.FormatInt(controller.Generation, 10) + object.SetAnnotations(annotations) return object } diff --git a/internal/expectations/expectations.go b/internal/expectations/expectations.go index 32c9bea..7b378f0 100644 --- a/internal/expectations/expectations.go +++ b/internal/expectations/expectations.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package expectations import ( diff --git a/internal/expectations/expectations_test.go b/internal/expectations/expectations_test.go index e2ad052..0e03519 100644 --- a/internal/expectations/expectations_test.go +++ b/internal/expectations/expectations_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package expectations import ( diff --git a/internal/fieldindex/fieldindex.go b/internal/fieldindex/fieldindex.go index 0907803..76dd61b 100644 --- a/internal/fieldindex/fieldindex.go +++ b/internal/fieldindex/fieldindex.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package fieldindex import ( diff --git a/internal/fieldindex/fieldindex_test.go b/internal/fieldindex/fieldindex_test.go index 633008d..296d8f2 100644 --- a/internal/fieldindex/fieldindex_test.go +++ b/internal/fieldindex/fieldindex_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package fieldindex import ( @@ -6,7 +22,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -14,25 +29,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" ) -var testenv *envtest.Environment +var env *envtest.Environment var cfg *rest.Config -var clientset *kubernetes.Clientset var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - testenv = &envtest.Environment{} + env = &envtest.Environment{} var err error - cfg, err = testenv.Start() - Expect(err).NotTo(HaveOccurred()) - - clientset, err = kubernetes.NewForConfig(cfg) + cfg, err = env.Start() Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { - Expect(testenv.Stop()).To(Succeed()) + Expect(env.Stop()).To(Succeed()) }) func TestRegisterFieldIndexes(t *testing.T) { @@ -42,7 +53,7 @@ func TestRegisterFieldIndexes(t *testing.T) { informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) - By("register field indexes for mobius controller") + By("register field indexes for controller") err = RegisterFieldIndexes(context.TODO(), informer) Expect(err).NotTo(HaveOccurred()) }) diff --git a/internal/patch/patch.go b/internal/patch/patch.go index 28063e1..c141987 100644 --- a/internal/patch/patch.go +++ b/internal/patch/patch.go @@ -1,12 +1,13 @@ package patch import ( + "encoding/json" + + "github.com/samber/lo" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/wjiec/mobius/pkg/must" ) // CreateTwoWayMergePatch creates a patch that can be passed to StrategicMergePatch between @@ -15,7 +16,7 @@ import ( func CreateTwoWayMergePatch[T client.Object](original, desired T, fns ...mergepatch.PreconditionFunc) (client.Patch, error) { var object T - data, err := strategicpatch.CreateTwoWayMergePatch(must.JsonMarshal(original), must.JsonMarshal(desired), object, fns...) + data, err := strategicpatch.CreateTwoWayMergePatch(lo.Must(json.Marshal(original)), lo.Must(json.Marshal(desired)), object, fns...) if err != nil { return nil, err } diff --git a/internal/patch/patch_test.go b/internal/patch/patch_test.go index 3f26d3d..7c0cbbf 100644 --- a/internal/patch/patch_test.go +++ b/internal/patch/patch_test.go @@ -5,11 +5,8 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - - networkingv1alpha1 "github.com/wjiec/mobius/api/networking/v1alpha1" ) const ( @@ -18,30 +15,23 @@ const ( func TestCreateTwoWayMergePatch(t *testing.T) { t.Run("normal", func(t *testing.T) { - original := &networkingv1alpha1.ExternalProxy{ + original := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: "example", - Namespace: "default", - CreationTimestamp: metav1.Time{}, + Name: "example", + Namespace: "default", Annotations: map[string]string{ TestRevisionAnnotationKey: "1", }, }, - Spec: networkingv1alpha1.ExternalProxySpec{ - Backends: []networkingv1alpha1.ExternalProxyBackend{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ { - Addresses: []corev1.EndpointAddress{ - {IP: "192.168.1.1"}, - }, - Ports: []corev1.EndpointPort{ - {Name: "http", Port: 80}, - }, + Name: "app", + Image: "nginx:alpine", }, - }, - Service: networkingv1alpha1.ExternalProxyService{ - Name: ptr.To("foobar"), - Ports: []corev1.ServicePort{ - {Name: "http", Port: 80}, + { + Name: "proxy", + Image: "nginx:alpine", }, }, }, @@ -49,42 +39,17 @@ func TestCreateTwoWayMergePatch(t *testing.T) { desired := original.DeepCopy() desired.Annotations[TestRevisionAnnotationKey] = "2" - desired.Spec.Service.Name = nil - desired.Spec.Ingress = &networkingv1alpha1.ExternalProxyIngress{ - Rules: []networkingv1alpha1.ExternalProxyIngressRule{ - {Host: "www.example.com"}, - }, - } - - patch, err := CreateTwoWayMergePatch(original, desired) - if assert.NoError(t, err) { - data, err := patch.Data(original) - if assert.NoError(t, err) { - t.Logf("Patch(%s): %s", patch.Type(), data) - } - } - }) - - t.Run("ingress", func(t *testing.T) { - original := &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example", - Namespace: "default", - }, - Spec: networkingv1.IngressSpec{ - IngressClassName: ptr.To("nginx"), - Rules: []networkingv1.IngressRule{ - {Host: "example.com"}, + desired.Spec.Containers[0].Name = "web" + desired.Spec.InitContainers = []corev1.Container{ + { + Name: "sysctl", + Image: "alpine:latest", + SecurityContext: &corev1.SecurityContext{ + Privileged: ptr.To(true), }, }, } - desired := original.DeepCopy() - desired.Spec.IngressClassName = nil - desired.Spec.Rules = []networkingv1.IngressRule{ - {Host: "www.example.com"}, - } - patch, err := CreateTwoWayMergePatch(original, desired) if assert.NoError(t, err) { data, err := patch.Data(original) @@ -95,12 +60,12 @@ func TestCreateTwoWayMergePatch(t *testing.T) { }) t.Run("unchanged", func(t *testing.T) { - original := &networkingv1.Ingress{} + original := &corev1.Pod{} patch, err := CreateTwoWayMergePatch(original, original) if assert.NoError(t, err) { data, err := patch.Data(original) if assert.NoError(t, err) { - t.Logf("Patch(%s): %#v", patch.Type(), data) + t.Logf("Patch(%s): %s", patch.Type(), data) } } }) diff --git a/internal/sync/singleton/singleton.go b/internal/sync/singleton/singleton.go index f10beb0..36cd38c 100644 --- a/internal/sync/singleton/singleton.go +++ b/internal/sync/singleton/singleton.go @@ -57,7 +57,7 @@ func (s *Singleton[C, R]) Sync(ctx context.Context, controller C) error { s.Expect(expectations.ControllerKeyFromCtx(ctx), expectations.ActionCreations, desiredObject.GetName()) if err = s.eventHandler.Create(ctx, desiredObject); err != nil { - s.Observe(expectations.ControllerKeyFromCtx(ctx), expectations.ActionDeletions, desiredObject.GetName()) + s.Observe(expectations.ControllerKeyFromCtx(ctx), expectations.ActionCreations, desiredObject.GetName()) return err } @@ -75,6 +75,7 @@ func (s *Singleton[C, R]) Sync(ctx context.Context, controller C) error { // Delete all objects that are inactivated. var eg multierror.Group for _, currObject := range ownerObjects { + //goland:noinspection GoDfaNilDereference We've already determined that activatedObject is not nil if !hasActivatedObject || currObject.GetUID() != activatedObject.GetUID() { eg.Go(func() error { return s.eventHandler.Delete(ctx, currObject) }) } diff --git a/internal/webhook/networking/v1alpha1/externalproxy_webhook.go b/internal/webhook/networking/v1alpha1/externalproxy_webhook.go new file mode 100644 index 0000000..fec6940 --- /dev/null +++ b/internal/webhook/networking/v1alpha1/externalproxy_webhook.go @@ -0,0 +1,499 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + netutils "k8s.io/utils/net" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + networkingv1alpha1 "github.com/wjiec/mobius/api/networking/v1alpha1" +) + +var ( + allowedTemplateObjectMetaFields = map[string]bool{ + "Name": true, + "Annotations": true, + "Labels": true, + } +) + +// SetupExternalProxyWebhookWithManager registers the webhook for ExternalProxy in the manager. +func SetupExternalProxyWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&networkingv1alpha1.ExternalProxy{}). + WithValidator(&ExternalProxyCustomValidator{}). + WithDefaulter(&ExternalProxyCustomDefaulter{ + DefaultServiceType: corev1.ServiceTypeClusterIP, + DefaultBackendPortProtocol: corev1.ProtocolTCP, + DefaultIngressPathType: networkingv1.PathTypeImplementationSpecific, + }). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-networking-laboys-org-v1alpha1-externalproxy,mutating=true,failurePolicy=fail,sideEffects=None,groups=networking.laboys.org,resources=externalproxies,verbs=create;update,versions=v1alpha1,name=mexternalproxy-v1alpha1.kb.io,admissionReviewVersions=v1 + +// ExternalProxyCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind ExternalProxy when those are created or updated. +type ExternalProxyCustomDefaulter struct { + DefaultServiceType corev1.ServiceType + DefaultBackendPortProtocol corev1.Protocol + DefaultIngressPathType networkingv1.PathType +} + +var _ webhook.CustomDefaulter = &ExternalProxyCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind ExternalProxy. +func (d *ExternalProxyCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + logger := logf.FromContext(ctx).WithName("externalproxy-defaulter") + + instance, ok := obj.(*networkingv1alpha1.ExternalProxy) + if !ok { + return fmt.Errorf("expected an ExternalProxy object but got %T", obj) + } + logger.Info("Defaulting for ExternalProxy", "name", instance.GetName()) + + d.applyDefaults(instance) + return nil +} + +// applyDefaults applies default values to ExternalProxy fields. +func (d *ExternalProxyCustomDefaulter) applyDefaults(ep *networkingv1alpha1.ExternalProxy) { + if len(ep.Spec.Service.Name) == 0 { + ep.Spec.Service.Name = ep.Name + } + if len(ep.Spec.Service.Type) == 0 { + ep.Spec.Service.Type = d.DefaultServiceType + } + + noServicePorts := len(ep.Spec.Service.Ports) == 0 + for i := range ep.Spec.Backends { + backend := &ep.Spec.Backends[i] + for port := range backend.Ports { + if len(backend.Ports[port].Protocol) == 0 { + backend.Ports[port].Protocol = d.DefaultBackendPortProtocol + } + + // If the port is not specified in the Service object, then we copy + // the port information from the first Backend into the Service. + if noServicePorts && i == 0 { + ep.Spec.Service.Ports = append(ep.Spec.Service.Ports, corev1.ServicePort{ + Name: backend.Ports[port].Name, + Protocol: backend.Ports[port].Protocol, + AppProtocol: backend.Ports[port].AppProtocol, + Port: backend.Ports[port].Port, + TargetPort: preferredTargetPort(&backend.Ports[port]), + }) + } + } + } + + if ingress := ep.Spec.Ingress; ingress != nil { + if len(ingress.Name) == 0 { + ingress.Name = ep.Name + } + + // If there is only one port, then we can generate default HTTP traffic rules for it + if len(ep.Spec.Service.Ports) == 1 && len(ingress.Rules) != 0 { + var ingressBackendServicePort networkingv1alpha1.ExternalProxyServiceBackendPort + // If we can use the name that's best, otherwise we'll have to use the port number + // to specify the destination of the traffic. + if len(ep.Spec.Service.Ports[0].Name) != 0 { + ingressBackendServicePort.Name = ep.Spec.Service.Ports[0].Name + } else { + ingressBackendServicePort.Number = ep.Spec.Service.Ports[0].Port + } + + for i := range ingress.Rules { + rule := &ingress.Rules[i] + if rule.HTTP == nil { + rule.HTTP = &networkingv1alpha1.ExternalProxyIngressHttpRuleValue{ + Paths: []networkingv1alpha1.ExternalProxyIngressHttpPath{ + {Path: "/"}, + }, + } + } + + if len(rule.HTTP.Paths) != 0 { + for path := range rule.HTTP.Paths { + if rule.HTTP.Paths[path].PathType == nil { + rule.HTTP.Paths[path].PathType = ptr.To(d.DefaultIngressPathType) + } + + if rule.HTTP.Paths[path].Backend == nil { + rule.HTTP.Paths[path].Backend = &networkingv1alpha1.ExternalProxyIngressBackend{ + Port: ingressBackendServicePort, + } + } + } + } + } + } + } +} + +func preferredTargetPort(port *corev1.EndpointPort) intstr.IntOrString { + if len(port.Name) != 0 { + return intstr.FromString(port.Name) + } + return intstr.FromInt32(port.Port) +} + +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-networking-laboys-org-v1alpha1-externalproxy,mutating=false,failurePolicy=fail,sideEffects=None,groups=networking.laboys.org,resources=externalproxies,verbs=create;update,versions=v1alpha1,name=vexternalproxy-v1alpha1.kb.io,admissionReviewVersions=v1 + +// ExternalProxyCustomValidator struct is responsible for validating the ExternalProxy resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type ExternalProxyCustomValidator struct{} + +var _ webhook.CustomValidator = &ExternalProxyCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type ExternalProxy. +func (v *ExternalProxyCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + logger := logf.FromContext(ctx).WithName("externalproxy-validator") + + instance, ok := obj.(*networkingv1alpha1.ExternalProxy) + if !ok { + return nil, fmt.Errorf("expected a ExternalProxy object but got %T", obj) + } + logger.Info("Validation for ExternalProxy upon creation", "name", instance.GetName()) + + return nil, v.validateExternalProxy(instance) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type ExternalProxy. +func (v *ExternalProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + logger := logf.FromContext(ctx).WithName("externalproxy-validator") + + instance, ok := newObj.(*networkingv1alpha1.ExternalProxy) + if !ok { + return nil, fmt.Errorf("expected a ExternalProxy object for the newObj but got %T", newObj) + } + logger.Info("Validation for ExternalProxy upon update", "name", instance.GetName()) + + return nil, v.validateExternalProxy(instance) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type ExternalProxy. +func (v *ExternalProxyCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// validateExternalProxy validates the fields of a ExternalProxy object. +func (v *ExternalProxyCustomValidator) validateExternalProxy(instance *networkingv1alpha1.ExternalProxy) error { + allErrors := validateExternalProxySpec(&instance.Spec, field.NewPath("spec")) + + if len(allErrors) != 0 { + return apierrors.NewInvalid( + networkingv1alpha1.GroupVersion.WithKind("ExternalProxy").GroupKind(), + instance.Name, allErrors) + } + return nil +} + +func validateExternalProxySpec(spec *networkingv1alpha1.ExternalProxySpec, path *field.Path) field.ErrorList { + var allErrors field.ErrorList + + allPortNames := sets.New[string]() + for _, port := range spec.Service.Ports { + allPortNames.Insert(port.Name) + } + + allErrors = append(allErrors, validateExternalProxyBackends(spec.Backends, allPortNames, path.Child("backends"))...) + allErrors = append(allErrors, validateExternalProxyService(&spec.Service, path.Child("service"))...) + if spec.Ingress != nil { + opts := IngressValidationOptions{ + AllowInvalidSecretName: false, + AllowInvalidWildcardHostRule: false, + } + allErrors = append(allErrors, validateExternalProxyIngress(spec.Ingress, path.Child("ingress"), opts)...) + } + + return allErrors +} + +func validateExternalProxyBackends(backends []networkingv1alpha1.ExternalProxyBackend, allPortNames sets.Set[string], path *field.Path) field.ErrorList { + var allErrors field.ErrorList + if len(backends) == 0 { + allErrors = append(allErrors, field.Required(path, "must be specified")) + } + + for i := range backends { + backend := &backends[i] + idxPath := path.Index(i) + + if len(backend.Addresses) == 0 { + allErrors = append(allErrors, field.Required(idxPath, "must specify at least one `address`")) + } + if len(backend.Ports) == 0 { + allErrors = append(allErrors, field.Required(idxPath, "must specify at least one `port`")) + } + + for addr := range backend.Addresses { + allErrors = append(allErrors, validateExternalProxyBackendAddress(&backend.Addresses[addr], idxPath.Child("addresses").Index(addr))...) + } + + requireName := len(backend.Ports) > 1 + for port := range backend.Ports { + portPath := idxPath.Child("ports").Index(port) + if requireName && !allPortNames.Has(backend.Ports[port].Name) { + allErrors = append(allErrors, field.Invalid(portPath.Child("name"), backend.Ports[port].Name, "should match the port in the Service")) + } + allErrors = append(allErrors, validateEndpointPort(&backend.Ports[port], requireName, portPath)...) + } + } + + return allErrors +} + +func validateExternalProxyBackendAddress(address *networkingv1alpha1.ExternalProxyBackendAddress, path *field.Path) field.ErrorList { + return ValidateNonSpecialIP(address.IP, path.Child("ip")) +} + +func validateExternalProxyService(svc *networkingv1alpha1.ExternalProxyService, path *field.Path) field.ErrorList { + allErrors := validateExternalProxySubObjectMetadata(&svc.ObjectMeta, apimachineryvalidation.NameIsDNS1035Label, path.Child("metadata")) + + // If the user does not specify the port of the Service, we will automatically populate + // the Service's port information in Defaulter. + if len(svc.Ports) == 0 { + allErrors = append(allErrors, field.Required(path.Child("ports"), "must specify at least one `port`")) + } + + requireName := len(svc.Ports) > 1 + allPortNames := sets.Set[string]{} + for i := range svc.Ports { + portPath := path.Child("ports").Index(i) + allErrors = append(allErrors, validateExternalProxyServicePort(&svc.Ports[i], requireName, &allPortNames, portPath)...) + } + + return allErrors +} + +func validateExternalProxySubObjectMetadata(objectMeta *metav1.ObjectMeta, nameFn apimachineryvalidation.ValidateNameFunc, path *field.Path) field.ErrorList { + var allErrors field.ErrorList + allErrors = append(allErrors, apimachineryvalidation.ValidateObjectMeta(objectMeta, false, nameFn, path)...) + allErrors = append(allErrors, apimachineryvalidation.ValidateAnnotations(objectMeta.Annotations, path.Child("annotations"))...) + allErrors = append(allErrors, unversionedvalidation.ValidateLabels(objectMeta.Labels, path.Child("labels"))...) + // All other fields are not supported and thus must not be set + // to avoid confusion. We could reject individual fields, + // but then adding a new one to ObjectMeta wouldn't be checked + // unless this code gets updated. Instead, we ensure that + // only allowed fields are set via reflection. + allErrors = append(allErrors, validateFieldAllowList(*objectMeta, allowedTemplateObjectMetaFields, "cannot be set", path)...) + + return allErrors +} + +func validateExternalProxyServicePort(sp *corev1.ServicePort, requireName bool, allPortName *sets.Set[string], path *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if requireName && len(sp.Name) == 0 { + allErrs = append(allErrs, field.Required(path.Child("name"), "")) + } else if len(sp.Name) != 0 { + allErrs = append(allErrs, ValidateDNS1123Label(sp.Name, path.Child("name"))...) + if allPortName.Has(sp.Name) { + allErrs = append(allErrs, field.Duplicate(path.Child("name"), sp.Name)) + } else { + allPortName.Insert(sp.Name) + } + } + + for _, msg := range validation.IsValidPortNum(int(sp.Port)) { + allErrs = append(allErrs, field.Invalid(path.Child("port"), sp.Port, msg)) + } + + if len(sp.Protocol) == 0 { + allErrs = append(allErrs, field.Required(path.Child("protocol"), "")) + } else if !supportedPortProtocols.Has(sp.Protocol) { + allErrs = append(allErrs, field.NotSupported(path.Child("protocol"), sp.Protocol, sets.List(supportedPortProtocols))) + } + + allErrs = append(allErrs, ValidatePortNumOrName(sp.TargetPort, path.Child("targetPort"))...) + + if sp.AppProtocol != nil { + allErrs = append(allErrs, ValidateQualifiedName(*sp.AppProtocol, path.Child("appProtocol"))...) + } + + return allErrs +} + +func validateExternalProxyIngress(ing *networkingv1alpha1.ExternalProxyIngress, path *field.Path, opts IngressValidationOptions) field.ErrorList { + var allErrors field.ErrorList + allErrors = append(allErrors, validateExternalProxySubObjectMetadata(&ing.ObjectMeta, apimachineryvalidation.NameIsDNSSubdomain, path.Child("metadata"))...) + + if ing.IngressClassName != nil { + for _, msg := range apimachineryvalidation.NameIsDNSSubdomain(*ing.IngressClassName, false) { + allErrors = append(allErrors, field.Invalid(path.Child("ingressClassName"), *ing.IngressClassName, msg)) + } + } + + if ing.DefaultBackend != nil { + allErrors = append(allErrors, validateExternalProxyIngressBackend(ing.DefaultBackend, path.Child("defaultBackend"))...) + } + + if len(ing.Rules) == 0 && ing.DefaultBackend == nil { + allErrors = append(allErrors, field.Invalid(path, ing.Rules, + "either `defaultBackend` or `rules` must be specified")) + } + + if len(ing.TLS) > 0 { + allErrors = append(allErrors, validateIngressTLS(ing.TLS, path.Child("tls"), opts)...) + } + + if len(ing.Rules) != 0 { + allErrors = append(allErrors, validateExternalProxyIngressRule(ing.Rules, path.Child("rules"), opts)...) + } + + return allErrors +} + +func validateExternalProxyIngressRule(rule []networkingv1alpha1.ExternalProxyIngressRule, path *field.Path, opts IngressValidationOptions) field.ErrorList { + var allErrors field.ErrorList + for i, elem := range rule { + wildcardHost := false + if len(elem.Host) > 0 { + if isIP := netutils.ParseIPSloppy(elem.Host) != nil; isIP { + allErrors = append(allErrors, field.Invalid(path.Index(i).Child("host"), elem.Host, "must be a DNS name, not an IP address")) + } + + if strings.Contains(elem.Host, "*") { + for _, msg := range validation.IsWildcardDNS1123Subdomain(elem.Host) { + allErrors = append(allErrors, field.Invalid(path.Index(i).Child("host"), elem.Host, msg)) + } + wildcardHost = true + } else { + for _, msg := range validation.IsDNS1123Subdomain(elem.Host) { + allErrors = append(allErrors, field.Invalid(path.Index(i).Child("host"), elem.Host, msg)) + } + } + } + + if !wildcardHost || !opts.AllowInvalidWildcardHostRule { + allErrors = append(allErrors, validateExternalProxyIngressHttpRuleValue(elem.HTTP, path.Index(i))...) + } + } + + return allErrors +} + +func validateExternalProxyIngressHttpRuleValue(value *networkingv1alpha1.ExternalProxyIngressHttpRuleValue, path *field.Path) field.ErrorList { + var allErrors field.ErrorList + if value != nil { + if len(value.Paths) == 0 { + allErrors = append(allErrors, field.Required(path.Child("paths"), "")) + } + + for i := range value.Paths { + allErrors = append(allErrors, validateExternalProxyIngressHttpPath(&value.Paths[i], path.Child("paths"))...) + } + } + + return allErrors +} + +var ( + supportedPathTypes = sets.NewString( + string(networkingv1.PathTypeExact), + string(networkingv1.PathTypePrefix), + string(networkingv1.PathTypeImplementationSpecific), + ) + invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F"} + invalidPathSuffixes = []string{"/..", "/."} +) + +func validateExternalProxyIngressHttpPath(hp *networkingv1alpha1.ExternalProxyIngressHttpPath, path *field.Path) field.ErrorList { + allErrors := field.ErrorList{} + + if hp.PathType == nil { + return append(allErrors, field.Required(path.Child("pathType"), "pathType must be specified")) + } + + switch *hp.PathType { + case networkingv1.PathTypeExact, networkingv1.PathTypePrefix: + if !strings.HasPrefix(hp.Path, "/") { + allErrors = append(allErrors, field.Invalid(path.Child("path"), hp.Path, "must be an absolute path")) + } + if len(hp.Path) > 0 { + for _, invalidSeq := range invalidPathSequences { + if strings.Contains(hp.Path, invalidSeq) { + allErrors = append(allErrors, field.Invalid(path.Child("path"), hp.Path, fmt.Sprintf("must not contain '%s'", invalidSeq))) + } + } + + for _, invalidSuff := range invalidPathSuffixes { + if strings.HasSuffix(hp.Path, invalidSuff) { + allErrors = append(allErrors, field.Invalid(path.Child("path"), hp.Path, fmt.Sprintf("cannot end with '%s'", invalidSuff))) + } + } + } + case networkingv1.PathTypeImplementationSpecific: + if len(hp.Path) > 0 { + if !strings.HasPrefix(hp.Path, "/") { + allErrors = append(allErrors, field.Invalid(path.Child("path"), hp.Path, "must be an absolute path")) + } + } + default: + allErrors = append(allErrors, field.NotSupported(path.Child("pathType"), *hp.PathType, supportedPathTypes.List())) + } + + allErrors = append(allErrors, validateExternalProxyIngressBackend(hp.Backend, path.Child("backend"))...) + return allErrors +} + +func validateExternalProxyIngressBackend(be *networkingv1alpha1.ExternalProxyIngressBackend, path *field.Path) field.ErrorList { + var allErrors field.ErrorList + + hasPortName := len(be.Port.Name) > 0 + hasPortNumber := be.Port.Number != 0 + if hasPortName && hasPortNumber { + allErrors = append(allErrors, field.Invalid(path, "", "cannot set both port name & port number")) + } else if hasPortName { + for _, msg := range validation.IsValidPortName(be.Port.Name) { + allErrors = append(allErrors, field.Invalid(path.Child("service", "port", "name"), be.Port.Name, msg)) + } + } else if hasPortNumber { + for _, msg := range validation.IsValidPortNum(int(be.Port.Number)) { + allErrors = append(allErrors, field.Invalid(path.Child("service", "port", "number"), be.Port.Number, msg)) + } + } else { + allErrors = append(allErrors, field.Required(path, "port name or number is required")) + } + + return allErrors +} diff --git a/internal/webhook/networking/v1alpha1/externalproxy_webhook_test.go b/internal/webhook/networking/v1alpha1/externalproxy_webhook_test.go new file mode 100644 index 0000000..aa11589 --- /dev/null +++ b/internal/webhook/networking/v1alpha1/externalproxy_webhook_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + networkingv1alpha1 "github.com/wjiec/mobius/api/networking/v1alpha1" +) + +var _ = Describe("ExternalProxy Webhook", func() { + var ( + obj *networkingv1alpha1.ExternalProxy + oldObj *networkingv1alpha1.ExternalProxy + validator ExternalProxyCustomValidator + defaulter ExternalProxyCustomDefaulter + ) + + BeforeEach(func() { + obj = &networkingv1alpha1.ExternalProxy{} + oldObj = &networkingv1alpha1.ExternalProxy{} + validator = ExternalProxyCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + + defaulter = ExternalProxyCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + + objMeta := metav1.ObjectMeta{Name: "foo", Namespace: "default"} + obj.ObjectMeta = objMeta + oldObj.ObjectMeta = objMeta + }) + + AfterEach(func() {}) + + Context("When creating ExternalProxy under Defaulting Webhook", func() { + It("Should apply defaults when a required field is empty", func() { + By("the name of the service is not set") + obj.Spec.Service.Name = "" + + By("calling the Default method to apply defaults") + Expect(defaulter.Default(ctx, obj)).To(Succeed()) + + By("checking that the default values are set") + Expect(obj.Spec.Service.Name).To(Equal(obj.Name)) + }) + }) + + Context("When creating or updating ExternalProxy under Validating Webhook", func() { + It("Should deny creation if a required field is missing", func() { + By("simulating an invalid creation scenario") + obj.Spec.Service.Name = "" + Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + }) + }) + +}) diff --git a/internal/webhook/networking/v1alpha1/k8s_validation.go b/internal/webhook/networking/v1alpha1/k8s_validation.go new file mode 100644 index 0000000..8be1b3c --- /dev/null +++ b/internal/webhook/networking/v1alpha1/k8s_validation.go @@ -0,0 +1,194 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" + + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + netutils "k8s.io/utils/net" +) + +func ValidateDNS1123Label(value string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for _, msg := range validation.IsDNS1123Label(value) { + allErrs = append(allErrs, field.Invalid(fldPath, value, msg)) + } + return allErrs +} + +// ValidateNonSpecialIP is used to validate Endpoints, EndpointSlices, and +// external IPs. Specifically, this disallows unspecified and loopback addresses +// are nonsensical and link-local addresses tend to be used for node-centric +// purposes (e.g. metadata service). +// +// IPv6 references +// - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml +// - https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml +func ValidateNonSpecialIP(ipAddress string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + ip := netutils.ParseIPSloppy(ipAddress) + if ip == nil { + allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "must be a valid IP address")) + return allErrs + } + + if ip.IsUnspecified() { + allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, fmt.Sprintf("may not be unspecified (%v)", ipAddress))) + } + if ip.IsLoopback() { + allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the loopback range (127.0.0.0/8, ::1/128)")) + } + if ip.IsLinkLocalUnicast() { + allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local range (169.254.0.0/16, fe80::/10)")) + } + if ip.IsLinkLocalMulticast() { + allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24, ff02::/10)")) + } + return allErrs +} + +// ValidateQualifiedName validates if name is what Kubernetes calls a "qualified name". +func ValidateQualifiedName(value string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for _, msg := range validation.IsQualifiedName(value) { + allErrs = append(allErrs, field.Invalid(fldPath, value, msg)) + } + return allErrs +} + +var supportedPortProtocols = sets.New( + corev1.ProtocolTCP, + corev1.ProtocolUDP, + corev1.ProtocolSCTP, +) + +func validateEndpointPort(port *corev1.EndpointPort, requireName bool, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if requireName && len(port.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } else if len(port.Name) != 0 { + allErrs = append(allErrs, ValidateDNS1123Label(port.Name, fldPath.Child("name"))...) + } + + for _, msg := range validation.IsValidPortNum(int(port.Port)) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("port"), port.Port, msg)) + } + + if len(port.Protocol) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("protocol"), "")) + } else if !supportedPortProtocols.Has(port.Protocol) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("protocol"), port.Protocol, sets.List(supportedPortProtocols))) + } + + if port.AppProtocol != nil { + allErrs = append(allErrs, ValidateQualifiedName(*port.AppProtocol, fldPath.Child("appProtocol"))...) + } + + return allErrs +} + +// ValidateFieldAcceptList checks that only allowed fields are set. +// The value must be a struct (not a pointer to a struct!). +func validateFieldAllowList(value interface{}, allowedFields map[string]bool, errorText string, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + reflectType, reflectValue := reflect.TypeOf(value), reflect.ValueOf(value) + for i := 0; i < reflectType.NumField(); i++ { + f := reflectType.Field(i) + if allowedFields[f.Name] { + continue + } + + // Compare the value of this field to its zero value to determine if it has been set + if !reflect.DeepEqual(reflectValue.Field(i).Interface(), reflect.Zero(f.Type).Interface()) { + r, n := utf8.DecodeRuneInString(f.Name) + lcName := string(unicode.ToLower(r)) + f.Name[n:] + allErrs = append(allErrs, field.Forbidden(fldPath.Child(lcName), errorText)) + } + } + + return allErrs +} + +func ValidatePortNumOrName(port intstr.IntOrString, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if port.Type == intstr.Int { + for _, msg := range validation.IsValidPortNum(port.IntValue()) { + allErrs = append(allErrs, field.Invalid(fldPath, port.IntValue(), msg)) + } + } else if port.Type == intstr.String { + for _, msg := range validation.IsValidPortName(port.StrVal) { + allErrs = append(allErrs, field.Invalid(fldPath, port.StrVal, msg)) + } + } else { + allErrs = append(allErrs, field.InternalError(fldPath, fmt.Errorf("unknown type: %v", port.Type))) + } + return allErrs +} + +// IngressValidationOptions cover beta to GA transitions for HTTP PathType +type IngressValidationOptions struct { + // AllowInvalidSecretName indicates whether spec.tls[*].secretName values that are not valid Secret names should be allowed + AllowInvalidSecretName bool + + // AllowInvalidWildcardHostRule indicates whether invalid rule values are allowed in rules with wildcard hostnames + AllowInvalidWildcardHostRule bool +} + +func validateIngressTLS(ingressTLS []networkingv1.IngressTLS, fldPath *field.Path, opts IngressValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + // the wildcard spec from RFC 6125 into account. + for tlsIndex, tls := range ingressTLS { + for i, host := range tls.Hosts { + if strings.Contains(host, "*") { + for _, msg := range validation.IsWildcardDNS1123Subdomain(host) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(tlsIndex).Child("hosts").Index(i), host, msg)) + } + continue + } + for _, msg := range validation.IsDNS1123Subdomain(host) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(tlsIndex).Child("hosts").Index(i), host, msg)) + } + } + + if !opts.AllowInvalidSecretName { + for _, msg := range validateTLSSecretName(tls.SecretName) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(tlsIndex).Child("secretName"), tls.SecretName, msg)) + } + } + } + + return allErrs +} + +func validateTLSSecretName(name string) []string { + if len(name) == 0 { + return nil + } + return apimachineryvalidation.NameIsDNSSubdomain(name, false) +} diff --git a/internal/webhook/networking/v1alpha1/webhook_suite_test.go b/internal/webhook/networking/v1alpha1/webhook_suite_test.go new file mode 100644 index 0000000..0f8b672 --- /dev/null +++ b/internal/webhook/networking/v1alpha1/webhook_suite_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2024 Jayson Wang. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + + networkingv1alpha1 "github.com/wjiec/mobius/api/networking/v1alpha1" + + // +kubebuilder:scaffold:imports + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")}, + }, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = networkingv1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupExternalProxyWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/pkg/must/json.go b/pkg/must/json.go deleted file mode 100644 index ddba110..0000000 --- a/pkg/must/json.go +++ /dev/null @@ -1,15 +0,0 @@ -package must - -import "encoding/json" - -// JsonMarshal marshals the given value v to a JSON format data. -// -// If marshalling fails, it panics with the encountered error. -func JsonMarshal[T any](v T) []byte { - data, err := json.Marshal(v) - if err != nil { - panic(err) - } - - return data -} diff --git a/pkg/must/json_test.go b/pkg/must/json_test.go deleted file mode 100644 index 2fc430a..0000000 --- a/pkg/must/json_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package must - -import ( - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" -) - -func TestJsonMarshal(t *testing.T) { - t.Run("normal", func(t *testing.T) { - assert.NotPanics(t, func() { - JsonMarshal(&corev1.Pod{}) - }) - }) - - t.Run("nil", func(t *testing.T) { - assert.NotPanics(t, func() { - JsonMarshal[*corev1.Pod](nil) - }) - }) - - t.Run("panic", func(t *testing.T) { - assert.NotPanics(t, func() { - JsonMarshal(struct{ unexported int }{}) - }) - }) -} diff --git a/test/utils/utils.go b/test/utils/utils.go index a54d8a1..d65607a 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -35,7 +35,7 @@ const ( ) func warnError(err error) { - fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) + _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. @@ -52,12 +52,12 @@ func Run(cmd *exec.Cmd) ([]byte, error) { cmd.Dir = dir if err := os.Chdir(cmd.Dir); err != nil { - fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) } cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") - fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() if err != nil { return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))