├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_loadbalancers.yaml │ │ ├── cainjection_in_loadbalancerconfigs.yaml │ │ ├── webhook_in_loadbalancers.yaml │ │ └── webhook_in_loadbalancerconfigs.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ ├── lb.paperlb.com_loadbalancerconfigs.yaml │ │ └── lb.paperlb.com_loadbalancers.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── loadbalancer_viewer_role.yaml │ ├── kustomization.yaml │ ├── loadbalancerconfig_viewer_role.yaml │ ├── loadbalancer_editor_role.yaml │ ├── loadbalancerconfig_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── samples │ ├── lb_v1alpha1_loadbalancer.yaml │ └── lb_v1alpha1_loadbalancerconfig.yaml └── manifests │ └── paperlb.yaml ├── logo-color.png ├── paperlb-archi.png ├── paperlb-low-resolution.png ├── mocks ├── gen_mocks.sh └── mock_http_lb_updater.go ├── tools └── tools.go ├── .dockerignore ├── demo ├── load_balancer_config.yaml ├── service.yaml └── deployment.yaml ├── .gitignore ├── hack └── boilerplate.go.txt ├── PROJECT ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── loadbalancerconfig_types.go │ ├── loadbalancer_types.go │ └── zz_generated.deepcopy.go ├── Dockerfile ├── services ├── http_lb_updater_test.go └── http_lb_updater.go ├── .github └── workflows │ └── ci.yaml ├── go.mod ├── controllers ├── suite_test.go ├── loadbalancer_controller_test.go ├── loadbalancer_controller.go ├── service_controller_test.go └── service_controller.go ├── main.go ├── README.md ├── Makefile ├── LICENSE └── go.sum /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didil/paperlb/HEAD/logo-color.png -------------------------------------------------------------------------------- /paperlb-archi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didil/paperlb/HEAD/paperlb-archi.png -------------------------------------------------------------------------------- /paperlb-low-resolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didil/paperlb/HEAD/paperlb-low-resolution.png -------------------------------------------------------------------------------- /mocks/gen_mocks.sh: -------------------------------------------------------------------------------- 1 | 2 | bin/mockgen -source services/http_lb_updater.go -destination mocks/mock_http_lb_updater.go -package mocks -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/golang/mock/mockgen" 8 | ) 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: quay.io/didil/paperlb 8 | newTag: 6202a2c 9 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /demo/load_balancer_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: lb.paperlb.com/v1alpha1 2 | kind: LoadBalancerConfig 3 | metadata: 4 | name: default-lb-config 5 | namespace: paperlb-system 6 | spec: 7 | default: true 8 | httpUpdaterURL: "http://192.168.64.1:3000/api/v1/lb" 9 | host: "192.168.64.1" 10 | portRange: 11 | low: 8100 12 | high: 8200 -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_loadbalancers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: loadbalancers.lb.paperlb.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_loadbalancerconfigs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: loadbalancerconfigs.lb.paperlb.com 8 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/lb_v1alpha1_loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: lb.paperlb.com/v1alpha1 2 | kind: LoadBalancer 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: loadbalancer 6 | app.kubernetes.io/instance: loadbalancer-sample 7 | app.kubernetes.io/part-of: paperlb 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: paperlb 10 | name: loadbalancer-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /demo/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: k8s-pod-info-api 6 | name: k8s-pod-info-api-service 7 | #optional annotation to use a config different than the default config 8 | #annotations: 9 | # lb.paperlb.com/config-name: "my-special-config" 10 | spec: 11 | ports: 12 | - port: 5000 13 | protocol: TCP 14 | targetPort: 4000 15 | selector: 16 | app: k8s-pod-info-api 17 | type: LoadBalancer -------------------------------------------------------------------------------- /config/samples/lb_v1alpha1_loadbalancerconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: lb.paperlb.com/v1alpha1 2 | kind: LoadBalancerConfig 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: loadbalancerconfig 6 | app.kubernetes.io/instance: loadbalancerconfig-sample 7 | app.kubernetes.io/part-of: paperlb 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: paperlb 10 | name: loadbalancerconfig-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | Dockerfile.cross 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_loadbalancers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: loadbalancers.lb.paperlb.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_loadbalancerconfigs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: loadbalancerconfigs.lb.paperlb.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: paperlb 9 | app.kubernetes.io/part-of: paperlb 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/loadbalancer_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view loadbalancers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: loadbalancer-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: loadbalancer-viewer-role 13 | rules: 14 | - apiGroups: 15 | - lb.paperlb.com 16 | resources: 17 | - loadbalancers 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - lb.paperlb.com 24 | resources: 25 | - loadbalancers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/loadbalancerconfig_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view loadbalancerconfigs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: loadbalancerconfig-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: loadbalancerconfig-viewer-role 13 | rules: 14 | - apiGroups: 15 | - lb.paperlb.com 16 | resources: 17 | - loadbalancerconfigs 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - lb.paperlb.com 24 | resources: 25 | - loadbalancerconfigs/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: paperlb.com 6 | layout: 7 | - go.kubebuilder.io/v3 8 | projectName: paperlb 9 | repo: github.com/didil/paperlb 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: paperlb.com 16 | group: lb 17 | kind: LoadBalancer 18 | path: github.com/didil/paperlb/api/v1alpha1 19 | version: v1alpha1 20 | - api: 21 | crdVersion: v1 22 | namespaced: true 23 | domain: paperlb.com 24 | group: lb 25 | kind: LoadBalancerConfig 26 | path: github.com/didil/paperlb/api/v1alpha1 27 | version: v1alpha1 28 | version: "3" 29 | -------------------------------------------------------------------------------- /config/rbac/loadbalancer_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit loadbalancers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: loadbalancer-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: loadbalancer-editor-role 13 | rules: 14 | - apiGroups: 15 | - lb.paperlb.com 16 | resources: 17 | - loadbalancers 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - lb.paperlb.com 28 | resources: 29 | - loadbalancers/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/loadbalancerconfig_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit loadbalancerconfigs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: loadbalancerconfig-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: loadbalancerconfig-editor-role 13 | rules: 14 | - apiGroups: 15 | - lb.paperlb.com 16 | resources: 17 | - loadbalancerconfigs 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - lb.paperlb.com 28 | resources: 29 | - loadbalancerconfigs/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: paperlb 12 | app.kubernetes.io/part-of: paperlb 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/lb.paperlb.com_loadbalancers.yaml 6 | - bases/lb.paperlb.com_loadbalancerconfigs.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- patches/webhook_in_loadbalancers.yaml 13 | #- patches/webhook_in_loadbalancerconfigs.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_loadbalancers.yaml 19 | #- patches/cainjection_in_loadbalancerconfigs.yaml 20 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /demo/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: k8s-pod-info-api 6 | name: k8s-pod-info-api 7 | spec: 8 | replicas: 4 9 | selector: 10 | matchLabels: 11 | app: k8s-pod-info-api 12 | template: 13 | metadata: 14 | labels: 15 | app: k8s-pod-info-api 16 | spec: 17 | containers: 18 | - image: quay.io/didil/k8s-pod-info-api:21b1240 19 | name: k8s-pod-info-api 20 | env: 21 | - name: "PORT" 22 | value: "4000" 23 | - name: POD_NAME 24 | valueFrom: 25 | fieldRef: 26 | fieldPath: metadata.name 27 | - name: POD_IP 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: status.podIP 31 | - name: POD_NAMESPACE 32 | valueFrom: 33 | fieldRef: 34 | fieldPath: metadata.namespace 35 | - name: POD_SERVICE_ACCOUNT_NAME 36 | valueFrom: 37 | fieldRef: 38 | fieldPath: spec.serviceAccountName 39 | - name: NODE_NAME 40 | valueFrom: 41 | fieldRef: 42 | fieldPath: spec.nodeName 43 | ports: 44 | - containerPort: 3000 45 | 46 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the lb v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=lb.paperlb.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "lb.paperlb.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.19 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY main.go main.go 16 | COPY api/ api/ 17 | COPY controllers/ controllers/ 18 | COPY services/ services/ 19 | 20 | # Build 21 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 22 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 23 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 24 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 25 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go 26 | 27 | # Use distroless as minimal base image to package the manager binary 28 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 29 | FROM gcr.io/distroless/static:nonroot 30 | WORKDIR / 31 | COPY --from=builder /workspace/manager . 32 | USER 65532:65532 33 | 34 | ENTRYPOINT ["/manager"] 35 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - lb.paperlb.com 10 | resources: 11 | - loadbalancerconfigs 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - lb.paperlb.com 22 | resources: 23 | - loadbalancerconfigs/status 24 | verbs: 25 | - get 26 | - patch 27 | - update 28 | - apiGroups: 29 | - lb.paperlb.com 30 | resources: 31 | - loadbalancers 32 | verbs: 33 | - create 34 | - delete 35 | - get 36 | - list 37 | - patch 38 | - update 39 | - watch 40 | - apiGroups: 41 | - lb.paperlb.com 42 | resources: 43 | - loadbalancers/finalizers 44 | verbs: 45 | - update 46 | - apiGroups: 47 | - lb.paperlb.com 48 | resources: 49 | - loadbalancers/status 50 | verbs: 51 | - get 52 | - patch 53 | - update 54 | - apiGroups: 55 | - v1 56 | resources: 57 | - nodes 58 | verbs: 59 | - get 60 | - list 61 | - watch 62 | - apiGroups: 63 | - v1 64 | resources: 65 | - nodes/status 66 | verbs: 67 | - get 68 | - apiGroups: 69 | - v1 70 | resources: 71 | - services 72 | verbs: 73 | - create 74 | - delete 75 | - get 76 | - list 77 | - patch 78 | - update 79 | - watch 80 | - apiGroups: 81 | - v1 82 | resources: 83 | - services/finalizers 84 | verbs: 85 | - update 86 | - apiGroups: 87 | - v1 88 | resources: 89 | - services/status 90 | verbs: 91 | - get 92 | - patch 93 | - update 94 | -------------------------------------------------------------------------------- /services/http_lb_updater_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestHTTPLBUpdaterUpdate(t *testing.T) { 14 | u := NewHTTPLBUpdaterClient() 15 | 16 | params := &UpdateParams{ 17 | BackendName: "namespace-a_myservice", 18 | LBPort: 9000, 19 | LBProtocol: "tcp", 20 | UpstreamServers: []UpstreamServer{ 21 | { 22 | Host: "192.168.101.2", 23 | Port: 5014, 24 | }, 25 | { 26 | Host: "192.168.101.3", 27 | Port: 5014, 28 | }, 29 | }, 30 | ProxyTimeoutSeconds: 6, 31 | ProxyConnectTimeoutSeconds: 3, 32 | } 33 | 34 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | p := &UpdateParams{} 36 | 37 | err := json.NewDecoder(r.Body).Decode(p) 38 | assert.NoError(t, err) 39 | 40 | assert.Equal(t, p, params) 41 | 42 | w.Write([]byte(`{}`)) 43 | })) 44 | 45 | err := u.Update(context.Background(), s.URL, params) 46 | assert.NoError(t, err) 47 | } 48 | 49 | func TestHTTPLBUpdaterDelete(t *testing.T) { 50 | u := NewHTTPLBUpdaterClient() 51 | 52 | params := &DeleteParams{ 53 | BackendName: "namespace-a_myservice", 54 | } 55 | 56 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | p := &DeleteParams{} 58 | 59 | err := json.NewDecoder(r.Body).Decode(p) 60 | assert.NoError(t, err) 61 | 62 | assert.Equal(t, p, params) 63 | 64 | w.Write([]byte(`{}`)) 65 | })) 66 | 67 | err := u.Delete(context.Background(), s.URL, params) 68 | assert.NoError(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | affinity: 12 | nodeAffinity: 13 | requiredDuringSchedulingIgnoredDuringExecution: 14 | nodeSelectorTerms: 15 | - matchExpressions: 16 | - key: kubernetes.io/arch 17 | operator: In 18 | values: 19 | - amd64 20 | - arm64 21 | - ppc64le 22 | - s390x 23 | - key: kubernetes.io/os 24 | operator: In 25 | values: 26 | - linux 27 | containers: 28 | - name: kube-rbac-proxy 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - "ALL" 34 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 35 | args: 36 | - "--secure-listen-address=0.0.0.0:8443" 37 | - "--upstream=http://127.0.0.1:8080/" 38 | - "--logtostderr=true" 39 | - "--v=0" 40 | ports: 41 | - containerPort: 8443 42 | protocol: TCP 43 | name: https 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 128Mi 48 | requests: 49 | cpu: 5m 50 | memory: 64Mi 51 | - name: manager 52 | args: 53 | - "--health-probe-bind-address=:8081" 54 | - "--metrics-bind-address=127.0.0.1:8080" 55 | - "--leader-elect" 56 | -------------------------------------------------------------------------------- /mocks/mock_http_lb_updater.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: services/http_lb_updater.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | services "github.com/didil/paperlb/services" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockHTTPLBUpdaterClient is a mock of HTTPLBUpdaterClient interface. 16 | type MockHTTPLBUpdaterClient struct { 17 | ctrl *gomock.Controller 18 | recorder *MockHTTPLBUpdaterClientMockRecorder 19 | } 20 | 21 | // MockHTTPLBUpdaterClientMockRecorder is the mock recorder for MockHTTPLBUpdaterClient. 22 | type MockHTTPLBUpdaterClientMockRecorder struct { 23 | mock *MockHTTPLBUpdaterClient 24 | } 25 | 26 | // NewMockHTTPLBUpdaterClient creates a new mock instance. 27 | func NewMockHTTPLBUpdaterClient(ctrl *gomock.Controller) *MockHTTPLBUpdaterClient { 28 | mock := &MockHTTPLBUpdaterClient{ctrl: ctrl} 29 | mock.recorder = &MockHTTPLBUpdaterClientMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockHTTPLBUpdaterClient) EXPECT() *MockHTTPLBUpdaterClientMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Delete mocks base method. 39 | func (m *MockHTTPLBUpdaterClient) Delete(ctx context.Context, url string, params *services.DeleteParams) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Delete", ctx, url, params) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Delete indicates an expected call of Delete. 47 | func (mr *MockHTTPLBUpdaterClientMockRecorder) Delete(ctx, url, params interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockHTTPLBUpdaterClient)(nil).Delete), ctx, url, params) 50 | } 51 | 52 | // Update mocks base method. 53 | func (m *MockHTTPLBUpdaterClient) Update(ctx context.Context, url string, params *services.UpdateParams) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Update", ctx, url, params) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // Update indicates an expected call of Update. 61 | func (mr *MockHTTPLBUpdaterClientMockRecorder) Update(ctx, url, params interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockHTTPLBUpdaterClient)(nil).Update), ctx, url, params) 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | [push, pull_request] 5 | jobs: 6 | build_app: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - name: Set up Go 10 | uses: actions/setup-go@v3 11 | with: 12 | go-version: 1.19 13 | 14 | - id: go-cache-paths 15 | run: | 16 | echo "::set-output name=go-build::$(go env GOCACHE)" 17 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 18 | 19 | - uses: actions/checkout@v3 20 | 21 | - name: Go Build Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ${{ steps.go-cache-paths.outputs.go-build }} 25 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 26 | 27 | - name: Go Mod Cache 28 | uses: actions/cache@v3 29 | with: 30 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 31 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 32 | 33 | - name: Build 34 | run: GOFLAGS=-v make build 35 | 36 | - name: Test 37 | run: GOFLAGS=-v make test 38 | 39 | 40 | build_container_image: 41 | needs: build_app 42 | runs-on: ubuntu-22.04 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Set up QEMU 47 | if: ${{ github.ref == 'refs/heads/main' }} 48 | uses: docker/setup-qemu-action@v2 49 | with: 50 | platforms: 'arm64' 51 | 52 | - name: Set up Docker Buildx 53 | uses: docker/setup-buildx-action@v2 54 | 55 | - name: Login to quay.io 56 | uses: docker/login-action@v2 57 | with: 58 | registry: quay.io 59 | username: ${{ secrets.QUAY_USERNAME }} 60 | password: ${{ secrets.QUAY_ROBOT_TOKEN }} 61 | 62 | - name: Set output vars 63 | id: set_output_vars 64 | run: echo "short_sha=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT 65 | 66 | - name: Build 67 | if: ${{ github.ref != 'refs/heads/main' }} 68 | uses: docker/build-push-action@v4 69 | with: 70 | context: . 71 | cache-from: type=gha 72 | cache-to: type=gha,mode=max 73 | push: false 74 | platforms: linux/amd64 75 | tags: ${{ vars.DOCKER_IMAGE_NAME }}:${{ steps.set_output_vars.outputs.short_sha }} 76 | 77 | - name: Build / Push 78 | if: ${{ github.ref == 'refs/heads/main' }} 79 | uses: docker/build-push-action@v4 80 | with: 81 | context: . 82 | cache-from: type=gha 83 | cache-to: type=gha,mode=max 84 | push: true 85 | platforms: linux/amd64,linux/arm64 86 | tags: ${{ vars.DOCKER_IMAGE_NAME }}:${{ steps.set_output_vars.outputs.short_sha }} 87 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: paperlb-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: paperlb- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | #- manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | # objref: 49 | # kind: Certificate 50 | # group: cert-manager.io 51 | # version: v1 52 | # name: serving-cert # this name should match the one in certificate.yaml 53 | # fieldref: 54 | # fieldpath: metadata.namespace 55 | #- name: CERTIFICATE_NAME 56 | # objref: 57 | # kind: Certificate 58 | # group: cert-manager.io 59 | # version: v1 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | #- name: SERVICE_NAMESPACE # namespace of the service 62 | # objref: 63 | # kind: Service 64 | # version: v1 65 | # name: webhook-service 66 | # fieldref: 67 | # fieldpath: metadata.namespace 68 | #- name: SERVICE_NAME 69 | # objref: 70 | # kind: Service 71 | # version: v1 72 | # name: webhook-service 73 | -------------------------------------------------------------------------------- /api/v1alpha1/loadbalancerconfig_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 24 | 25 | // LoadBalancerConfigSpec defines the desired state of LoadBalancerConfig 26 | type LoadBalancerConfigSpec struct { 27 | // Default defines if this config is the default config 28 | // +kubebuilder:validation:Required 29 | Default bool `json:"default,omitempty"` 30 | // HTTPUpdaterURL is the http updater url 31 | // +kubebuilder:validation:Required 32 | HTTPUpdaterURL string `json:"httpUpdaterURL,omitempty"` 33 | // Host is the load balancer host 34 | // +kubebuilder:validation:Required 35 | Host string `json:"host,omitempty"` 36 | // PortRange is the load balancer port range 37 | // +kubebuilder:validation:Required 38 | PortRange PortRange `json:"portRange,omitempty"` 39 | } 40 | 41 | // PortRange defines the load balancer port range 42 | type PortRange struct { 43 | // Low is the lower limit of the port range 44 | // +kubebuilder:validation:Required 45 | // +kubebuilder:validation:Minimum=1 46 | Low int `json:"low,omitempty"` 47 | // High is the higher limit of the port range 48 | // +kubebuilder:validation:Required 49 | // +kubebuilder:validation:Maximum=65535 50 | High int `json:"high,omitempty"` 51 | } 52 | 53 | // LoadBalancerConfigStatus defines the observed state of LoadBalancerConfig 54 | type LoadBalancerConfigStatus struct { 55 | } 56 | 57 | //+kubebuilder:object:root=true 58 | //+kubebuilder:subresource:status 59 | 60 | // LoadBalancerConfig is the Schema for the loadbalancerconfigs API 61 | type LoadBalancerConfig struct { 62 | metav1.TypeMeta `json:",inline"` 63 | metav1.ObjectMeta `json:"metadata,omitempty"` 64 | 65 | Spec LoadBalancerConfigSpec `json:"spec,omitempty"` 66 | Status LoadBalancerConfigStatus `json:"status,omitempty"` 67 | } 68 | 69 | //+kubebuilder:object:root=true 70 | 71 | // LoadBalancerConfigList contains a list of LoadBalancerConfig 72 | type LoadBalancerConfigList struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ListMeta `json:"metadata,omitempty"` 75 | Items []LoadBalancerConfig `json:"items"` 76 | } 77 | 78 | func init() { 79 | SchemeBuilder.Register(&LoadBalancerConfig{}, &LoadBalancerConfigList{}) 80 | } 81 | -------------------------------------------------------------------------------- /config/crd/bases/lb.paperlb.com_loadbalancerconfigs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.11.1 7 | creationTimestamp: null 8 | name: loadbalancerconfigs.lb.paperlb.com 9 | spec: 10 | group: lb.paperlb.com 11 | names: 12 | kind: LoadBalancerConfig 13 | listKind: LoadBalancerConfigList 14 | plural: loadbalancerconfigs 15 | singular: loadbalancerconfig 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: LoadBalancerConfig is the Schema for the loadbalancerconfigs 22 | API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: LoadBalancerConfigSpec defines the desired state of LoadBalancerConfig 38 | properties: 39 | default: 40 | description: Default defines if this config is the default config 41 | type: boolean 42 | host: 43 | description: Host is the load balancer host 44 | type: string 45 | httpUpdaterURL: 46 | description: HTTPUpdaterURL is the http updater url 47 | type: string 48 | portRange: 49 | description: PortRange is the load balancer port range 50 | properties: 51 | high: 52 | description: High is the higher limit of the port range 53 | maximum: 65535 54 | type: integer 55 | low: 56 | description: Low is the lower limit of the port range 57 | minimum: 1 58 | type: integer 59 | type: object 60 | type: object 61 | status: 62 | description: LoadBalancerConfigStatus defines the observed state of LoadBalancerConfig 63 | type: object 64 | type: object 65 | served: true 66 | storage: true 67 | subresources: 68 | status: {} 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/didil/paperlb 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.3 7 | github.com/golang/mock v1.6.0 8 | github.com/onsi/ginkgo/v2 v2.6.0 9 | github.com/onsi/gomega v1.24.1 10 | github.com/pkg/errors v0.9.1 11 | github.com/stretchr/testify v1.8.1 12 | k8s.io/api v0.26.0 13 | k8s.io/apimachinery v0.26.0 14 | k8s.io/client-go v0.26.0 15 | sigs.k8s.io/controller-runtime v0.14.1 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 23 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 24 | github.com/fsnotify/fsnotify v1.6.0 // indirect 25 | github.com/go-logr/zapr v1.2.3 // indirect 26 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 27 | github.com/go-openapi/jsonreference v0.20.0 // indirect 28 | github.com/go-openapi/swag v0.19.14 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 31 | github.com/golang/protobuf v1.5.2 // indirect 32 | github.com/google/gnostic v0.5.7-v3refs // indirect 33 | github.com/google/go-cmp v0.5.9 // indirect 34 | github.com/google/gofuzz v1.1.0 // indirect 35 | github.com/google/uuid v1.1.2 // indirect 36 | github.com/imdario/mergo v0.3.6 // indirect 37 | github.com/josharian/intern v1.0.0 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/mailru/easyjson v0.7.6 // indirect 40 | github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 44 | github.com/pmezard/go-difflib v1.0.0 // indirect 45 | github.com/prometheus/client_golang v1.14.0 // indirect 46 | github.com/prometheus/client_model v0.3.0 // indirect 47 | github.com/prometheus/common v0.37.0 // indirect 48 | github.com/prometheus/procfs v0.8.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | go.uber.org/atomic v1.7.0 // indirect 51 | go.uber.org/multierr v1.6.0 // indirect 52 | go.uber.org/zap v1.24.0 // indirect 53 | golang.org/x/mod v0.6.0 // indirect 54 | golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect 55 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 56 | golang.org/x/sys v0.3.0 // indirect 57 | golang.org/x/term v0.3.0 // indirect 58 | golang.org/x/text v0.5.0 // indirect 59 | golang.org/x/time v0.3.0 // indirect 60 | golang.org/x/tools v0.2.0 // indirect 61 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 62 | google.golang.org/appengine v1.6.7 // indirect 63 | google.golang.org/protobuf v1.28.1 // indirect 64 | gopkg.in/inf.v0 v0.9.1 // indirect 65 | gopkg.in/yaml.v2 v2.4.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | k8s.io/apiextensions-apiserver v0.26.0 // indirect 68 | k8s.io/component-base v0.26.0 // indirect 69 | k8s.io/klog/v2 v2.80.1 // indirect 70 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 71 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect 72 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 73 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 74 | sigs.k8s.io/yaml v1.3.0 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /services/http_lb_updater.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "net/http" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type HTTPLBUpdaterClient interface { 15 | Update(ctx context.Context, url string, params *UpdateParams) error 16 | Delete(ctx context.Context, url string, params *DeleteParams) error 17 | } 18 | 19 | type httpLBUpdaterClient struct { 20 | httpClient *http.Client 21 | } 22 | 23 | type UpstreamServer struct { 24 | Host string `json:"host"` 25 | Port int `json:"port"` 26 | } 27 | 28 | type UpdateParams struct { 29 | BackendName string `json:"backendName"` 30 | LBPort int `json:"lbPort"` 31 | LBProtocol string `json:"lbProtocol"` 32 | UpstreamServers []UpstreamServer `json:"upstreamServers"` 33 | // fields below are not implemented in CRD yet 34 | ProxyTimeoutSeconds int `json:"proxyTimeoutSeconds"` 35 | ProxyConnectTimeoutSeconds int `json:"proxyConnectTimeoutSeconds"` 36 | } 37 | 38 | type DeleteParams struct { 39 | BackendName string `json:"backendName"` 40 | } 41 | 42 | func NewHTTPLBUpdaterClient() HTTPLBUpdaterClient { 43 | cl := &httpLBUpdaterClient{} 44 | 45 | t := http.DefaultTransport.(*http.Transport).Clone() 46 | t.MaxIdleConns = 50 47 | t.MaxConnsPerHost = 50 48 | t.MaxIdleConnsPerHost = 50 49 | 50 | httpClient := &http.Client{ 51 | Timeout: 10 * time.Second, 52 | Transport: t, 53 | } 54 | 55 | cl.httpClient = httpClient 56 | 57 | return cl 58 | } 59 | 60 | func (cl *httpLBUpdaterClient) Update(ctx context.Context, url string, params *UpdateParams) error { 61 | var buf bytes.Buffer 62 | 63 | err := json.NewEncoder(&buf).Encode(params) 64 | if err != nil { 65 | return errors.Wrapf(err, "failed to encode update request") 66 | } 67 | 68 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) 69 | if err != nil { 70 | return errors.Wrapf(err, "failed to create update request") 71 | } 72 | 73 | resp, err := cl.httpClient.Do(req) 74 | if err != nil { 75 | return errors.Wrapf(err, "failed to make update request") 76 | } 77 | defer resp.Body.Close() 78 | 79 | if resp.StatusCode < 400 { 80 | return nil 81 | } 82 | 83 | jsonErr := &JSONErr{} 84 | 85 | err = json.NewDecoder(resp.Body).Decode(jsonErr) 86 | if err != nil { 87 | return errors.Wrapf(err, "failed to decode response") 88 | } 89 | 90 | return errors.New(jsonErr.Err) 91 | } 92 | 93 | func (cl *httpLBUpdaterClient) Delete(ctx context.Context, url string, params *DeleteParams) error { 94 | var buf bytes.Buffer 95 | 96 | err := json.NewEncoder(&buf).Encode(params) 97 | if err != nil { 98 | return errors.Wrapf(err, "failed to encode delete request") 99 | } 100 | 101 | req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, &buf) 102 | if err != nil { 103 | return errors.Wrapf(err, "failed to create delete request") 104 | } 105 | 106 | resp, err := cl.httpClient.Do(req) 107 | if err != nil { 108 | return errors.Wrapf(err, "failed to make delete request") 109 | } 110 | defer resp.Body.Close() 111 | 112 | if resp.StatusCode < 400 { 113 | return nil 114 | } 115 | 116 | jsonErr := &JSONErr{} 117 | 118 | err = json.NewDecoder(resp.Body).Decode(jsonErr) 119 | if err != nil { 120 | return errors.Wrapf(err, "failed to decode response") 121 | } 122 | 123 | return errors.New(jsonErr.Err) 124 | } 125 | 126 | type JSONErr struct { 127 | Err string `json:"err"` 128 | } 129 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/envtest" 31 | logf "sigs.k8s.io/controller-runtime/pkg/log" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | ctrl "sigs.k8s.io/controller-runtime" 35 | 36 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 37 | corev1 "k8s.io/api/core/v1" 38 | //+kubebuilder:scaffold:imports 39 | ) 40 | 41 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 42 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 43 | 44 | var cfg *rest.Config 45 | var k8sClient client.Client 46 | var testEnv *envtest.Environment 47 | var ctx context.Context 48 | var cancel context.CancelFunc 49 | 50 | func TestAPIs(t *testing.T) { 51 | RegisterFailHandler(Fail) 52 | 53 | RunSpecs(t, "Controller Suite") 54 | } 55 | 56 | var loadBalancerReconciler *LoadBalancerReconciler 57 | 58 | var _ = BeforeSuite(func() { 59 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 60 | 61 | ctx, cancel = context.WithCancel(context.TODO()) 62 | 63 | By("bootstrapping test environment") 64 | testEnv = &envtest.Environment{ 65 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 66 | ErrorIfCRDPathMissing: true, 67 | } 68 | 69 | var err error 70 | // cfg is defined in this file globally. 71 | cfg, err = testEnv.Start() 72 | Expect(err).NotTo(HaveOccurred()) 73 | Expect(cfg).NotTo(BeNil()) 74 | 75 | err = corev1.AddToScheme(scheme.Scheme) 76 | Expect(err).NotTo(HaveOccurred()) 77 | 78 | err = lbv1alpha1.AddToScheme(scheme.Scheme) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | //+kubebuilder:scaffold:scheme 82 | 83 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 84 | Expect(err).NotTo(HaveOccurred()) 85 | Expect(k8sClient).NotTo(BeNil()) 86 | 87 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 88 | Scheme: scheme.Scheme, 89 | MetricsBindAddress: ":8082", 90 | Port: 9445, 91 | }) 92 | Expect(err).ToNot(HaveOccurred()) 93 | 94 | err = (&ServiceReconciler{ 95 | Client: k8sManager.GetClient(), 96 | Scheme: k8sManager.GetScheme(), 97 | }).SetupWithManager(k8sManager) 98 | Expect(err).ToNot(HaveOccurred()) 99 | 100 | loadBalancerReconciler = &LoadBalancerReconciler{ 101 | Client: k8sManager.GetClient(), 102 | Scheme: k8sManager.GetScheme(), 103 | } 104 | 105 | err = loadBalancerReconciler.SetupWithManager(k8sManager) 106 | Expect(err).ToNot(HaveOccurred()) 107 | 108 | go func() { 109 | defer GinkgoRecover() 110 | err = k8sManager.Start(ctx) 111 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 112 | }() 113 | 114 | }) 115 | 116 | var _ = AfterSuite(func() { 117 | cancel() 118 | By("tearing down the test environment") 119 | err := testEnv.Stop() 120 | Expect(err).NotTo(HaveOccurred()) 121 | }) 122 | -------------------------------------------------------------------------------- /controllers/loadbalancer_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "k8s.io/apimachinery/pkg/types" 12 | 13 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 14 | "github.com/didil/paperlb/mocks" 15 | "github.com/didil/paperlb/services" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | ) 18 | 19 | var _ = Describe("LoadBalancer controller", func() { 20 | const ( 21 | namespaceName = "default" 22 | 23 | timeout = time.Second * 5 24 | duration = time.Second * 10 25 | interval = time.Millisecond * 250 26 | ) 27 | 28 | Context("Reconcile a LoadBalancer", func() { 29 | var lb *lbv1alpha1.LoadBalancer 30 | var gomockController *gomock.Controller 31 | var httpLbUpdaterClient *mocks.MockHTTPLBUpdaterClient 32 | 33 | BeforeEach(func() { 34 | gomockController = gomock.NewController(GinkgoT()) 35 | 36 | httpLbUpdaterClient = mocks.NewMockHTTPLBUpdaterClient(gomockController) 37 | loadBalancerReconciler.HTTPLBUpdaterClient = httpLbUpdaterClient 38 | }) 39 | 40 | It("Should update the load balancer", func() { 41 | defer GinkgoRecover() 42 | 43 | ctx := context.Background() 44 | 45 | loadBalancerConfigName := "my-load-balancer-config" 46 | loadBalancerName := "my-test-service" 47 | updaterURL := "http://example.com/api/v1/lb" 48 | lbHost := "192.168.55.99" 49 | lbPort := 8888 50 | lbProtocol := "TCP" 51 | nodeHost := "1.2.3.4" 52 | nodePort := 30100 53 | 54 | httpLbUpdaterClient.EXPECT(). 55 | Update(gomock.Any(), gomock.Any(), gomock.Any()). 56 | Do(func(ctx context.Context, url string, params *services.UpdateParams) error { 57 | Expect(url).To(Equal(updaterURL)) 58 | Expect(params.BackendName).To(Equal("default_my-test-service")) 59 | Expect(params.LBPort).To(Equal(lbPort)) 60 | Expect(params.LBProtocol).To(Equal(lbProtocol)) 61 | Expect(len(params.UpstreamServers)).To(Equal(1)) 62 | Expect(params.UpstreamServers[0].Host).To(Equal(nodeHost)) 63 | Expect(params.UpstreamServers[0].Port).To(Equal(nodePort)) 64 | return nil 65 | }). 66 | AnyTimes().Return(nil) 67 | httpLbUpdaterClient.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil) 68 | 69 | lb = &lbv1alpha1.LoadBalancer{ 70 | ObjectMeta: metav1.ObjectMeta{ 71 | Name: loadBalancerName, 72 | Namespace: namespaceName, 73 | }, 74 | Spec: lbv1alpha1.LoadBalancerSpec{ 75 | ConfigName: loadBalancerConfigName, 76 | HTTPUpdater: lbv1alpha1.HTTPUpdater{ 77 | URL: updaterURL, 78 | }, 79 | Host: lbHost, 80 | Port: lbPort, 81 | Protocol: lbProtocol, 82 | Targets: []lbv1alpha1.Target{ 83 | { 84 | Host: nodeHost, 85 | Port: nodePort, 86 | }, 87 | }, 88 | }, 89 | } 90 | Expect(k8sClient.Create(ctx, lb)).Should(Succeed()) 91 | 92 | // wait for load balancer http update 93 | lb = &lbv1alpha1.LoadBalancer{} 94 | Eventually(func() error { 95 | err := k8sClient.Get(ctx, types.NamespacedName{Name: loadBalancerName, Namespace: namespaceName}, lb) 96 | if err != nil { 97 | return err 98 | } 99 | if lb.Status.Phase != lbv1alpha1.LoadBalancerPhaseReady { 100 | return fmt.Errorf("lb not ready yet %s", lb.Status.Phase) 101 | } 102 | return nil 103 | }, timeout, interval).Should(BeNil()) 104 | 105 | Expect(lb.Status.Phase).To(Equal(lbv1alpha1.LoadBalancerPhaseReady)) 106 | Expect(lb.Status.TargetCount).To(Equal(1)) 107 | }) 108 | 109 | AfterEach(func() { 110 | ctx := context.Background() 111 | Expect(k8sClient.Delete(ctx, lb)).Should(Succeed()) 112 | gomockController.Finish() 113 | }) 114 | }) 115 | 116 | }) 117 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: paperlb 10 | app.kubernetes.io/part-of: paperlb 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: paperlb 25 | app.kubernetes.io/part-of: paperlb 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /config/crd/bases/lb.paperlb.com_loadbalancers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.11.1 7 | creationTimestamp: null 8 | name: loadbalancers.lb.paperlb.com 9 | spec: 10 | group: lb.paperlb.com 11 | names: 12 | kind: LoadBalancer 13 | listKind: LoadBalancerList 14 | plural: loadbalancers 15 | singular: loadbalancer 16 | scope: Namespaced 17 | versions: 18 | - additionalPrinterColumns: 19 | - jsonPath: .spec.host 20 | name: Host 21 | type: string 22 | - jsonPath: .spec.port 23 | name: Port 24 | type: string 25 | - jsonPath: .spec.protocol 26 | name: Protocol 27 | type: string 28 | - jsonPath: .status.targetCount 29 | name: TargetCount 30 | type: string 31 | - jsonPath: .status.phase 32 | name: Status 33 | type: string 34 | name: v1alpha1 35 | schema: 36 | openAPIV3Schema: 37 | description: LoadBalancer is the Schema for the loadbalancers API 38 | properties: 39 | apiVersion: 40 | description: 'APIVersion defines the versioned schema of this representation 41 | of an object. Servers should convert recognized schemas to the latest 42 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 43 | type: string 44 | kind: 45 | description: 'Kind is a string value representing the REST resource this 46 | object represents. Servers may infer this from the endpoint the client 47 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 48 | type: string 49 | metadata: 50 | type: object 51 | spec: 52 | description: LoadBalancerSpec defines the desired state of LoadBalancer 53 | properties: 54 | configName: 55 | description: ConfigName is the loadbalancer config name 56 | type: string 57 | host: 58 | description: Host is the lb host 59 | type: string 60 | httpUpdater: 61 | description: HTTPUpdater is the http updater 62 | properties: 63 | url: 64 | description: URL to request lb updates 65 | type: string 66 | type: object 67 | port: 68 | description: Port is the lb host 69 | type: integer 70 | protocol: 71 | description: Protocol is the lb protocol 72 | type: string 73 | targets: 74 | description: Targets is the list of targets 75 | items: 76 | properties: 77 | host: 78 | description: Host is the target host 79 | type: string 80 | port: 81 | description: Port is the target port 82 | type: integer 83 | type: object 84 | type: array 85 | type: object 86 | status: 87 | description: LoadBalancerStatus defines the observed state of LoadBalancer 88 | properties: 89 | phase: 90 | default: PENDING 91 | enum: 92 | - PENDING 93 | - READY 94 | type: string 95 | targetCount: 96 | description: TargetCount is the number of targets 97 | type: integer 98 | type: object 99 | type: object 100 | served: true 101 | storage: true 102 | subresources: 103 | status: {} 104 | -------------------------------------------------------------------------------- /api/v1alpha1/loadbalancer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // LoadBalancerSpec defines the desired state of LoadBalancer 27 | type LoadBalancerSpec struct { 28 | // ConfigName is the loadbalancer config name 29 | // +kubebuilder:validation:Required 30 | ConfigName string `json:"configName,omitempty"` 31 | // HTTPUpdater is the http updater 32 | // +kubebuilder:validation:Required 33 | HTTPUpdater HTTPUpdater `json:"httpUpdater,omitempty"` 34 | // Host is the lb host 35 | // +kubebuilder:validation:Required 36 | Host string `json:"host,omitempty"` 37 | // Port is the lb host 38 | // +kubebuilder:validation:Required 39 | Port int `json:"port,omitempty"` 40 | // Protocol is the lb protocol 41 | // +kubebuilder:validation:Required 42 | Protocol string `json:"protocol,omitempty"` 43 | // Targets is the list of targets 44 | Targets []Target `json:"targets,omitempty"` 45 | } 46 | 47 | type Target struct { 48 | // Host is the target host 49 | // +kubebuilder:validation:Required 50 | Host string `json:"host,omitempty"` 51 | // Port is the target port 52 | // +kubebuilder:validation:Required 53 | Port int `json:"port,omitempty"` 54 | } 55 | 56 | type LoadBalancerPhase string 57 | 58 | const ( 59 | // LoadBalancerPhase pending 60 | LoadBalancerPhasePending LoadBalancerPhase = "PENDING" 61 | // LoadBalancerPhase ready 62 | LoadBalancerPhaseReady LoadBalancerPhase = "READY" 63 | ) 64 | 65 | // LoadBalancerStatus defines the observed state of LoadBalancer 66 | type LoadBalancerStatus struct { 67 | // +kubebuilder:validation:Enum=PENDING;READY 68 | // +kubebuilder:validation:Required 69 | // +kubebuilder:default=PENDING 70 | Phase LoadBalancerPhase `json:"phase,omitempty"` 71 | 72 | // TargetCount is the number of targets 73 | TargetCount int `json:"targetCount,omitempty"` 74 | } 75 | 76 | // HTTPUpdater is the http updated for the load balancer 77 | type HTTPUpdater struct { 78 | // URL to request lb updates 79 | URL string `json:"url,omitempty"` 80 | } 81 | 82 | // +kubebuilder:object:root=true 83 | // +kubebuilder:subresource:status 84 | // +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` 85 | // +kubebuilder:printcolumn:name="Port",type=string,JSONPath=`.spec.port` 86 | // +kubebuilder:printcolumn:name="Protocol",type=string,JSONPath=`.spec.protocol` 87 | // +kubebuilder:printcolumn:name="TargetCount",type=string,JSONPath=`.status.targetCount` 88 | // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` 89 | // LoadBalancer is the Schema for the loadbalancers API 90 | type LoadBalancer struct { 91 | metav1.TypeMeta `json:",inline"` 92 | metav1.ObjectMeta `json:"metadata,omitempty"` 93 | 94 | Spec LoadBalancerSpec `json:"spec,omitempty"` 95 | Status LoadBalancerStatus `json:"status,omitempty"` 96 | } 97 | 98 | //+kubebuilder:object:root=true 99 | 100 | // LoadBalancerList contains a list of LoadBalancer 101 | type LoadBalancerList struct { 102 | metav1.TypeMeta `json:",inline"` 103 | metav1.ListMeta `json:"metadata,omitempty"` 104 | Items []LoadBalancer `json:"items"` 105 | } 106 | 107 | func init() { 108 | SchemeBuilder.Register(&LoadBalancer{}, &LoadBalancerList{}) 109 | } 110 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 35 | "github.com/didil/paperlb/controllers" 36 | "github.com/didil/paperlb/services" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(lbv1alpha1.AddToScheme(scheme)) 49 | //+kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | var probeAddr string 56 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 57 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 58 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 59 | "Enable leader election for controller manager. "+ 60 | "Enabling this will ensure there is only one active controller manager.") 61 | opts := zap.Options{ 62 | Development: true, 63 | } 64 | opts.BindFlags(flag.CommandLine) 65 | flag.Parse() 66 | 67 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 68 | 69 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 70 | Scheme: scheme, 71 | MetricsBindAddress: metricsAddr, 72 | Port: 9443, 73 | HealthProbeBindAddress: probeAddr, 74 | LeaderElection: enableLeaderElection, 75 | LeaderElectionID: "ccd53c55.paperlb.com", 76 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 77 | // when the Manager ends. This requires the binary to immediately end when the 78 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 79 | // speeds up voluntary leader transitions as the new leader don't have to wait 80 | // LeaseDuration time first. 81 | // 82 | // In the default scaffold provided, the program ends immediately after 83 | // the manager stops, so would be fine to enable this option. However, 84 | // if you are doing or is intended to do any operation such as perform cleanups 85 | // after the manager stops then its usage might be unsafe. 86 | // LeaderElectionReleaseOnCancel: true, 87 | }) 88 | if err != nil { 89 | setupLog.Error(err, "unable to start manager") 90 | os.Exit(1) 91 | } 92 | 93 | httpLbUpdaterClient := services.NewHTTPLBUpdaterClient() 94 | 95 | if err = (&controllers.LoadBalancerReconciler{ 96 | Client: mgr.GetClient(), 97 | Scheme: mgr.GetScheme(), 98 | HTTPLBUpdaterClient: httpLbUpdaterClient, 99 | }).SetupWithManager(mgr); err != nil { 100 | setupLog.Error(err, "unable to create controller", "controller", "LoadBalancer") 101 | os.Exit(1) 102 | } 103 | 104 | if err = (&controllers.ServiceReconciler{ 105 | Client: mgr.GetClient(), 106 | Scheme: mgr.GetScheme(), 107 | }).SetupWithManager(mgr); err != nil { 108 | setupLog.Error(err, "unable to create controller", "controller", "Service") 109 | os.Exit(1) 110 | } 111 | 112 | //+kubebuilder:scaffold:builder 113 | 114 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 115 | setupLog.Error(err, "unable to set up health check") 116 | os.Exit(1) 117 | } 118 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 119 | setupLog.Error(err, "unable to set up ready check") 120 | os.Exit(1) 121 | } 122 | 123 | setupLog.Info("starting manager") 124 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 125 | setupLog.Error(err, "problem running manager") 126 | os.Exit(1) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /controllers/loadbalancer_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | apierrors "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/log" 29 | 30 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 31 | "github.com/didil/paperlb/services" 32 | ) 33 | 34 | // LoadBalancerReconciler reconciles a LoadBalancer object 35 | type LoadBalancerReconciler struct { 36 | client.Client 37 | Scheme *runtime.Scheme 38 | HTTPLBUpdaterClient services.HTTPLBUpdaterClient 39 | } 40 | 41 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancers,verbs=get;list;watch;create;update;patch;delete 42 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancers/status,verbs=get;update;patch 43 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancers/finalizers,verbs=update 44 | 45 | func (r *LoadBalancerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 46 | logger := log.FromContext(ctx) 47 | 48 | lb := &lbv1alpha1.LoadBalancer{} 49 | err := r.Get(ctx, req.NamespacedName, lb) 50 | if err != nil { 51 | if apierrors.IsNotFound(err) { 52 | // Request object not found, could have been deleted after reconcile request. 53 | logger.Info("LoadBlancer resource not found. Ignoring since object must be deleted") 54 | return ctrl.Result{}, nil 55 | } 56 | 57 | logger.Error(err, "Failed to get Load Balancer") 58 | return ctrl.Result{}, err 59 | } 60 | 61 | // examine DeletionTimestamp to determine if object is under deletion 62 | if lb.ObjectMeta.DeletionTimestamp.IsZero() { 63 | // The object is not being deleted, so if it does not have our finalizer, 64 | // then lets add the finalizer and update the object. This is equivalent to 65 | // registering our finalizer. 66 | 67 | if !containsString(lb.ObjectMeta.Finalizers, loadBalancerFinalizerName) { 68 | lb.ObjectMeta.Finalizers = append(lb.ObjectMeta.Finalizers, loadBalancerFinalizerName) 69 | if err := r.Update(ctx, lb); err != nil { 70 | logger.Error(err, "Failed to update load balancer finalizers") 71 | return ctrl.Result{}, err 72 | } 73 | 74 | // Object updated - return and requeue 75 | return ctrl.Result{Requeue: true}, nil 76 | } 77 | } else { 78 | // The object is being deleted 79 | if containsString(lb.ObjectMeta.Finalizers, loadBalancerFinalizerName) { 80 | // our finalizer is present, delete lb via http updater 81 | logger.Info("Deleting load balancer via http updater") 82 | 83 | err := r.HTTPLBUpdaterClient.Delete(ctx, lb.Spec.HTTPUpdater.URL, &services.DeleteParams{ 84 | BackendName: lbBackendName(lb.Namespace, lb.Name), 85 | }) 86 | if err != nil { 87 | logger.Error(err, "Failed to delete load balancer via http updater") 88 | return ctrl.Result{RequeueAfter: httpUpdateRetryAfter}, err 89 | } 90 | 91 | // remove our finalizer from the list and update it. 92 | lb.ObjectMeta.Finalizers = removeString(lb.ObjectMeta.Finalizers, loadBalancerFinalizerName) 93 | if err := r.Update(context.Background(), lb); err != nil { 94 | logger.Error(err, "Failed to delete load balancer finalizer") 95 | return ctrl.Result{}, err 96 | } 97 | } 98 | 99 | // Stop reconciliation as the item is being deleted 100 | return ctrl.Result{}, nil 101 | } 102 | 103 | // check if lb update is pending 104 | if lb.Status.Phase != lbv1alpha1.LoadBalancerPhaseReady { 105 | logger.Info("Updating load balancer via http updater", "oldPhase", lb.Status.Phase, "newPhase", lbv1alpha1.LoadBalancerPhaseReady) 106 | 107 | upstreamServers := []services.UpstreamServer{} 108 | for _, target := range lb.Spec.Targets { 109 | upstreamServers = append(upstreamServers, services.UpstreamServer{Host: target.Host, Port: target.Port}) 110 | } 111 | 112 | err := r.HTTPLBUpdaterClient.Update(ctx, lb.Spec.HTTPUpdater.URL, &services.UpdateParams{ 113 | BackendName: lbBackendName(lb.Namespace, lb.Name), 114 | LBPort: lb.Spec.Port, 115 | LBProtocol: lb.Spec.Protocol, 116 | UpstreamServers: upstreamServers, 117 | }) 118 | if err != nil { 119 | logger.Error(err, "Failed to Update load balancer via http updater") 120 | return ctrl.Result{RequeueAfter: httpUpdateRetryAfter}, err 121 | } 122 | 123 | lb.Status.Phase = lbv1alpha1.LoadBalancerPhaseReady 124 | lb.Status.TargetCount = len(lb.Spec.Targets) 125 | err = r.Status().Update(ctx, lb) 126 | if err != nil { 127 | logger.Error(err, "Failed to Update load balancer status") 128 | return ctrl.Result{}, err 129 | } 130 | 131 | // Updated successfully - return and requeue 132 | return ctrl.Result{Requeue: true}, nil 133 | } 134 | 135 | return ctrl.Result{}, nil 136 | } 137 | 138 | func lbBackendName(namespace, name string) string { 139 | return fmt.Sprintf("%s_%s", namespace, name) 140 | } 141 | 142 | var httpUpdateRetryAfter = 5 * time.Second 143 | 144 | const loadBalancerFinalizerName = "lb.paperlb.com/lb-finalizer" 145 | 146 | // SetupWithManager sets up the controller with the Manager. 147 | func (r *LoadBalancerReconciler) SetupWithManager(mgr ctrl.Manager) error { 148 | return ctrl.NewControllerManagedBy(mgr). 149 | For(&lbv1alpha1.LoadBalancer{}). 150 | Complete(r) 151 | } 152 | 153 | func containsString(slice []string, s string) bool { 154 | for _, item := range slice { 155 | if item == s { 156 | return true 157 | } 158 | } 159 | return false 160 | } 161 | 162 | func removeString(slice []string, s string) (result []string) { 163 | for _, item := range slice { 164 | if item == s { 165 | continue 166 | } 167 | result = append(result, item) 168 | } 169 | return 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PaperLB 2 | 3 | ![Alt text](logo-color.png?raw=true "PaperLB Logo") 4 | 5 | A kubernetes network load balancer operator implementation. 6 | 7 | 8 | **THIS SOFTWARE IS WORK IN PROGRESS / ALPHA RELEASE AND IS NOT MEANT FOR USAGE IN PRODUCTION SYSTEMS** 9 | 10 | ## What is PaperLB ? 11 | Introduction blog article: [PaperLB. A Kubernetes Network Load Balancer Implementation](https://didil.medium.com/paperlb-fc4c28a82acb) 12 | 13 | You might have noticed that vanilla Kubernetes does not come with a Load Balancer implementation. If you create a LoadBalancer Service in a self-hosted cluster setup, its status will remain "pending" and it won't show an external IP you can use to access the service. It should look something like this: 14 | 15 | ````bash 16 | $ kubectl get services 17 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 18 | k8s-pod-info-api-service LoadBalancer 10.43.12.233 5000:31767/TCP 6s 19 | ````` 20 | 21 | On the other hand, when you create a LoadBalancer Service in a managed kubernetes cluster such as GCP GKE or AWS EKS, the service will receive an External IP from a Load Balancer assigned by the cloud provider. 22 | 23 | The idea behind PaperLB is to allow "LoadBalancer" type services to work with external network load balancers in any environment. PaperLB allows you to use an external L4 Load Balancer of your choice (an nginx server for example) in front of your Kubernetes cluster services. It should work on your development clusters running locally as well as cloud virtual machines or bare metal. 24 | 25 | ![Alt text](paperlb-archi.png?raw=true "PaperLB Architecture") 26 | 27 | ## How does it work ? 28 | PaperLB is implemented as a kubernetes "Operator": 29 | - Custom Resource Definitions 30 | - Kubernetes Controllers that manage the Custom Resources and interact with your load balancer 31 | 32 | The idea is: 33 | 34 | - You create a Kubernetes LoadBalancer type service and a LoadBalancerConfig configuration object 35 | - The controller notices the service and LoadBalancerConfig and creates a "LoadBalancer" object 36 | - The controller notices the "LoadBalancer" object and updates your network load balancer using the config data + the service/nodes info 37 | 38 | ## Features 39 | - Works with TCP or UDP L4 load balancers 40 | - Adapters implemented: 41 | - Nginx: https://github.com/didil/nginx-lb-updater 42 | - Updates load balancer configuration on: 43 | - Node updates 44 | - Service updates 45 | - Deletes load balancer configuration on Service deletion 46 | 47 | ## Getting Started 48 | You’ll need a kubernetes cluster to run against. You can use a local cluster for testing, or run against a remote cluster. 49 | **Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). 50 | 51 | ### Usage 52 | You can find the full example in the demo/ directory 53 | Service example: 54 | ````yaml 55 | apiVersion: v1 56 | kind: Service 57 | metadata: 58 | labels: 59 | app: k8s-pod-info-api 60 | name: k8s-pod-info-api-service 61 | #optional annotation to use a config different than the default config 62 | #annotations: 63 | # lb.paperlb.com/config-name: "my-special-config" 64 | spec: 65 | ports: 66 | - port: 5000 67 | protocol: TCP 68 | targetPort: 4000 69 | selector: 70 | app: k8s-pod-info-api 71 | type: LoadBalancer 72 | ```` 73 | 74 | LoadBalancerConfig example: 75 | ````yaml 76 | apiVersion: lb.paperlb.com/v1alpha1 77 | kind: LoadBalancerConfig 78 | metadata: 79 | name: default-lb-config 80 | namespace: paperlb-system 81 | spec: 82 | default: true 83 | httpUpdaterURL: "http://192.168.64.1:3000/api/v1/lb" 84 | host: "192.168.64.1" 85 | portRange: 86 | low: 8100 87 | high: 8200 88 | ```` 89 | 90 | LoadBalancerConfig fields: 91 | - `.spec.default`: "true" if this should be the default config, false otherwise 92 | - `.spec.httpUpdaterURL`: URL where the http lb updater instance can be called. The API is explained here: https://github.com/didil/nginx-lb-updater#api 93 | - `.spec.host`: Load Balancer Host 94 | - `.spec.portRange`: The controller will select a load balancer port from this range 95 | - `.spec.portRange.low`: Lowest of the available ports on the load balancer 96 | - `.spec.portRange.high`: Highest of the available ports on the load balancer 97 | 98 | When you apply these manifests, a load balancer resource should be created. To get the load balancer connection info you can run: 99 | ````bash 100 | $ k get loadbalancer k8s-pod-info-api-service 101 | NAME HOST PORT PROTOCOL TARGETCOUNT STATUS 102 | k8s-pod-info-api-service 192.168.64.1 8100 TCP 2 READY 103 | ```` 104 | 105 | Testing with Curl 106 | ````bash 107 | $ curl -s 192.168.64.1:8100/api/v1/info|jq 108 | "pod": { 109 | "name": "k8s-pod-info-api-84dc7c9bdd-mz74t", 110 | "ip": "10.42.0.27", 111 | "namespace": "default", 112 | "serviceAccountName": "default" 113 | }, 114 | "node": { 115 | "name": "k3s-local-server" 116 | } 117 | } 118 | ```` 119 | 120 | 121 | ### Run tests 122 | To run tests: 123 | ```sh 124 | make test 125 | ``` 126 | 127 | ### Run locally 128 | 1. Install the CRDs into the cluster: 129 | 130 | ```sh 131 | make install 132 | ``` 133 | 134 | 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 135 | 136 | ```sh 137 | make run 138 | ``` 139 | 140 | 3. You will need to run a load balancer instance and an API to allow the load balancer to be updated. You can use the example from this repository https://github.com/didil/nginx-lb-updater#run-locally 141 | 142 | 4. The demo folder contains sample resource definitions to create a service and deployment. You can tweak them and run: 143 | ```sh 144 | kubectl apply -f demo/ 145 | ``` 146 | 147 | # Install PaperLB manifests to remote cluster 148 | To install PaperLB CRDs and controllers, run: 149 | ````bash 150 | kubectl apply -f https://raw.githubusercontent.com/didil/paperlb/v0.2.0/config/manifests/paperlb.yaml 151 | ```` 152 | The resources are created in the `paperlb-system` namespace. 153 | 154 | ## Contributing 155 | Please feel free to open issues and / PRs if you'd like to contribute ! You can also get in touch at adil-paperlb@ledidil.com 156 | 157 | ## License 158 | 159 | Copyright 2023 Adil H. 160 | 161 | Licensed under the Apache License, Version 2.0 (the "License"); 162 | you may not use this file except in compliance with the License. 163 | You may obtain a copy of the License at 164 | 165 | http://www.apache.org/licenses/LICENSE-2.0 166 | 167 | Unless required by applicable law or agreed to in writing, software 168 | distributed under the License is distributed on an "AS IS" BASIS, 169 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 170 | See the License for the specific language governing permissions and 171 | limitations under the License. 172 | 173 | -------------------------------------------------------------------------------- /controllers/service_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "k8s.io/apimachinery/pkg/types" 12 | "k8s.io/apimachinery/pkg/util/intstr" 13 | 14 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 15 | "github.com/didil/paperlb/mocks" 16 | corev1 "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | var _ = Describe("Service controller", func() { 21 | const ( 22 | namespaceName = "default" 23 | 24 | timeout = time.Second * 5 25 | duration = time.Second * 10 26 | interval = time.Millisecond * 250 27 | ) 28 | 29 | Context("Reconcile a service", func() { 30 | var gomockController *gomock.Controller 31 | var httpLbUpdaterClient *mocks.MockHTTPLBUpdaterClient 32 | var service *corev1.Service 33 | var loadBalancerConfig *lbv1alpha1.LoadBalancerConfig 34 | var loadBalancer *lbv1alpha1.LoadBalancer 35 | var node1 *corev1.Node 36 | var node2 *corev1.Node 37 | var paperLBSystemNamespace *corev1.Namespace 38 | 39 | BeforeEach(func() { 40 | gomockController = gomock.NewController(GinkgoT()) 41 | 42 | httpLbUpdaterClient = mocks.NewMockHTTPLBUpdaterClient(gomockController) 43 | loadBalancerReconciler.HTTPLBUpdaterClient = httpLbUpdaterClient 44 | }) 45 | 46 | It("Should create/update the load balancer", func() { 47 | httpLbUpdaterClient.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil) 48 | httpLbUpdaterClient.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil) 49 | ctx := context.Background() 50 | 51 | serviceName := "test-service" 52 | loadBalancerName := "test-service" 53 | updaterURL := "http://example.com/api/v1/lb" 54 | lbHost := "192.168.55.99" 55 | lbPortLow := 9000 56 | lbPortHigh := 9050 57 | 58 | lbProtocol := "TCP" 59 | 60 | port := 8000 61 | targetPort := 8100 62 | nodePort := 30100 63 | 64 | paperLBSystemNamespace = &corev1.Namespace{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: paperLBSystemNamespaceName, 67 | }, 68 | } 69 | Expect(k8sClient.Create(ctx, paperLBSystemNamespace)).Should(Succeed()) 70 | 71 | nodeHost1 := "1.2.3.4" 72 | node1 = &corev1.Node{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: "node-1", 75 | }, 76 | Status: corev1.NodeStatus{ 77 | Addresses: []corev1.NodeAddress{ 78 | { 79 | Type: corev1.NodeExternalIP, 80 | Address: nodeHost1, 81 | }, 82 | }, 83 | Conditions: []corev1.NodeCondition{ 84 | { 85 | Type: corev1.NodeReady, 86 | Status: corev1.ConditionTrue, 87 | }, 88 | }, 89 | }, 90 | } 91 | Expect(k8sClient.Create(ctx, node1)).Should(Succeed()) 92 | 93 | loadBalancerConfig = &lbv1alpha1.LoadBalancerConfig{ 94 | ObjectMeta: metav1.ObjectMeta{ 95 | Name: "default-lb-config", 96 | Namespace: paperLBSystemNamespaceName, 97 | }, 98 | Spec: lbv1alpha1.LoadBalancerConfigSpec{ 99 | Default: true, 100 | HTTPUpdaterURL: updaterURL, 101 | Host: lbHost, 102 | PortRange: lbv1alpha1.PortRange{ 103 | Low: lbPortLow, 104 | High: lbPortHigh, 105 | }, 106 | }, 107 | } 108 | Expect(k8sClient.Create(ctx, loadBalancerConfig)).Should(Succeed()) 109 | 110 | service = &corev1.Service{ 111 | ObjectMeta: metav1.ObjectMeta{ 112 | Name: serviceName, 113 | Namespace: namespaceName, 114 | }, 115 | Spec: corev1.ServiceSpec{ 116 | Ports: []corev1.ServicePort{ 117 | { 118 | Port: int32(port), 119 | TargetPort: intstr.FromInt(targetPort), 120 | NodePort: int32(nodePort), 121 | }, 122 | }, 123 | Type: corev1.ServiceTypeLoadBalancer, 124 | }, 125 | } 126 | Expect(k8sClient.Create(ctx, service)).Should(Succeed()) 127 | 128 | // wait for load balancer to be ready 129 | loadBalancer = &lbv1alpha1.LoadBalancer{} 130 | Eventually(func() error { 131 | err := k8sClient.Get(ctx, types.NamespacedName{Name: loadBalancerName, Namespace: namespaceName}, loadBalancer) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if loadBalancer.Status.Phase != lbv1alpha1.LoadBalancerPhaseReady { 137 | return fmt.Errorf("lb not ready yet") 138 | } 139 | 140 | return nil 141 | }, timeout, interval).Should(BeNil()) 142 | 143 | Expect(loadBalancer.OwnerReferences).To(HaveLen(1)) 144 | Expect(loadBalancer.OwnerReferences[0].UID).To(Equal(service.UID)) 145 | 146 | Expect(loadBalancer.Spec.ConfigName).To(Equal(loadBalancerConfig.Name)) 147 | Expect(loadBalancer.Spec.HTTPUpdater.URL).To(Equal(updaterURL)) 148 | Expect(loadBalancer.Spec.Host).To(Equal(lbHost)) 149 | Expect(loadBalancer.Spec.Port).To(Equal(lbPortLow)) 150 | Expect(loadBalancer.Spec.Protocol).To(Equal(lbProtocol)) 151 | Expect(loadBalancer.Spec.Targets).To(HaveLen(1)) 152 | Expect(loadBalancer.Spec.Targets[0]).To(Equal(lbv1alpha1.Target{ 153 | Host: nodeHost1, 154 | Port: nodePort, 155 | })) 156 | 157 | nodeHost2 := "1.2.3.5" 158 | node2 = &corev1.Node{ 159 | ObjectMeta: metav1.ObjectMeta{ 160 | Name: "node-2", 161 | }, 162 | Status: corev1.NodeStatus{ 163 | Addresses: []corev1.NodeAddress{ 164 | { 165 | Type: corev1.NodeExternalIP, 166 | Address: nodeHost2, 167 | }, 168 | }, 169 | Conditions: []corev1.NodeCondition{ 170 | { 171 | Type: corev1.NodeReady, 172 | Status: corev1.ConditionTrue, 173 | }, 174 | }, 175 | }, 176 | } 177 | Expect(k8sClient.Create(ctx, node2)).Should(Succeed()) 178 | 179 | // wait for load balancer update 180 | loadBalancer = &lbv1alpha1.LoadBalancer{} 181 | Eventually(func() error { 182 | err := k8sClient.Get(ctx, types.NamespacedName{Name: loadBalancerName, Namespace: namespaceName}, loadBalancer) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | if len(loadBalancer.Spec.Targets) != 2 { 188 | return fmt.Errorf("there should be 2 targets") 189 | } 190 | 191 | return nil 192 | }, timeout, interval).Should(BeNil()) 193 | 194 | Expect(loadBalancer.OwnerReferences).To(HaveLen(1)) 195 | Expect(loadBalancer.OwnerReferences[0].UID).To(Equal(service.UID)) 196 | 197 | Expect(loadBalancer.Spec.ConfigName).To(Equal(loadBalancerConfig.Name)) 198 | Expect(loadBalancer.Spec.HTTPUpdater.URL).To(Equal(updaterURL)) 199 | Expect(loadBalancer.Spec.Host).To(Equal(lbHost)) 200 | Expect(loadBalancer.Spec.Port).To(Equal(lbPortLow)) 201 | Expect(loadBalancer.Spec.Protocol).To(Equal(lbProtocol)) 202 | Expect(loadBalancer.Spec.Targets).To(ContainElements( 203 | lbv1alpha1.Target{ 204 | Host: nodeHost1, 205 | Port: nodePort, 206 | }, lbv1alpha1.Target{ 207 | Host: nodeHost2, 208 | Port: nodePort, 209 | }, 210 | )) 211 | 212 | }) 213 | 214 | AfterEach(func() { 215 | ctx := context.Background() 216 | Expect(k8sClient.Delete(ctx, service)).Should(Succeed()) 217 | Expect(k8sClient.Delete(ctx, node1)).Should(Succeed()) 218 | Expect(k8sClient.Delete(ctx, node2)).Should(Succeed()) 219 | Expect(k8sClient.Delete(ctx, loadBalancer)).Should(Succeed()) 220 | Expect(k8sClient.Delete(ctx, loadBalancerConfig)).Should(Succeed()) 221 | Expect(k8sClient.Delete(ctx, paperLBSystemNamespace)).Should(Succeed()) 222 | gomockController.Finish() 223 | }) 224 | }) 225 | 226 | }) 227 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= quay.io/didil/paperlb:6202a2c 4 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 5 | ENVTEST_K8S_VERSION = 1.26.0 6 | 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | # Setting SHELL to bash allows bash commands to be executed by recipes. 15 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 16 | SHELL = /usr/bin/env bash -o pipefail 17 | .SHELLFLAGS = -ec 18 | 19 | .PHONY: all 20 | all: build 21 | 22 | ##@ General 23 | 24 | # The help target prints out all targets with their descriptions organized 25 | # beneath their categories. The categories are represented by '##@' and the 26 | # target descriptions by '##'. The awk commands is responsible for reading the 27 | # entire set of makefiles included in this invocation, looking for lines of the 28 | # file as xyz: ## something, and then pretty-format the target and help. Then, 29 | # if there's a line with ##@ something, that gets pretty-printed as a category. 30 | # More info on the usage of ANSI control characters for terminal formatting: 31 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 32 | # More info on the awk command: 33 | # http://linuxcommand.org/lc3_adv_awk.php 34 | 35 | .PHONY: help 36 | help: ## Display this help. 37 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 38 | 39 | ##@ Development 40 | 41 | .PHONY: manifests 42 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 43 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 44 | 45 | .PHONY: generate 46 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 47 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 48 | 49 | .PHONY: fmt 50 | fmt: ## Run go fmt against code. 51 | go fmt ./... 52 | 53 | .PHONY: vet 54 | vet: ## Run go vet against code. 55 | go vet ./... 56 | 57 | .PHONY: test 58 | test: manifests generate fmt vet envtest ## Run tests. 59 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out 60 | 61 | ##@ Build 62 | 63 | .PHONY: build 64 | build: manifests generate fmt vet ## Build manager binary. 65 | go build -o bin/manager main.go 66 | 67 | .PHONY: run 68 | run: manifests generate fmt vet ## Run a controller from your host. 69 | go run ./main.go 70 | 71 | # If you wish built the manager image targeting other platforms you can use the --platform flag. 72 | # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. 73 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 74 | .PHONY: docker-build 75 | docker-build: test ## Build docker image with the manager. 76 | docker build -t ${IMG} . 77 | 78 | .PHONY: docker-push 79 | docker-push: ## Push docker image with the manager. 80 | docker push ${IMG} 81 | 82 | # PLATFORMS defines the target platforms for the manager image be build to provide support to multiple 83 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 84 | # - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ 85 | # - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 86 | # - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) 87 | # To properly provided solutions that supports more than one platform you should use this option. 88 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 89 | .PHONY: docker-buildx 90 | docker-buildx: test ## Build and push docker image for the manager for cross-platform support 91 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 92 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 93 | - docker buildx create --name project-v3-builder 94 | docker buildx use project-v3-builder 95 | - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 96 | - docker buildx rm project-v3-builder 97 | rm Dockerfile.cross 98 | 99 | ##@ Deployment 100 | 101 | ifndef ignore-not-found 102 | ignore-not-found = false 103 | endif 104 | 105 | .PHONY: install 106 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 107 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 108 | 109 | .PHONY: uninstall 110 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 111 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 112 | 113 | .PHONY: deploy 114 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 115 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 116 | $(KUSTOMIZE) build config/default | kubectl apply -f - 117 | 118 | .PHONY: undeploy 119 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 120 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 121 | 122 | .PHONY: build-manifests 123 | build-manifests: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 124 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 125 | $(KUSTOMIZE) build config/default > config/manifests/paperlb.yaml 126 | 127 | ##@ Build Dependencies 128 | 129 | ## Location to install dependencies to 130 | LOCALBIN ?= $(shell pwd)/bin 131 | $(LOCALBIN): 132 | mkdir -p $(LOCALBIN) 133 | 134 | ## Tool Binaries 135 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 136 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 137 | ENVTEST ?= $(LOCALBIN)/setup-envtest 138 | 139 | ## Tool Versions 140 | KUSTOMIZE_VERSION ?= v3.8.7 141 | CONTROLLER_TOOLS_VERSION ?= v0.11.1 142 | 143 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 144 | .PHONY: kustomize 145 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. 146 | $(KUSTOMIZE): $(LOCALBIN) 147 | @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ 148 | echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ 149 | rm -rf $(LOCALBIN)/kustomize; \ 150 | fi 151 | test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } 152 | 153 | .PHONY: controller-gen 154 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 155 | $(CONTROLLER_GEN): $(LOCALBIN) 156 | test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ 157 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 158 | 159 | .PHONY: envtest 160 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 161 | $(ENVTEST): $(LOCALBIN) 162 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 163 | 164 | .PHONY: install-tools 165 | install-tools: 166 | @cat tools/tools.go | grep _ | awk -F'"' '{print $$2}' | GOBIN=$(LOCALBIN) xargs -tI % go install % 167 | 168 | .PHONY: gen-mocks 169 | gen-mocks: install-tools 170 | mocks/gen_mocks.sh 171 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2023. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *HTTPUpdater) DeepCopyInto(out *HTTPUpdater) { 30 | *out = *in 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPUpdater. 34 | func (in *HTTPUpdater) DeepCopy() *HTTPUpdater { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(HTTPUpdater) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) { 45 | *out = *in 46 | out.TypeMeta = in.TypeMeta 47 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 48 | in.Spec.DeepCopyInto(&out.Spec) 49 | out.Status = in.Status 50 | } 51 | 52 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancer. 53 | func (in *LoadBalancer) DeepCopy() *LoadBalancer { 54 | if in == nil { 55 | return nil 56 | } 57 | out := new(LoadBalancer) 58 | in.DeepCopyInto(out) 59 | return out 60 | } 61 | 62 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 63 | func (in *LoadBalancer) DeepCopyObject() runtime.Object { 64 | if c := in.DeepCopy(); c != nil { 65 | return c 66 | } 67 | return nil 68 | } 69 | 70 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 71 | func (in *LoadBalancerConfig) DeepCopyInto(out *LoadBalancerConfig) { 72 | *out = *in 73 | out.TypeMeta = in.TypeMeta 74 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 75 | out.Spec = in.Spec 76 | out.Status = in.Status 77 | } 78 | 79 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerConfig. 80 | func (in *LoadBalancerConfig) DeepCopy() *LoadBalancerConfig { 81 | if in == nil { 82 | return nil 83 | } 84 | out := new(LoadBalancerConfig) 85 | in.DeepCopyInto(out) 86 | return out 87 | } 88 | 89 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 90 | func (in *LoadBalancerConfig) DeepCopyObject() runtime.Object { 91 | if c := in.DeepCopy(); c != nil { 92 | return c 93 | } 94 | return nil 95 | } 96 | 97 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 98 | func (in *LoadBalancerConfigList) DeepCopyInto(out *LoadBalancerConfigList) { 99 | *out = *in 100 | out.TypeMeta = in.TypeMeta 101 | in.ListMeta.DeepCopyInto(&out.ListMeta) 102 | if in.Items != nil { 103 | in, out := &in.Items, &out.Items 104 | *out = make([]LoadBalancerConfig, len(*in)) 105 | for i := range *in { 106 | (*in)[i].DeepCopyInto(&(*out)[i]) 107 | } 108 | } 109 | } 110 | 111 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerConfigList. 112 | func (in *LoadBalancerConfigList) DeepCopy() *LoadBalancerConfigList { 113 | if in == nil { 114 | return nil 115 | } 116 | out := new(LoadBalancerConfigList) 117 | in.DeepCopyInto(out) 118 | return out 119 | } 120 | 121 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 122 | func (in *LoadBalancerConfigList) DeepCopyObject() runtime.Object { 123 | if c := in.DeepCopy(); c != nil { 124 | return c 125 | } 126 | return nil 127 | } 128 | 129 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 130 | func (in *LoadBalancerConfigSpec) DeepCopyInto(out *LoadBalancerConfigSpec) { 131 | *out = *in 132 | out.PortRange = in.PortRange 133 | } 134 | 135 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerConfigSpec. 136 | func (in *LoadBalancerConfigSpec) DeepCopy() *LoadBalancerConfigSpec { 137 | if in == nil { 138 | return nil 139 | } 140 | out := new(LoadBalancerConfigSpec) 141 | in.DeepCopyInto(out) 142 | return out 143 | } 144 | 145 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 146 | func (in *LoadBalancerConfigStatus) DeepCopyInto(out *LoadBalancerConfigStatus) { 147 | *out = *in 148 | } 149 | 150 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerConfigStatus. 151 | func (in *LoadBalancerConfigStatus) DeepCopy() *LoadBalancerConfigStatus { 152 | if in == nil { 153 | return nil 154 | } 155 | out := new(LoadBalancerConfigStatus) 156 | in.DeepCopyInto(out) 157 | return out 158 | } 159 | 160 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 161 | func (in *LoadBalancerList) DeepCopyInto(out *LoadBalancerList) { 162 | *out = *in 163 | out.TypeMeta = in.TypeMeta 164 | in.ListMeta.DeepCopyInto(&out.ListMeta) 165 | if in.Items != nil { 166 | in, out := &in.Items, &out.Items 167 | *out = make([]LoadBalancer, len(*in)) 168 | for i := range *in { 169 | (*in)[i].DeepCopyInto(&(*out)[i]) 170 | } 171 | } 172 | } 173 | 174 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerList. 175 | func (in *LoadBalancerList) DeepCopy() *LoadBalancerList { 176 | if in == nil { 177 | return nil 178 | } 179 | out := new(LoadBalancerList) 180 | in.DeepCopyInto(out) 181 | return out 182 | } 183 | 184 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 185 | func (in *LoadBalancerList) DeepCopyObject() runtime.Object { 186 | if c := in.DeepCopy(); c != nil { 187 | return c 188 | } 189 | return nil 190 | } 191 | 192 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 193 | func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { 194 | *out = *in 195 | out.HTTPUpdater = in.HTTPUpdater 196 | if in.Targets != nil { 197 | in, out := &in.Targets, &out.Targets 198 | *out = make([]Target, len(*in)) 199 | copy(*out, *in) 200 | } 201 | } 202 | 203 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerSpec. 204 | func (in *LoadBalancerSpec) DeepCopy() *LoadBalancerSpec { 205 | if in == nil { 206 | return nil 207 | } 208 | out := new(LoadBalancerSpec) 209 | in.DeepCopyInto(out) 210 | return out 211 | } 212 | 213 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 214 | func (in *LoadBalancerStatus) DeepCopyInto(out *LoadBalancerStatus) { 215 | *out = *in 216 | } 217 | 218 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerStatus. 219 | func (in *LoadBalancerStatus) DeepCopy() *LoadBalancerStatus { 220 | if in == nil { 221 | return nil 222 | } 223 | out := new(LoadBalancerStatus) 224 | in.DeepCopyInto(out) 225 | return out 226 | } 227 | 228 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 229 | func (in *PortRange) DeepCopyInto(out *PortRange) { 230 | *out = *in 231 | } 232 | 233 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortRange. 234 | func (in *PortRange) DeepCopy() *PortRange { 235 | if in == nil { 236 | return nil 237 | } 238 | out := new(PortRange) 239 | in.DeepCopyInto(out) 240 | return out 241 | } 242 | 243 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 244 | func (in *Target) DeepCopyInto(out *Target) { 245 | *out = *in 246 | } 247 | 248 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. 249 | func (in *Target) DeepCopy() *Target { 250 | if in == nil { 251 | return nil 252 | } 253 | out := new(Target) 254 | in.DeepCopyInto(out) 255 | return out 256 | } 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /config/manifests/paperlb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: manager 6 | app.kubernetes.io/created-by: paperlb 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/name: namespace 10 | app.kubernetes.io/part-of: paperlb 11 | control-plane: controller-manager 12 | name: paperlb-system 13 | --- 14 | apiVersion: apiextensions.k8s.io/v1 15 | kind: CustomResourceDefinition 16 | metadata: 17 | annotations: 18 | controller-gen.kubebuilder.io/version: v0.11.1 19 | creationTimestamp: null 20 | name: loadbalancerconfigs.lb.paperlb.com 21 | spec: 22 | group: lb.paperlb.com 23 | names: 24 | kind: LoadBalancerConfig 25 | listKind: LoadBalancerConfigList 26 | plural: loadbalancerconfigs 27 | singular: loadbalancerconfig 28 | scope: Namespaced 29 | versions: 30 | - name: v1alpha1 31 | schema: 32 | openAPIV3Schema: 33 | description: LoadBalancerConfig is the Schema for the loadbalancerconfigs API 34 | properties: 35 | apiVersion: 36 | 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' 37 | type: string 38 | kind: 39 | 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' 40 | type: string 41 | metadata: 42 | type: object 43 | spec: 44 | description: LoadBalancerConfigSpec defines the desired state of LoadBalancerConfig 45 | properties: 46 | default: 47 | description: Default defines if this config is the default config 48 | type: boolean 49 | host: 50 | description: Host is the load balancer host 51 | type: string 52 | httpUpdaterURL: 53 | description: HTTPUpdaterURL is the http updater url 54 | type: string 55 | portRange: 56 | description: PortRange is the load balancer port range 57 | properties: 58 | high: 59 | description: High is the higher limit of the port range 60 | maximum: 65535 61 | type: integer 62 | low: 63 | description: Low is the lower limit of the port range 64 | minimum: 1 65 | type: integer 66 | type: object 67 | type: object 68 | status: 69 | description: LoadBalancerConfigStatus defines the observed state of LoadBalancerConfig 70 | type: object 71 | type: object 72 | served: true 73 | storage: true 74 | subresources: 75 | status: {} 76 | --- 77 | apiVersion: apiextensions.k8s.io/v1 78 | kind: CustomResourceDefinition 79 | metadata: 80 | annotations: 81 | controller-gen.kubebuilder.io/version: v0.11.1 82 | creationTimestamp: null 83 | name: loadbalancers.lb.paperlb.com 84 | spec: 85 | group: lb.paperlb.com 86 | names: 87 | kind: LoadBalancer 88 | listKind: LoadBalancerList 89 | plural: loadbalancers 90 | singular: loadbalancer 91 | scope: Namespaced 92 | versions: 93 | - additionalPrinterColumns: 94 | - jsonPath: .spec.host 95 | name: Host 96 | type: string 97 | - jsonPath: .spec.port 98 | name: Port 99 | type: string 100 | - jsonPath: .spec.protocol 101 | name: Protocol 102 | type: string 103 | - jsonPath: .status.targetCount 104 | name: TargetCount 105 | type: string 106 | - jsonPath: .status.phase 107 | name: Status 108 | type: string 109 | name: v1alpha1 110 | schema: 111 | openAPIV3Schema: 112 | description: LoadBalancer is the Schema for the loadbalancers API 113 | properties: 114 | apiVersion: 115 | 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' 116 | type: string 117 | kind: 118 | 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' 119 | type: string 120 | metadata: 121 | type: object 122 | spec: 123 | description: LoadBalancerSpec defines the desired state of LoadBalancer 124 | properties: 125 | configName: 126 | description: ConfigName is the loadbalancer config name 127 | type: string 128 | host: 129 | description: Host is the lb host 130 | type: string 131 | httpUpdater: 132 | description: HTTPUpdater is the http updater 133 | properties: 134 | url: 135 | description: URL to request lb updates 136 | type: string 137 | type: object 138 | port: 139 | description: Port is the lb host 140 | type: integer 141 | protocol: 142 | description: Protocol is the lb protocol 143 | type: string 144 | targets: 145 | description: Targets is the list of targets 146 | items: 147 | properties: 148 | host: 149 | description: Host is the target host 150 | type: string 151 | port: 152 | description: Port is the target port 153 | type: integer 154 | type: object 155 | type: array 156 | type: object 157 | status: 158 | description: LoadBalancerStatus defines the observed state of LoadBalancer 159 | properties: 160 | phase: 161 | default: PENDING 162 | enum: 163 | - PENDING 164 | - READY 165 | type: string 166 | targetCount: 167 | description: TargetCount is the number of targets 168 | type: integer 169 | type: object 170 | type: object 171 | served: true 172 | storage: true 173 | subresources: 174 | status: {} 175 | --- 176 | apiVersion: v1 177 | kind: ServiceAccount 178 | metadata: 179 | labels: 180 | app.kubernetes.io/component: rbac 181 | app.kubernetes.io/created-by: paperlb 182 | app.kubernetes.io/instance: controller-manager 183 | app.kubernetes.io/managed-by: kustomize 184 | app.kubernetes.io/name: serviceaccount 185 | app.kubernetes.io/part-of: paperlb 186 | name: paperlb-controller-manager 187 | namespace: paperlb-system 188 | --- 189 | apiVersion: rbac.authorization.k8s.io/v1 190 | kind: Role 191 | metadata: 192 | labels: 193 | app.kubernetes.io/component: rbac 194 | app.kubernetes.io/created-by: paperlb 195 | app.kubernetes.io/instance: leader-election-role 196 | app.kubernetes.io/managed-by: kustomize 197 | app.kubernetes.io/name: role 198 | app.kubernetes.io/part-of: paperlb 199 | name: paperlb-leader-election-role 200 | namespace: paperlb-system 201 | rules: 202 | - apiGroups: 203 | - "" 204 | resources: 205 | - configmaps 206 | verbs: 207 | - get 208 | - list 209 | - watch 210 | - create 211 | - update 212 | - patch 213 | - delete 214 | - apiGroups: 215 | - coordination.k8s.io 216 | resources: 217 | - leases 218 | verbs: 219 | - get 220 | - list 221 | - watch 222 | - create 223 | - update 224 | - patch 225 | - delete 226 | - apiGroups: 227 | - "" 228 | resources: 229 | - events 230 | verbs: 231 | - create 232 | - patch 233 | --- 234 | apiVersion: rbac.authorization.k8s.io/v1 235 | kind: ClusterRole 236 | metadata: 237 | creationTimestamp: null 238 | name: paperlb-manager-role 239 | rules: 240 | - apiGroups: 241 | - lb.paperlb.com 242 | resources: 243 | - loadbalancerconfigs 244 | verbs: 245 | - create 246 | - delete 247 | - get 248 | - list 249 | - patch 250 | - update 251 | - watch 252 | - apiGroups: 253 | - lb.paperlb.com 254 | resources: 255 | - loadbalancerconfigs/status 256 | verbs: 257 | - get 258 | - patch 259 | - update 260 | - apiGroups: 261 | - lb.paperlb.com 262 | resources: 263 | - loadbalancers 264 | verbs: 265 | - create 266 | - delete 267 | - get 268 | - list 269 | - patch 270 | - update 271 | - watch 272 | - apiGroups: 273 | - lb.paperlb.com 274 | resources: 275 | - loadbalancers/finalizers 276 | verbs: 277 | - update 278 | - apiGroups: 279 | - lb.paperlb.com 280 | resources: 281 | - loadbalancers/status 282 | verbs: 283 | - get 284 | - patch 285 | - update 286 | - apiGroups: 287 | - v1 288 | resources: 289 | - nodes 290 | verbs: 291 | - get 292 | - list 293 | - watch 294 | - apiGroups: 295 | - v1 296 | resources: 297 | - nodes/status 298 | verbs: 299 | - get 300 | - apiGroups: 301 | - v1 302 | resources: 303 | - services 304 | verbs: 305 | - create 306 | - delete 307 | - get 308 | - list 309 | - patch 310 | - update 311 | - watch 312 | - apiGroups: 313 | - v1 314 | resources: 315 | - services/finalizers 316 | verbs: 317 | - update 318 | - apiGroups: 319 | - v1 320 | resources: 321 | - services/status 322 | verbs: 323 | - get 324 | - patch 325 | - update 326 | --- 327 | apiVersion: rbac.authorization.k8s.io/v1 328 | kind: ClusterRole 329 | metadata: 330 | labels: 331 | app.kubernetes.io/component: kube-rbac-proxy 332 | app.kubernetes.io/created-by: paperlb 333 | app.kubernetes.io/instance: metrics-reader 334 | app.kubernetes.io/managed-by: kustomize 335 | app.kubernetes.io/name: clusterrole 336 | app.kubernetes.io/part-of: paperlb 337 | name: paperlb-metrics-reader 338 | rules: 339 | - nonResourceURLs: 340 | - /metrics 341 | verbs: 342 | - get 343 | --- 344 | apiVersion: rbac.authorization.k8s.io/v1 345 | kind: ClusterRole 346 | metadata: 347 | labels: 348 | app.kubernetes.io/component: kube-rbac-proxy 349 | app.kubernetes.io/created-by: paperlb 350 | app.kubernetes.io/instance: proxy-role 351 | app.kubernetes.io/managed-by: kustomize 352 | app.kubernetes.io/name: clusterrole 353 | app.kubernetes.io/part-of: paperlb 354 | name: paperlb-proxy-role 355 | rules: 356 | - apiGroups: 357 | - authentication.k8s.io 358 | resources: 359 | - tokenreviews 360 | verbs: 361 | - create 362 | - apiGroups: 363 | - authorization.k8s.io 364 | resources: 365 | - subjectaccessreviews 366 | verbs: 367 | - create 368 | --- 369 | apiVersion: rbac.authorization.k8s.io/v1 370 | kind: RoleBinding 371 | metadata: 372 | labels: 373 | app.kubernetes.io/component: rbac 374 | app.kubernetes.io/created-by: paperlb 375 | app.kubernetes.io/instance: leader-election-rolebinding 376 | app.kubernetes.io/managed-by: kustomize 377 | app.kubernetes.io/name: rolebinding 378 | app.kubernetes.io/part-of: paperlb 379 | name: paperlb-leader-election-rolebinding 380 | namespace: paperlb-system 381 | roleRef: 382 | apiGroup: rbac.authorization.k8s.io 383 | kind: Role 384 | name: paperlb-leader-election-role 385 | subjects: 386 | - kind: ServiceAccount 387 | name: paperlb-controller-manager 388 | namespace: paperlb-system 389 | --- 390 | apiVersion: rbac.authorization.k8s.io/v1 391 | kind: ClusterRoleBinding 392 | metadata: 393 | labels: 394 | app.kubernetes.io/component: rbac 395 | app.kubernetes.io/created-by: paperlb 396 | app.kubernetes.io/instance: manager-rolebinding 397 | app.kubernetes.io/managed-by: kustomize 398 | app.kubernetes.io/name: clusterrolebinding 399 | app.kubernetes.io/part-of: paperlb 400 | name: paperlb-manager-rolebinding 401 | roleRef: 402 | apiGroup: rbac.authorization.k8s.io 403 | kind: ClusterRole 404 | name: paperlb-manager-role 405 | subjects: 406 | - kind: ServiceAccount 407 | name: paperlb-controller-manager 408 | namespace: paperlb-system 409 | --- 410 | apiVersion: rbac.authorization.k8s.io/v1 411 | kind: ClusterRoleBinding 412 | metadata: 413 | labels: 414 | app.kubernetes.io/component: kube-rbac-proxy 415 | app.kubernetes.io/created-by: paperlb 416 | app.kubernetes.io/instance: proxy-rolebinding 417 | app.kubernetes.io/managed-by: kustomize 418 | app.kubernetes.io/name: clusterrolebinding 419 | app.kubernetes.io/part-of: paperlb 420 | name: paperlb-proxy-rolebinding 421 | roleRef: 422 | apiGroup: rbac.authorization.k8s.io 423 | kind: ClusterRole 424 | name: paperlb-proxy-role 425 | subjects: 426 | - kind: ServiceAccount 427 | name: paperlb-controller-manager 428 | namespace: paperlb-system 429 | --- 430 | apiVersion: v1 431 | kind: Service 432 | metadata: 433 | labels: 434 | app.kubernetes.io/component: kube-rbac-proxy 435 | app.kubernetes.io/created-by: paperlb 436 | app.kubernetes.io/instance: controller-manager-metrics-service 437 | app.kubernetes.io/managed-by: kustomize 438 | app.kubernetes.io/name: service 439 | app.kubernetes.io/part-of: paperlb 440 | control-plane: controller-manager 441 | name: paperlb-controller-manager-metrics-service 442 | namespace: paperlb-system 443 | spec: 444 | ports: 445 | - name: https 446 | port: 8443 447 | protocol: TCP 448 | targetPort: https 449 | selector: 450 | control-plane: controller-manager 451 | --- 452 | apiVersion: apps/v1 453 | kind: Deployment 454 | metadata: 455 | labels: 456 | app.kubernetes.io/component: manager 457 | app.kubernetes.io/created-by: paperlb 458 | app.kubernetes.io/instance: controller-manager 459 | app.kubernetes.io/managed-by: kustomize 460 | app.kubernetes.io/name: deployment 461 | app.kubernetes.io/part-of: paperlb 462 | control-plane: controller-manager 463 | name: paperlb-controller-manager 464 | namespace: paperlb-system 465 | spec: 466 | replicas: 1 467 | selector: 468 | matchLabels: 469 | control-plane: controller-manager 470 | template: 471 | metadata: 472 | annotations: 473 | kubectl.kubernetes.io/default-container: manager 474 | labels: 475 | control-plane: controller-manager 476 | spec: 477 | affinity: 478 | nodeAffinity: 479 | requiredDuringSchedulingIgnoredDuringExecution: 480 | nodeSelectorTerms: 481 | - matchExpressions: 482 | - key: kubernetes.io/arch 483 | operator: In 484 | values: 485 | - amd64 486 | - arm64 487 | - ppc64le 488 | - s390x 489 | - key: kubernetes.io/os 490 | operator: In 491 | values: 492 | - linux 493 | containers: 494 | - args: 495 | - --secure-listen-address=0.0.0.0:8443 496 | - --upstream=http://127.0.0.1:8080/ 497 | - --logtostderr=true 498 | - --v=0 499 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 500 | name: kube-rbac-proxy 501 | ports: 502 | - containerPort: 8443 503 | name: https 504 | protocol: TCP 505 | resources: 506 | limits: 507 | cpu: 500m 508 | memory: 128Mi 509 | requests: 510 | cpu: 5m 511 | memory: 64Mi 512 | securityContext: 513 | allowPrivilegeEscalation: false 514 | capabilities: 515 | drop: 516 | - ALL 517 | - args: 518 | - --health-probe-bind-address=:8081 519 | - --metrics-bind-address=127.0.0.1:8080 520 | - --leader-elect 521 | command: 522 | - /manager 523 | image: quay.io/didil/paperlb:6202a2c 524 | livenessProbe: 525 | httpGet: 526 | path: /healthz 527 | port: 8081 528 | initialDelaySeconds: 15 529 | periodSeconds: 20 530 | name: manager 531 | readinessProbe: 532 | httpGet: 533 | path: /readyz 534 | port: 8081 535 | initialDelaySeconds: 5 536 | periodSeconds: 10 537 | resources: 538 | limits: 539 | cpu: 500m 540 | memory: 128Mi 541 | requests: 542 | cpu: 10m 543 | memory: 64Mi 544 | securityContext: 545 | allowPrivilegeEscalation: false 546 | capabilities: 547 | drop: 548 | - ALL 549 | securityContext: 550 | runAsNonRoot: true 551 | serviceAccountName: paperlb-controller-manager 552 | terminationGracePeriodSeconds: 10 553 | -------------------------------------------------------------------------------- /controllers/service_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "sort" 23 | 24 | "k8s.io/apimachinery/pkg/api/equality" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/types" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/handler" 31 | "sigs.k8s.io/controller-runtime/pkg/log" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | "sigs.k8s.io/controller-runtime/pkg/source" 34 | 35 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 | 37 | lbv1alpha1 "github.com/didil/paperlb/api/v1alpha1" 38 | "github.com/go-logr/logr" 39 | "github.com/pkg/errors" 40 | corev1 "k8s.io/api/core/v1" 41 | ) 42 | 43 | // ServiceReconciler reconciles a Service object 44 | type ServiceReconciler struct { 45 | client.Client 46 | Scheme *runtime.Scheme 47 | } 48 | 49 | //+kubebuilder:rbac:groups=v1,resources=services,verbs=get;list;watch;create;update;patch;delete 50 | //+kubebuilder:rbac:groups=v1,resources=services/status,verbs=get;update;patch 51 | //+kubebuilder:rbac:groups=v1,resources=services/finalizers,verbs=update 52 | //+kubebuilder:rbac:groups=v1,resources=nodes,verbs=get;list;watch 53 | //+kubebuilder:rbac:groups=v1,resources=nodes/status,verbs=get 54 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancers,verbs=get;list;watch;create;update;patch;delete 55 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancers/status,verbs=get;update;patch 56 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancerconfigs,verbs=get;list;watch;create;update;patch;delete 57 | //+kubebuilder:rbac:groups=lb.paperlb.com,resources=loadbalancerconfigs/status,verbs=get;update;patch 58 | 59 | func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 60 | logger := log.FromContext(ctx) 61 | 62 | svc, err := r.serviceFor(ctx, req.NamespacedName) 63 | if err != nil { 64 | // Error reading the object - requeue the request. 65 | logger.Error(err, "Failed to get service", "error", err) 66 | return ctrl.Result{}, err 67 | } 68 | if svc == nil { 69 | logger.Info("Service resource not found. Ignoring since object must be deleted") 70 | return ctrl.Result{}, nil 71 | } 72 | 73 | if !svc.ObjectMeta.DeletionTimestamp.IsZero() { 74 | // service is being deleted, skip 75 | return ctrl.Result{}, nil 76 | } 77 | 78 | if svc.Spec.Type != corev1.ServiceTypeLoadBalancer { 79 | // not a a load balancer service 80 | return ctrl.Result{}, nil 81 | } 82 | 83 | loadBalancerClass := svc.Spec.LoadBalancerClass 84 | if loadBalancerClass != nil && *loadBalancerClass != paperLBloadBalancerClass { 85 | // load balancer class is set and not ours 86 | return ctrl.Result{}, nil 87 | } 88 | 89 | var loadBalancerConfig *lbv1alpha1.LoadBalancerConfig 90 | if configName := svc.Annotations[loadBalancerConfigNameKey]; configName != "" { 91 | // find config with the name specified 92 | loadBalancerConfig, err = r.getLoadBalancerConfigByName(ctx, configName) 93 | if err != nil { 94 | logger.Error(err, "Failed to get config by name", "loadBalancerConfigName", configName, "error", err) 95 | return ctrl.Result{}, err 96 | } 97 | if loadBalancerConfig == nil { 98 | logger.Error(err, "Specified load balancer config not found", "loadBalancerConfigName", configName) 99 | return ctrl.Result{}, nil 100 | } 101 | } else { 102 | // find default config 103 | loadBalancerConfig, err = r.getDefaultLoadBalancerConfig(ctx) 104 | if err != nil { 105 | logger.Error(err, "Failed to get default config", "error", err) 106 | return ctrl.Result{}, err 107 | } 108 | if loadBalancerConfig == nil { 109 | logger.Info("Default load balancer config not found") 110 | return ctrl.Result{}, nil 111 | } 112 | } 113 | 114 | httpUpdaterURL := loadBalancerConfig.Spec.HTTPUpdaterURL 115 | if httpUpdaterURL == "" { 116 | // no http updater url set 117 | logger.Info("No http updater url set in load balancer config", "loadBalancerConfigName", loadBalancerConfig.Name) 118 | return ctrl.Result{}, nil 119 | } 120 | 121 | loadBalancerHost := loadBalancerConfig.Spec.Host 122 | if loadBalancerHost == "" { 123 | // no host set 124 | logger.Info("No host set in load balancer config", "loadBalancerConfigName", loadBalancerConfig.Name) 125 | return ctrl.Result{}, nil 126 | } 127 | 128 | if len(svc.Spec.Ports) == 0 { 129 | // no ports set 130 | logger.Info("no ports set on service") 131 | return ctrl.Result{}, nil 132 | } 133 | 134 | loadBalancerProtocol := svc.Spec.Ports[0].Protocol 135 | if loadBalancerProtocol == "" { 136 | // defaults to TCP 137 | loadBalancerProtocol = corev1.ProtocolTCP 138 | } 139 | // TCP, UDP or blank (defaults to TCP) are allowed 140 | if loadBalancerProtocol != corev1.ProtocolTCP && loadBalancerProtocol != corev1.ProtocolUDP { 141 | // protocol invalid 142 | logger.Info("Invalid Protocol Set for PaperLB load balancer", "loadBalancerProtocol", loadBalancerProtocol) 143 | return ctrl.Result{}, nil 144 | } 145 | 146 | targets, err := r.getTargets(logger, ctx, svc) 147 | if err != nil { 148 | return ctrl.Result{}, err 149 | } 150 | if targets == nil { 151 | // no targets, skip 152 | return ctrl.Result{}, err 153 | } 154 | 155 | // Define new load balancer 156 | lb, err := r.loadBalancerForService(svc, loadBalancerConfig.Name, httpUpdaterURL, loadBalancerHost, string(loadBalancerProtocol), targets) 157 | if err != nil { 158 | logger.Error(err, "Failed to build new load balancer", "LoadBalancer.Name", svc.Name) 159 | return ctrl.Result{}, err 160 | } 161 | 162 | // Check if the object already exists, if not create a new one 163 | existingLb := &lbv1alpha1.LoadBalancer{} 164 | err = r.Get(ctx, types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, existingLb) 165 | if err != nil { 166 | if apierrors.IsNotFound(err) { 167 | loadBalancerPort, err := r.findAvailableLoadBalancerPort(ctx, loadBalancerConfig) 168 | if err != nil { 169 | logger.Error(err, "Failed to find available load balancer port", "loadBalancerConfigName", loadBalancerConfig.Name, "error", err) 170 | return ctrl.Result{}, err 171 | } 172 | if loadBalancerPort == 0 { 173 | logger.Info("No available load balancer port found", "loadBalancerConfigName", loadBalancerConfig.Name) 174 | return ctrl.Result{}, nil 175 | } 176 | 177 | lb.Spec.Port = loadBalancerPort 178 | 179 | logger.Info("Creating a Load Balancer", "LoadBalancer.Name", lb.Name) 180 | err = r.Create(ctx, lb) 181 | if err != nil { 182 | logger.Error(err, "Failed to create Load Balancer", "LoadBalancer.Name", lb.Name) 183 | return ctrl.Result{}, err 184 | } 185 | 186 | // created successfully - return and requeue 187 | return ctrl.Result{Requeue: true}, nil 188 | } 189 | 190 | logger.Error(err, "Failed to get Load Balancer") 191 | return ctrl.Result{}, err 192 | } 193 | 194 | if r.lbNeedsUpdate(&logger, lb, existingLb, loadBalancerConfig) { 195 | logger.Info("Updating Load Balancer", "LoadBalancer.Name", existingLb.Name) 196 | 197 | existingLb.Spec = lb.Spec 198 | 199 | err = r.Update(ctx, existingLb) 200 | if err != nil { 201 | logger.Error(err, "Failed to update Load Balancer", "LoadBalancer.Name", existingLb.Name) 202 | return ctrl.Result{}, err 203 | } 204 | 205 | // reset to pending 206 | existingLb.Status.Phase = lbv1alpha1.LoadBalancerPhasePending 207 | err = r.Status().Update(ctx, existingLb) 208 | if err != nil { 209 | logger.Error(err, "Failed to reset Load Balancer status to pending", "LoadBalancer.Name", existingLb.Name) 210 | return ctrl.Result{}, err 211 | } 212 | 213 | // Updated successfully - return and requeue 214 | return ctrl.Result{Requeue: true}, nil 215 | } 216 | 217 | if existingLb.Status.Phase == lbv1alpha1.LoadBalancerPhaseReady { 218 | portStatus := corev1.PortStatus{} 219 | 220 | portStatus.Port = int32(existingLb.Spec.Port) 221 | 222 | loadBalancerProtocol := existingLb.Spec.Protocol 223 | switch corev1.Protocol(loadBalancerProtocol) { 224 | case corev1.ProtocolTCP: 225 | portStatus.Protocol = corev1.ProtocolTCP 226 | case corev1.ProtocolUDP: 227 | portStatus.Protocol = corev1.ProtocolUDP 228 | default: 229 | portStatus.Protocol = corev1.ProtocolTCP 230 | } 231 | 232 | ports := []corev1.PortStatus{portStatus} 233 | 234 | targetIngresses := []corev1.LoadBalancerIngress{ 235 | { 236 | IP: loadBalancerHost, 237 | Ports: ports, 238 | }, 239 | } 240 | 241 | ingresses := svc.Status.LoadBalancer.Ingress 242 | if !equality.Semantic.DeepEqual(targetIngresses, ingresses) { 243 | svc.Status.LoadBalancer.Ingress = targetIngresses 244 | 245 | logger.Info("Adding Load Balancer Host to service", "host", loadBalancerHost) 246 | 247 | err = r.Status().Update(ctx, svc) 248 | if err != nil { 249 | logger.Error(err, "Failed to update service with load balancer host", "error", err) 250 | return ctrl.Result{}, err 251 | } 252 | } 253 | } 254 | 255 | return ctrl.Result{}, nil 256 | } 257 | 258 | var paperLBSystemNamespaceName = "paperlb-system" 259 | 260 | func (r *ServiceReconciler) getLoadBalancerConfigByName(ctx context.Context, name string) (*lbv1alpha1.LoadBalancerConfig, error) { 261 | config := &lbv1alpha1.LoadBalancerConfig{} 262 | 263 | err := r.Get(ctx, types.NamespacedName{Namespace: paperLBSystemNamespaceName, Name: name}, config) 264 | if err != nil { 265 | if apierrors.IsNotFound(err) { 266 | return nil, nil 267 | } 268 | return nil, errors.Wrapf(err, "failed to fetch load balancer config") 269 | } 270 | 271 | return config, nil 272 | } 273 | 274 | func (r *ServiceReconciler) lbNeedsUpdate(logger *logr.Logger, lb, existingLb *lbv1alpha1.LoadBalancer, config *lbv1alpha1.LoadBalancerConfig) bool { 275 | if existingLb.Spec.Port >= config.Spec.PortRange.Low && existingLb.Spec.Port <= config.Spec.PortRange.High { 276 | // existing port is still in valid range 277 | // keep the same port 278 | lb.Spec.Port = existingLb.Spec.Port 279 | } else { 280 | // load balancers need to be deleted manually in case of incompatible port changes on the config 281 | logger.Info("Load balancer config update requires port change, this case is not supported at the moment, update skipped.") 282 | return false 283 | } 284 | 285 | return !equality.Semantic.DeepEqual(lb.Spec, existingLb.Spec) 286 | } 287 | 288 | func (r *ServiceReconciler) getDefaultLoadBalancerConfig(ctx context.Context) (*lbv1alpha1.LoadBalancerConfig, error) { 289 | configsList := &lbv1alpha1.LoadBalancerConfigList{} 290 | err := r.List(ctx, configsList, client.InNamespace(paperLBSystemNamespaceName)) 291 | if err != nil { 292 | return nil, errors.Wrapf(err, "failed to list configs") 293 | } 294 | if len(configsList.Items) == 0 { 295 | return nil, nil 296 | } 297 | // sort configs by creation date to be able to take only the first one if multiple have default set to true 298 | sort.Slice(configsList.Items, func(i, j int) bool { 299 | return configsList.Items[i].CreationTimestamp.Before(&configsList.Items[j].CreationTimestamp) 300 | }) 301 | 302 | for _, config := range configsList.Items { 303 | if config.Spec.Default { 304 | return &config, nil 305 | } 306 | } 307 | 308 | return nil, nil 309 | } 310 | 311 | func (r *ServiceReconciler) findAvailableLoadBalancerPort(ctx context.Context, config *lbv1alpha1.LoadBalancerConfig) (int, error) { 312 | lbList := &lbv1alpha1.LoadBalancerList{} 313 | 314 | err := r.List(context.Background(), lbList, client.MatchingFields{loadBalancerConfigNameIndexField: string(config.Name)}) 315 | if err != nil { 316 | return 0, fmt.Errorf("could not list load balancers for config name %s", config.Name) 317 | } 318 | used := map[int]bool{} 319 | for _, lb := range lbList.Items { 320 | used[lb.Spec.Port] = true 321 | } 322 | 323 | for i := config.Spec.PortRange.Low; i <= config.Spec.PortRange.High; i++ { 324 | if !used[i] { 325 | return i, nil 326 | } 327 | } 328 | 329 | // no available port found 330 | return 0, nil 331 | } 332 | 333 | func (r *ServiceReconciler) findExternalIP(node *corev1.Node) string { 334 | addrs := node.Status.Addresses 335 | for _, addr := range addrs { 336 | if addr.Type == corev1.NodeExternalIP { 337 | return addr.Address 338 | } 339 | } 340 | return "" 341 | } 342 | 343 | func (r *ServiceReconciler) getTargets(logger logr.Logger, ctx context.Context, svc *corev1.Service) ([]lbv1alpha1.Target, error) { 344 | portsData := svc.Spec.Ports[0] 345 | 346 | nodePort := portsData.NodePort 347 | if nodePort == 0 { 348 | // nodeport not set 349 | logger.Info("nodeport not set on service") 350 | return nil, nil 351 | } 352 | 353 | // get nodes 354 | nodes := &corev1.NodeList{} 355 | err := r.List(ctx, nodes) 356 | if err != nil { 357 | logger.Error(err, "Failed to get nodes") 358 | return nil, err 359 | } 360 | 361 | targets := []lbv1alpha1.Target{} 362 | for _, node := range nodes.Items { 363 | if !r.isNodeReady(&node) { 364 | continue 365 | } 366 | 367 | host := r.findExternalIP(&node) 368 | if host == "" { 369 | logger.Error(err, "Failed to get external ip for node. Skipping node", "node", node.Name) 370 | continue 371 | } 372 | targets = append(targets, lbv1alpha1.Target{Host: host, Port: int(nodePort)}) 373 | } 374 | 375 | if len(targets) == 0 { 376 | // no targets 377 | logger.Info("no targets") 378 | return nil, nil 379 | } 380 | 381 | return targets, nil 382 | } 383 | 384 | func (r *ServiceReconciler) isNodeReady(node *corev1.Node) bool { 385 | for _, cond := range node.Status.Conditions { 386 | if cond.Type == corev1.NodeReady && cond.Status == corev1.ConditionTrue { 387 | // only target nodes that are healthy 388 | return true 389 | } 390 | } 391 | 392 | return false 393 | } 394 | 395 | func (r *ServiceReconciler) loadBalancerForService(svc *corev1.Service, configName string, httpUpdaterURL string, loadBalancerHost string, loadBalancerProtocol string, targets []lbv1alpha1.Target) (*lbv1alpha1.LoadBalancer, error) { 396 | lb := &lbv1alpha1.LoadBalancer{ 397 | ObjectMeta: metav1.ObjectMeta{ 398 | Name: svc.Name, 399 | Namespace: svc.Namespace, 400 | }, 401 | Spec: lbv1alpha1.LoadBalancerSpec{ 402 | ConfigName: configName, 403 | HTTPUpdater: lbv1alpha1.HTTPUpdater{ 404 | URL: httpUpdaterURL, 405 | }, 406 | Host: loadBalancerHost, 407 | Protocol: loadBalancerProtocol, 408 | Targets: targets, 409 | }, 410 | } 411 | // Set Service instance as the owner and controller 412 | err := ctrl.SetControllerReference(svc, lb, r.Scheme) 413 | if err != nil { 414 | return nil, err 415 | } 416 | 417 | return lb, nil 418 | } 419 | 420 | const loadBalancerConfigNameKey = "lb.paperlb.com/config-name" 421 | 422 | const paperLBloadBalancerClass = "lb.paperlb.com/paperlb-class" 423 | 424 | func (r *ServiceReconciler) serviceFor(ctx context.Context, name types.NamespacedName) (*corev1.Service, error) { 425 | svc := &corev1.Service{} 426 | err := r.Get(ctx, name, svc) 427 | if err != nil { 428 | if apierrors.IsNotFound(err) { 429 | // Request object not found, could have been deleted after reconcile request. 430 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 431 | // No errors 432 | return nil, nil 433 | } 434 | 435 | // Error reading the object - requeue the request. 436 | return nil, err 437 | } 438 | 439 | return svc, nil 440 | } 441 | 442 | // SetupWithManager sets up the controller with the Manager. 443 | func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { 444 | if err := r.createServiceTypeIndex(mgr); err != nil { 445 | return errors.Wrapf(err, "failed to create service type index") 446 | } 447 | 448 | if err := r.createLoadBalancerConfigNameIndex(mgr); err != nil { 449 | return errors.Wrapf(err, "failed to create load balancer config name index") 450 | } 451 | 452 | return ctrl.NewControllerManagedBy(mgr). 453 | For(&corev1.Service{}). 454 | // watches node changes to be able to update targets 455 | Watches(&source.Kind{Type: &corev1.Node{}}, handler.EnqueueRequestsFromMapFunc(r.mapNodeToServices)). 456 | // watches config changes to be able to update load balancer params 457 | Watches(&source.Kind{Type: &lbv1alpha1.LoadBalancerConfig{}}, handler.EnqueueRequestsFromMapFunc(r.mapLoadBalancerConfigToServices)). 458 | Owns(&lbv1alpha1.LoadBalancer{}). 459 | Complete(r) 460 | } 461 | 462 | const serviceTypeIndexField = ".spec.Type" 463 | 464 | func (r *ServiceReconciler) createServiceTypeIndex(mgr ctrl.Manager) error { 465 | return mgr.GetFieldIndexer().IndexField( 466 | context.Background(), 467 | &corev1.Service{}, 468 | serviceTypeIndexField, 469 | func(object client.Object) []string { 470 | svc := object.(*corev1.Service) 471 | return []string{string(svc.Spec.Type)} 472 | }) 473 | } 474 | 475 | const loadBalancerConfigNameIndexField = ".spec.ConfigName" 476 | 477 | func (r *ServiceReconciler) createLoadBalancerConfigNameIndex(mgr ctrl.Manager) error { 478 | return mgr.GetFieldIndexer().IndexField( 479 | context.Background(), 480 | &lbv1alpha1.LoadBalancer{}, 481 | loadBalancerConfigNameIndexField, 482 | func(object client.Object) []string { 483 | lb := object.(*lbv1alpha1.LoadBalancer) 484 | return []string{string(lb.Spec.ConfigName)} 485 | }) 486 | } 487 | 488 | func (r *ServiceReconciler) mapNodeToServices(object client.Object) []reconcile.Request { 489 | node := object.(*corev1.Node) 490 | 491 | ctx := context.Background() 492 | logger := log.FromContext(ctx) 493 | 494 | serviceList := &corev1.ServiceList{} 495 | 496 | err := r.List(context.Background(), serviceList, client.MatchingFields{serviceTypeIndexField: string(corev1.ServiceTypeLoadBalancer)}) 497 | if err != nil { 498 | logger.Error(err, "could not list services", "node", node.Name) 499 | return nil 500 | } 501 | 502 | requests := make([]reconcile.Request, 0, len(serviceList.Items)) 503 | 504 | for _, svc := range serviceList.Items { 505 | requests = append(requests, reconcile.Request{ 506 | NamespacedName: client.ObjectKeyFromObject(&svc), 507 | }) 508 | } 509 | 510 | return requests 511 | } 512 | 513 | func (r *ServiceReconciler) mapLoadBalancerConfigToServices(object client.Object) []reconcile.Request { 514 | lbConfig := object.(*lbv1alpha1.LoadBalancerConfig) 515 | 516 | ctx := context.Background() 517 | logger := log.FromContext(ctx) 518 | 519 | lbList := &lbv1alpha1.LoadBalancerList{} 520 | 521 | err := r.List(context.Background(), lbList, client.MatchingFields{loadBalancerConfigNameIndexField: string(lbConfig.Name)}) 522 | if err != nil { 523 | logger.Error(err, "could not list load balancers", "lbConfigName", lbConfig.Name) 524 | return nil 525 | } 526 | 527 | requests := make([]reconcile.Request, 0, len(lbList.Items)) 528 | 529 | for _, lb := range lbList.Items { 530 | ownerReferences := lb.GetOwnerReferences() 531 | for _, ownerReference := range ownerReferences { 532 | if ownerReference.Kind == "Service" { 533 | requests = append(requests, reconcile.Request{ 534 | NamespacedName: types.NamespacedName{ 535 | Namespace: lb.Namespace, 536 | Name: ownerReference.Name, 537 | }, 538 | }) 539 | } 540 | } 541 | } 542 | 543 | return requests 544 | } 545 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 37 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 38 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 39 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 40 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 41 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 42 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 43 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 44 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 45 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 46 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 47 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 48 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 49 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 50 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 52 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 53 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 54 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 55 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 56 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 57 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 59 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 61 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 62 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 63 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 64 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 65 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 66 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 67 | github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 68 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 69 | github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= 70 | github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= 71 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 72 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 73 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 74 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 75 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 76 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 77 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 78 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 79 | github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 80 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 81 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 82 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 83 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 84 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 85 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 86 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 87 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 88 | github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= 89 | github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= 90 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 91 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 92 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 93 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 94 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 95 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 96 | github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= 97 | github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 98 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 99 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 100 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 101 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 102 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 103 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 104 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 105 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 106 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 107 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 109 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 110 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 111 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 112 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 113 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 114 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 115 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 116 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 117 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 118 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 121 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 122 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 123 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 124 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 125 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 126 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 127 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 128 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 129 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 130 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 131 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 132 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 133 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 134 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 135 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 136 | github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= 137 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 138 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 139 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 140 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 141 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 146 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 147 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 148 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 149 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 150 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 151 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 152 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 153 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 154 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 155 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 156 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 160 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 161 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 162 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 163 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 164 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 165 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 166 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 167 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 168 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 169 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 170 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 171 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 172 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 173 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 174 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 175 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 176 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 177 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 178 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 179 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 180 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 181 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 182 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 183 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 184 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 185 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 186 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 187 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 188 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 189 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 190 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 191 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 192 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 193 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 194 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 195 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 196 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 197 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 198 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 199 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 200 | github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= 201 | github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 202 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 203 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 204 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 205 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 206 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 207 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 208 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 209 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 210 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 211 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 212 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 213 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 214 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 215 | github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= 216 | github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= 217 | github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= 218 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 219 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 220 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 221 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 222 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 223 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 224 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 225 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 226 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 227 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 228 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 229 | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 230 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 231 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 232 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 233 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 234 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 235 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 236 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 237 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 238 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 239 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 240 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 241 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 242 | github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= 243 | github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= 244 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 245 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 246 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 247 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 248 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 249 | github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= 250 | github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= 251 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 252 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 253 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 254 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 255 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 256 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 257 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 258 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 259 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 260 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 261 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 262 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 263 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 264 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 265 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 266 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 267 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 268 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 269 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 270 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 271 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 272 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 273 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 274 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 275 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 276 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 277 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 278 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 279 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 280 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 281 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 282 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 283 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 284 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 285 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 286 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 287 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 288 | go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 289 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 290 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 291 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 292 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 293 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 294 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 295 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 296 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 297 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 298 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 299 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 300 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 301 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 302 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 303 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 304 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 305 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 306 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 307 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 308 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 309 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 310 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 311 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 312 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 313 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 314 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 315 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 316 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 317 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 318 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 319 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 320 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 321 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 322 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 323 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 324 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 325 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 326 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 327 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 328 | golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= 329 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 330 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 331 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 332 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 333 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 334 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 335 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 336 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 337 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 338 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 339 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 340 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 341 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 342 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 343 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 344 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 345 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 346 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 347 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 348 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 349 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 350 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 351 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 352 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 353 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 354 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 355 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 356 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 357 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 358 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 359 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 360 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 361 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 362 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 363 | golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= 364 | golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 365 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 366 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 367 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 368 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 369 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 370 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 371 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= 372 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 373 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 374 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 375 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 376 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 377 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 378 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 379 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 380 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 381 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 382 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 383 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 384 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 385 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 386 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 387 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 388 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 421 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 422 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 423 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 424 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 425 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 426 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 427 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 429 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 430 | golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= 431 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 432 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 433 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 434 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 435 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 436 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 437 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 438 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 439 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 440 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 441 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 442 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 443 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 444 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 445 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 446 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 447 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 448 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 449 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 450 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 451 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 452 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 453 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 454 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 455 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 456 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 457 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 458 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 460 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 461 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 462 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 463 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 464 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 465 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 466 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 467 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 468 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 469 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 470 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 471 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 472 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 473 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 474 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 475 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 476 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 477 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 478 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 479 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 480 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 481 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 482 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 483 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 484 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 485 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 486 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 487 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 488 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 489 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 490 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= 491 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 492 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 493 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 494 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 495 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 496 | gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= 497 | gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= 498 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 499 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 500 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 501 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 502 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 503 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 504 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 505 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 506 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 507 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 508 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 509 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 510 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 511 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 512 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 513 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 514 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 515 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 516 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 517 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 518 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 519 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 520 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 521 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 522 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 523 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 524 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 525 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 526 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 527 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 528 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 529 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 530 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 531 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 532 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 533 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 534 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 535 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 536 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 537 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 538 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 539 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 540 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 541 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 542 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 543 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 544 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 545 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 546 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 547 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 548 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 549 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 550 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 551 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 552 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 553 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 554 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 555 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 556 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 557 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 558 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 559 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 560 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 561 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 562 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 563 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 564 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 565 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 566 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 567 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 568 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 569 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 570 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 571 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 572 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 573 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 574 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 575 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 576 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 577 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 578 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 579 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 580 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 581 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 582 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 583 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 584 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 585 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 586 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 587 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 588 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 589 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 590 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 591 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 592 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 593 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 594 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 595 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 596 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 597 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 598 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 599 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 600 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 601 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 602 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 603 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 604 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 605 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 606 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 607 | k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= 608 | k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= 609 | k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= 610 | k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= 611 | k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= 612 | k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= 613 | k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= 614 | k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= 615 | k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= 616 | k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= 617 | k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= 618 | k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 619 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= 620 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= 621 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= 622 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 623 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 624 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 625 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 626 | sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM= 627 | sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= 628 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= 629 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 630 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 631 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 632 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 633 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 634 | --------------------------------------------------------------------------------