funcmain() { var metricsAddr string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool
// flag 是 go 的 SDK,用于解析命令行参数,比如 --metrics-bind-address=:8081 就可以覆盖默认值 8080 flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", false, "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse()
// if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancelation and // Rapid Reset CVEs. For more information see: // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 // - https://github.com/advisories/GHSA-4374-p667-p6c8 disableHTTP2 := func(c *tls.Config) { setupLog.Info("disabling http/2") c.NextProtos = []string{"http/1.1"} }
// 初始化 manager,这是 controller runtime 的惯用法 mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, }, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "48ab7dee.tutorial.controller.io", // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly // speeds up voluntary leader transitions as the new leader don't have to wait // LeaseDuration time first. // // In the default scaffold provided, the program ends immediately after // the manager stops, so would be fine to enable this option. However, // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) }
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) }
.PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
.PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
$ kubebuilder create api --group resource --version v1 --kind Quota INFO Create Resource [y/n] y INFO Create Controller [y/n] y INFO Writing kustomize manifests for you to edit... INFO Writing scaffold for you to edit... INFO api/v1/quota_types.go INFO api/v1/groupversion_info.go INFO internal/controller/suite_test.go INFO internal/controller/quota_controller.go INFO internal/controller/quota_controller_test.go INFO Update dependencies:
// Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the Quota object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile func(r *QuotaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx)
// TODO(user): your logic here
return ctrl.Result{}, nil }
// SetupWithManager sets up the controller with the Manager. func(r *QuotaReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&resourcev1.Quota{}). Complete(r) }
BeforeEach(func() { By("creating the custom resource for the Kind Quota") err := k8sClient.Get(ctx, typeNamespacedName, quota) if err != nil && errors.IsNotFound(err) { resource := &resourcev1.Quota{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, // TODO(user): Specify other spec details if needed. } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } })
AfterEach(func() { // TODO(user): Cleanup logic after each test, like removing the resource instance. resource := &resourcev1.Quota{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Quota") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) It("should successfully reconcile the resource", func() { By("Reconciling the created resource") controllerReconciler := &QuotaReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), }
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) })
// Quota is the Schema for the quotas API type Quota struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"`
Spec QuotaSpec `json:"spec,omitempty"` Status QuotaStatus `json:"status,omitempty"` }
//+kubebuilder:object:root=true
// QuotaList contains a list of Quota type QuotaList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Quota `json:"items"` }
$ cat config/crd/bases/resource.tutorial.controller.io_quotas.yaml --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.14.0 name: quotas.resource.tutorial.controller.io spec: group: resource.tutorial.controller.io names: kind: Quota listKind: QuotaList plural: quotas singular: quota scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: description: Quota is the Schema for the quotas API properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: QuotaSpec defines the desired state of Quota properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: ResourceList is a set of (resource name, quantity) pairs. type: object type: object status: description: QuotaStatus defines the observed state of Quota type: object type: object served: true storage: true subresources: status: {}
INFO Writing kustomize manifests for you to edit... INFO Writing scaffold for you to edit... INFO api/v1/quota_webhook.go INFO api/v1/quota_webhook_test.go INFO api/v1/webhook_suite_test.go INFO Update dependencies:
On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: PROJECT new file: api/v1/quota_webhook.go # 新增了 Webhook 的实现函数 new file: api/v1/quota_webhook_test.go new file: api/v1/webhook_suite_test.go modified: api/v1/zz_generated.deepcopy.go modified: cmd/main.go new file: config/certmanager/certificate.yaml # 新增了 certmanager 组件,用于处理 Webhook 的证书问题,下面会解释 new file: config/certmanager/kustomization.yaml new file: config/certmanager/kustomizeconfig.yaml modified: config/crd/kustomization.yaml new file: config/crd/patches/cainjection_in_quotas.yaml new file: config/crd/patches/webhook_in_quotas.yaml modified: config/default/kustomization.yaml new file: config/default/manager_webhook_patch.yaml # 为 manager Deployment 增加了 webhook 的配置 new file: config/default/webhookcainjection_patch.yaml new file: config/webhook/kustomization.yaml new file: config/webhook/kustomizeconfig.yaml new file: config/webhook/service.yaml # 新增了 Service 用于 Webhook 服务 modified: go.mod
好奇的你肯定还有一个疑问,为什么路径是 /validate-resource-tutorial-controller-io-v1-quota 和 /mutate-resource-tutorial-controller-io-v1-quota,是因为 kubebuilder 想要简化这个流程,就是用了特定的规则来生成这样的 API 地址,在我们自己的代码里面是没有找到相关的实现的。这点也是 Go 社区很奇怪的设计之一。
// Default implements webhook.Defaulter so a webhook will be registered for the type // 实现 --defaulting 的功能,这里就是一个 mutating wenhook 的主题逻辑,所以上面可以看到 mutating=true func(r *Quota) Default() { quotalog.Info("default", "name", r.Name)
// TODO(user): fill in your defaulting logic. }
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. //+kubebuilder:webhook:path=/validate-resource-tutorial-controller-io-v1-quota,mutating=false,failurePolicy=fail,sideEffects=None,groups=resource.tutorial.controller.io,resources=quotas,verbs=create;update,versions=v1,name=vquota.kb.io,admissionReviewVersions=v1
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type // 创建的函数调用这里验证 func(r *Quota) ValidateCreate() (admission.Warnings, error) { quotalog.Info("validate create", "name", r.Name)
// TODO(user): fill in your validation logic upon object creation. returnnil, nil }
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type // 更新这里函数 func(r *Quota) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { quotalog.Info("validate update", "name", r.Name)
// TODO(user): fill in your validation logic upon object update. returnnil, nil }
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type // 删除这里验证 func(r *Quota) ValidateDelete() (admission.Warnings, error) { quotalog.Info("validate delete", "name", r.Name)
// TODO(user): fill in your validation logic upon object deletion. returnnil, nil }
// Checking if the path is already registered. // If so, just skip it. if !blder.isAlreadyHandled(path) { log.Info("Registering a mutating webhook", "GVK", blder.gvk, "path", path) // 实际上就是注册一个 mutating webhook 服务,路径就是下面生成的,mvh 就是逻辑,就是我们上面编写的 Default() blder.mgr.GetWebhookServer().Register(path, mwh) } } }
patches: # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. -path:manager_auth_proxy_patch.yaml
# 我们直接将这里注释即可
# - path: manager_auth_proxy_patch.yaml
这里刚好体现了 kustomize 的优越,我们只要注释这里就可以了
修改支持 CERTMANAGER 还是刚刚的文件
1 2 3 4 5 6 7
# config/default/kustomization.yaml -../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager
# config/default/kustomization.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -path:webhookcainjection_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations replacements: -source:# Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs kind:Certificate group:cert-manager.io version:v1 name:serving-cert# this name should match the one in certificate.yaml fieldPath:.metadata.namespace# namespace of the certificate CR targets: -select: kind:ValidatingWebhookConfiguration fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:0 create:true -select: kind:MutatingWebhookConfiguration fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:0 create:true -select: kind:CustomResourceDefinition fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:0 create:true -source: kind:Certificate group:cert-manager.io version:v1 name:serving-cert# this name should match the one in certificate.yaml fieldPath:.metadata.name targets: -select: kind:ValidatingWebhookConfiguration fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:1 create:true -select: kind:MutatingWebhookConfiguration fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:1 create:true -select: kind:CustomResourceDefinition fieldPaths: -.metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter:'/' index:1 create:true -source:# Add cert-manager annotation to the webhook Service kind:Service version:v1 name:webhook-service fieldPath:.metadata.name# namespace of the service targets: -select: kind:Certificate group:cert-manager.io version:v1 fieldPaths: -.spec.dnsNames.0 -.spec.dnsNames.1 options: delimiter:'.' index:0 create:true -source: kind:Service version:v1 name:webhook-service fieldPath:.metadata.namespace# namespace of the service targets: -select: kind:Certificate group:cert-manager.io version:v1 fieldPaths: -.spec.dnsNames.0 -.spec.dnsNames.1 options: delimiter:'.' index:1 create:true
$ k get pod -n cert-manager NAME READY STATUS RESTARTS AGE cert-manager-56d77974fd-2q7jq 1/1 Running 0 3m37s cert-manager-cainjector-855c6869b7-c65nb 1/1 Running 0 3m37s cert-manager-webhook-69c88dbbd7-2klv8 1/1 Running 0 3m37s
构建我们的系统
1
$ make docker-build docker-push IMG=manager:0.0.1
部署
1 2 3 4
$ make deploy IMG=manager:0.0.1 $ k get pod -n quota-limit-system NAME READY STATUS RESTARTS AGE quota-limit-controller-manager-5cf66977db-4wntp 1/1 Running 0 13s
我们尝试 apply 一个 quota,就可以看到日志
1 2
2024-03-27T03:53:52Z INFO quota-resource default {"name": "test"} 2024-03-27T03:53:52Z INFO quota-resource validate create {"name": "test"}
type PodValidator struct { Client client.Client Decoder *admission.Decoder }
func(v *PodValidator) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { log := logf.FromContext(ctx) pod, ok := obj.(*v1.Pod) if !ok { returnnil, fmt.Errorf("expected a Pod but got a %T", obj) }
log.Info("Validating Pod") key := "user"
// 获得用户的 "user" 信息 userName, found := pod.Labels[key] if !found { returnnil, fmt.Errorf("missing label %s", key) }
// 累计当前所有的 for i := range podList.Items { pod := podList.Items[i] for j := range pod.Spec.Containers { container := pod.Spec.Containers[j] cpu := container.Resources.Limits.Cpu().DeepCopy() cpuResource.Add(cpu) mem := container.Resources.Limits.Memory().DeepCopy() memResource.Add(mem) } }
if cpuResource.Cmp(quota.Spec.Limits.Cpu().DeepCopy()) < 0 || memResource.Cmp(quota.Spec.Limits.Memory().DeepCopy()) < 0 { returnnil, fmt.Errorf("user %s, limit out of quota", userName) }