Repository: kubeclipper/kubeclipper
Pull request #505 (2023-01-03 01:53:52)

feat: add load balancer addon
Signed-off-by: qinyer <qy913726062@gmail.com>

<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubeclipper/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubeclipper/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding convetions followed by Kubeclipper community: https://github.com/kubeclipper/community/blob/master/developer-guide/development/coding-conventions.md
-->

### What type of PR is this?
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature

### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

### Special notes for reviewers:
```
```

### Does this PR introduced a user-facing change?
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md
-->
```release-note

```

### Additional documentation, usage docs, etc.:
<!--
This section can be blank if this pull request does not require a release note.
Please use the following format for linking documentation or pass the
section below:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

kubeclipper-bot: @qinyer: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
kubeclipper-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/505#" title="Author self-approved">qinyer</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **wu-wenxiang** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @wu-wenxiang` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubeclipper%2Fkubeclipper).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubeclipper/kubeclipper/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["wu-wenxiang"]} -->
codecov-commenter: # [Codecov](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) Report
> Merging [#505](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (0ba3370) into [master](https://codecov.io/gh/kubeclipper/kubeclipper/commit/4f930304da083e571a9f96c9488b94e5f63ec88f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (4f93030) will **decrease** coverage by `0.06%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/graphs/tree.svg?width=650&height=150&src=pr&token=3PWFG2MHTE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)

```diff
@@            Coverage Diff             @@
##           master     #505      +/-   ##
==========================================
- Coverage   13.11%   13.04%   -0.07%     
==========================================
  Files         121      121              
  Lines       18927    18974      +47     
==========================================
- Hits         2483     2476       -7     
- Misses      16164    16228      +64     
+ Partials      280      270      -10     
```


| [Impacted Files](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) | Coverage Δ | |
|---|---|---|
| [pkg/component/validation/validation.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC92YWxpZGF0aW9uL3ZhbGlkYXRpb24uZ28=) | `76.19% <0.00%> (-23.81%)` | :arrow_down: |
| [pkg/clusteroperation/node.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NsdXN0ZXJvcGVyYXRpb24vbm9kZS5nbw==) | `60.25% <0.00%> (-4.49%)` | :arrow_down: |
| [pkg/apis/core/v1/utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS91dGlscy5nbw==) | `48.64% <0.00%> (-0.82%)` | :arrow_down: |
| [pkg/component/nfs/nfs.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2NvbXBvbmVudC9uZnMvbmZzLmdv) | `1.92% <0.00%> (-0.03%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/node.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9ub2RlLmdv) | `0.32% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/cluster.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9jbHVzdGVyLmdv) | `0.12% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/apis/core/v1/handler.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS9oYW5kbGVyLmdv) | `0.36% <0.00%> (-0.01%)` | :arrow_down: |
| [pkg/scheme/core/v1/k8s/kubeadm\_step.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3NjaGVtZS9jb3JlL3YxL2s4cy9rdWJlYWRtX3N0ZXAuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/utils/netutil/netutil.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL3V0aWxzL25ldHV0aWwvbmV0dXRpbC5nbw==) | `30.00% <0.00%> (+5.00%)` | :arrow_up: |

x893675: /cc @zhuzhenfan 
zhuzhenfan: /lgtm
kubeclipper-bot: LGTM label has been added.  <details>Git tree hash: a82d6741bb88666e529bb738b4b716774ca91454</details>
x893675: /lgtm
x893675: /approve
kubeclipper-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/505#" title="Author self-approved">qinyer</a>*, *<a href="https://github.com/kubeclipper/kubeclipper/pull/505#issuecomment-1370648603" title="Approved">x893675</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubeclipper%2Fkubeclipper).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubeclipper/kubeclipper/blob/master/OWNERS)~~ [x893675]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/cmd/kubeclipper-agent/main.go b/cmd/kubeclipper-agent/main.go
index 5e93d79..6455443 100644
--- a/cmd/kubeclipper-agent/main.go
+++ b/cmd/kubeclipper-agent/main.go
@@ -23,6 +23,7 @@ import (
 	"os"
 
 	"github.com/kubeclipper/kubeclipper/cmd/kubeclipper-agent/app"
+	_ "github.com/kubeclipper/kubeclipper/pkg/component/metallb"
 	_ "github.com/kubeclipper/kubeclipper/pkg/component/nfs"
 	_ "github.com/kubeclipper/kubeclipper/pkg/component/nfscsi"
 	_ "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/cri"
diff --git a/cmd/kubeclipper-server/main.go b/cmd/kubeclipper-server/main.go
index e4c1eba..a570a66 100644
--- a/cmd/kubeclipper-server/main.go
+++ b/cmd/kubeclipper-server/main.go
@@ -27,6 +27,7 @@ import (
 	"github.com/kubeclipper/kubeclipper/cmd/kubeclipper-server/app"
 	_ "github.com/kubeclipper/kubeclipper/pkg/authentication/identityprovider/oidc"
 	_ "github.com/kubeclipper/kubeclipper/pkg/clustermanage/kubeadm"
+	_ "github.com/kubeclipper/kubeclipper/pkg/component/metallb"
 	_ "github.com/kubeclipper/kubeclipper/pkg/component/nfs"
 	_ "github.com/kubeclipper/kubeclipper/pkg/component/nfscsi"
 	_ "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/cri"
diff --git a/pkg/component/interface.go b/pkg/component/interface.go
index 6ac3dc0..55078c5 100644
--- a/pkg/component/interface.go
+++ b/pkg/component/interface.go
@@ -42,6 +42,7 @@ const (
 	InternalCategoryKubernetes = "kubernetes"
 	InternalCategoryStorage    = "storage"
 	InternalCategoryPAAS       = "PAAS"
+	InternalCategoryLB         = "LB"
 )
 
 type Props struct {
diff --git a/pkg/component/metallb/i18n.go b/pkg/component/metallb/i18n.go
new file mode 100644
index 0000000..40259f4
--- /dev/null
+++ b/pkg/component/metallb/i18n.go
@@ -0,0 +1,46 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper Authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package metallb
+
+import "github.com/kubeclipper/kubeclipper/pkg/component"
+
+func initI18nForComponentMeta() error {
+	return component.AddI18nMessages(component.I18nMessages{
+		{
+			ID:      "metallb.metaTitle",
+			English: "metallb Setting",
+			Chinese: "metallb 设置",
+		},
+		{
+			ID:      "metallb.mode",
+			English: "Mode",
+			Chinese: "模式",
+		},
+		{
+			ID:      "metallb.addresses",
+			English: "IPAddressPool",
+			Chinese: "IP 地址池",
+		},
+		{
+			ID:      "metallb.imageRepoMirror",
+			English: "metallb Image Repository Mirror",
+			Chinese: "metallb 镜像仓库代理",
+		},
+	})
+}
diff --git a/pkg/component/metallb/metallb.go b/pkg/component/metallb/metallb.go
new file mode 100644
index 0000000..383ed4b
--- /dev/null
+++ b/pkg/component/metallb/metallb.go
@@ -0,0 +1,488 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper Authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *     http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package metallb
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/kubeclipper/kubeclipper/pkg/utils/cmdutil"
+
+	"github.com/kubeclipper/kubeclipper/pkg/component/common"
+
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+
+	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/kubeclipper/kubeclipper/pkg/component"
+	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
+	"github.com/kubeclipper/kubeclipper/pkg/component/validation"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/fileutil"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/netutil"
+	tmplutil "github.com/kubeclipper/kubeclipper/pkg/utils/template"
+)
+
+func init() {
+	l := &MetalLB{}
+	if err := component.Register(fmt.Sprintf(component.RegisterFormat, name, version), l); err != nil {
+		panic(err)
+	}
+
+	if err := component.RegisterTemplate(fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, metallb), l); err != nil {
+		panic(err)
+	}
+
+	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterStepKeyFormat, name, version, metallb), l); err != nil {
+		panic(err)
+	}
+
+	if err := initI18nForComponentMeta(); err != nil {
+		panic(err)
+	}
+}
+
+var (
+	_ component.Interface      = (*MetalLB)(nil)
+	_ component.TemplateRender = (*MetalLB)(nil)
+	_ component.StepRunnable   = (*MetalLB)(nil)
+)
+
+const (
+	metallb        = "metallb"
+	name           = "metallb"
+	version        = "v1"
+	namespace      = "metallb-system"
+	filename       = "metallb.yaml"
+	ipAddressPool  = "ip-address-pool.yaml"
+	advertisement  = "advertisement.yaml"
+	manifestsDir   = "/tmp/.metallb"
+	defaultMode    = "L2"
+	defaultVersion = "v0.13.7"
+)
+
+var (
+	errInvalidAddressPool = errors.New("invalid ip address pool")
+)
+
+type MetalLB struct {
+	ImageRepoMirror                            string   `json:"imageRepoMirror"` // optional
+	ManifestsDir                               string   `json:"manifestsDir"`    // optional
+	Mode                                       string   `json:"mode"`            // required
+	Addresses                                  []string `json:"addresses"`       // required
+	Version                                    string   `json:"version"`         // optional
+	installSteps, uninstallSteps, upgradeSteps []v1.Step
+}
+
+func (n *MetalLB) Ns() string {
+	return namespace
+}
+
+func (n *MetalLB) Svc() string {
+	return ""
+}
+
+func (n *MetalLB) RequestPath() string {
+	return ""
+}
+
+// Supported TODO: add metallb healthy condition check
+func (n *MetalLB) Supported() bool {
+	return false
+}
+
+func (n *MetalLB) GetInstanceName() string {
+	return name
+}
+
+func (n *MetalLB) RequireExtraCluster() []string {
+	return nil
+}
+
+func (n *MetalLB) CompleteWithExtraCluster(extra map[string]component.ExtraMetadata) error {
+	return nil
+}
+
+func (n *MetalLB) Validate() error {
+	// load balancer mode
+	if err := validation.MatchLoadBalancerMode(n.Mode); err != nil {
+		return err
+	}
+
+	// valid ip address pool
+	for _, address := range n.Addresses {
+		if strings.Contains(address, "/") {
+			if _, _, err := net.ParseCIDR(address); err != nil {
+				return errInvalidAddressPool
+			}
+		} else if strings.Contains(address, "-") {
+			for _, ip := range strings.Split(address, "-") {
+				if !netutil.IsValidIP(ip) {
+					return errInvalidAddressPool
+				}
+			}
+		} else {
+			if !netutil.IsValidIP(address) {
+				return errInvalidAddressPool
+			}
+		}
+	}
+
+	return nil
+}
+
+func (n *MetalLB) InitSteps(ctx context.Context) error {
+	metadata := component.GetExtraMetadata(ctx)
+	// when the component does not specify an ImageRepoMirror, the cluster LocalRegistry is inherited
+	if n.ImageRepoMirror == "" {
+		n.ImageRepoMirror = metadata.LocalRegistry
+	}
+	if metadata.Offline && n.ImageRepoMirror == "" {
+		// TODO: arch is unnecessary, version can be configured
+		imager := &common.Imager{
+			PkgName: metallb,
+			Version: defaultVersion,
+			CriName: metadata.CRI,
+			Offline: metadata.Offline,
+		}
+		steps, err := imager.InstallSteps(metadata.GetAllNodes())
+		if err != nil {
+			return err
+		}
+		n.installSteps = append(n.installSteps, steps...)
+	}
+
+	master := utils.UnwrapNodeList(metadata.Masters[:1])
+	if len(metadata.Masters) > 1 && metadata.ClusterStatus == v1.ClusterRunning {
+		avaMasters, err := metadata.Masters.AvailableKubeMasters()
+		if err != nil {
+			return err
+		}
+		master = utils.UnwrapNodeList(avaMasters[:1])
+	}
+	// strict ARP
+	arpStep := v1.Step{
+		ID:         strutil.GetUUID(),
+		Name:       "enableKubeProxyStrictARP",
+		Timeout:    metav1.Duration{Duration: 3 * time.Second},
+		ErrIgnore:  false,
+		RetryTimes: 1,
+		Nodes:      master,
+		Action:     v1.ActionInstall,
+		Commands: []v1.Command{
+			{
+				Type:         v1.CommandShell,
+				ShellCommand: []string{"bash", "-c", `kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e "s/strictARP: false/strictARP: true/" | kubectl apply -f - -n kube-system`},
+			},
+		},
+	}
+	// render step
+	bytes, err := json.Marshal(n)
+	if err != nil {
+		return err
+	}
+	rs := v1.Step{
+		ID:         strutil.GetUUID(),
+		Name:       "renderMetalLBManifests",
+		Timeout:    metav1.Duration{Duration: 3 * time.Second},
+		ErrIgnore:  false,
+		RetryTimes: 1,
+		Nodes:      master,
+		Action:     v1.ActionInstall,
+		Commands: []v1.Command{
+			{
+				Type: v1.CommandTemplateRender,
+				Template: &v1.TemplateCommand{
+					Identity: fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, metallb),
+					Data:     bytes,
+				},
+			},
+		},
+	}
+
+	n.installSteps = append(n.installSteps, []v1.Step{
+		arpStep,
+		rs,
+		{
+			ID:         strutil.GetUUID(),
+			Name:       "deployMetalLB",
+			Timeout:    metav1.Duration{Duration: 10 * time.Second},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      master,
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:         v1.CommandShell,
+					ShellCommand: []string{"kubectl", "apply", "-f", filepath.Join(n.ManifestsDir, filename)},
+				},
+			},
+		},
+		{
+			ID:         strutil.GetUUID(),
+			Name:       "checkMetalLBHealth",
+			Timeout:    metav1.Duration{Duration: 1 * time.Minute},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      master,
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:          v1.CommandCustom,
+					Identity:      fmt.Sprintf(component.RegisterStepKeyFormat, name, version, metallb),
+					CustomCommand: bytes,
+				},
+			},
+		},
+		{
+			ID:         strutil.GetUUID(),
+			Name:       "deployIPAddressPool",
+			Timeout:    metav1.Duration{Duration: 15 * time.Second},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      master,
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:         v1.CommandShell,
+					ShellCommand: []string{"bash", "-c", "sleep 10s"},
+				},
+				{
+					Type:         v1.CommandShell,
+					ShellCommand: []string{"kubectl", "apply", "-f", filepath.Join(n.ManifestsDir, ipAddressPool)},
+				},
+			},
+		},
+		{
+			ID:         strutil.GetUUID(),
+			Name:       "deployAdvertisement",
+			Timeout:    metav1.Duration{Duration: 3 * time.Second},
+			ErrIgnore:  false,
+			RetryTimes: 1,
+			Nodes:      master,
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:         v1.CommandShell,
+					ShellCommand: []string{"kubectl", "apply", "-f", filepath.Join(n.ManifestsDir, advertisement)},
+				},
+			},
+		},
+	}...)
+
+	// uninstall
+	if metadata.OperationType != v1.OperationDeleteCluster {
+		n.uninstallSteps = []v1.Step{
+			rs,
+			{
+				ID:         strutil.GetUUID(),
+				Name:       "removeMetalLB",
+				Timeout:    metav1.Duration{Duration: 5 * time.Minute},
+				ErrIgnore:  true,
+				RetryTimes: 1,
+				Nodes:      master,
+				Action:     v1.ActionUninstall,
+				Commands: []v1.Command{
+					{
+						Type:         v1.CommandShell,
+						ShellCommand: []string{"kubectl", "delete", "-f", filepath.Join(n.ManifestsDir, filename)},
+					},
+				},
+			},
+			{
+				ID:         strutil.GetUUID(),
+				Name:       "disableKubeProxyStrictARP",
+				Timeout:    metav1.Duration{Duration: 3 * time.Second},
+				ErrIgnore:  true,
+				RetryTimes: 1,
+				Nodes:      master,
+				Action:     v1.ActionInstall,
+				Commands: []v1.Command{
+					{
+						Type:         v1.CommandShell,
+						ShellCommand: []string{"bash", "-c", `kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e "s/strictARP: true/strictARP: false/" | kubectl apply -f - -n kube-system`},
+					},
+				},
+			},
+		}
+	}
+
+	return nil
+}
+
+func (n *MetalLB) GetName() string {
+	return name
+}
+
+func (n *MetalLB) GetVersion() string {
+	return version
+}
+
+func (n *MetalLB) GetComponentMeta(lang component.Lang) component.Meta {
+	loc := component.GetLocalizer(lang)
+	mode := component.JSON(defaultMode)
+
+	propMap := map[string]component.JSONSchemaProps{
+		"mode": {
+			Title:        loc.MustLocalize(&i18n.LocalizeConfig{MessageID: "metallb.mode"}),
+			Properties:   nil,
+			Type:         component.JSONSchemaTypeString,
+			Default:      mode,
+			Description:  "load balancer mode",
+			Priority:     2,
+			Dependencies: []string{"enabled"},
+			EnumNames:    []string{"L2", "BGP"},
+			Enum:         []component.JSON{"L2", "BGP"},
+		},
+		"addresses": {
+			Title:        loc.MustLocalize(&i18n.LocalizeConfig{MessageID: "metallb.addresses"}),
+			Properties:   nil,
+			Type:         component.JSONSchemaTypeArray,
+			Default:      nil,
+			Description:  "ip address pool",
+			Priority:     3,
+			Dependencies: []string{"enabled"},
+		},
+		"imageRepoMirror": {
+			Title:        loc.MustLocalize(&i18n.LocalizeConfig{MessageID: "metallb.imageRepoMirror"}),
+			Properties:   nil,
+			Type:         component.JSONSchemaTypeString,
+			Default:      nil,
+			Description:  "metallb image repository mirror, the component official repository is used by default",
+			Priority:     4,
+			Dependencies: []string{"enabled"},
+		},
+	}
+
+	return component.Meta{
+		Title:      loc.MustLocalize(&i18n.LocalizeConfig{MessageID: "metallb.metaTitle"}),
+		Name:       name,
+		Version:    version,
+		Unique:     true,
+		Template:   true,
+		Deprecated: false,
+		Dependence: []string{component.InternalCategoryKubernetes},
+		Category:   component.InternalCategoryLB,
+		Priority:   1,
+		Schema: &component.JSONSchemaProps{
+			Properties: propMap,
+			Required:   []string{"mode", "addresses"},
+			Type:       component.JSONSchemaTypeObject,
+			Default:    nil,
+		},
+	}
+}
+
+func (n *MetalLB) NewInstance() component.ObjectMeta {
+	return &MetalLB{
+		ManifestsDir: manifestsDir,
+		Version:      defaultVersion,
+	}
+}
+
+func (n *MetalLB) GetDependence() []string {
+	return []string{component.InternalCategoryKubernetes}
+}
+
+func (n *MetalLB) GetInstallSteps() []v1.Step {
+	return n.installSteps
+}
+
+func (n *MetalLB) GetUninstallSteps() []v1.Step {
+	return n.uninstallSteps
+}
+
+func (n *MetalLB) GetUpgradeSteps() []v1.Step {
+	return n.upgradeSteps
+}
+
+func (n *MetalLB) Install(ctx context.Context, opts component.Options) ([]byte, error) {
+	if err := utils.RetryFunc(ctx, opts, 5*time.Second, "checkMetalLBPodStatus", n.checkMetalLBPodStatus); err != nil {
+		return nil, err
+	}
+	return nil, nil
+}
+
+func (n *MetalLB) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
+	return nil, nil
+}
+
+func (n *MetalLB) checkMetalLBPodStatus(ctx context.Context, opts component.Options) error {
+	ec, err := cmdutil.RunCmdWithContext(ctx, opts.DryRun, "bash", "-c", fmt.Sprintf("kubectl get po -n %s | grep %s", namespace, "controller"))
+	if err != nil {
+		return err
+	}
+
+	if ec.StdOut() == "" {
+		return fmt.Errorf("there are no running metallb pods: %s", strings.Join(ec.Args[1:], ","))
+	}
+	return nil
+}
+
+func (n *MetalLB) renderTo(w io.Writer) error {
+	at := tmplutil.New()
+	_, err := at.RenderTo(w, manifestTemplate, n)
+	return err
+}
+
+func (n *MetalLB) renderIPAddressPool(w io.Writer) error {
+	at := tmplutil.New()
+	_, err := at.RenderTo(w, ipAddressPoolTemplate, n)
+	return err
+}
+
+func (n *MetalLB) renderAdvertisement(w io.Writer) error {
+	at := tmplutil.New()
+	_, err := at.RenderTo(w, advertisementTemplate, n)
+	return err
+}
+
+func (n *MetalLB) Render(ctx context.Context, opts component.Options) error {
+	if err := os.MkdirAll(n.ManifestsDir, 0755); err != nil {
+		return err
+	}
+	ipAddressPoolFile := filepath.Join(n.ManifestsDir, ipAddressPool)
+	if err := fileutil.WriteFileWithContext(ctx, ipAddressPoolFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644,
+		n.renderIPAddressPool, opts.DryRun); err != nil {
+		return err
+	}
+	advertisementFile := filepath.Join(n.ManifestsDir, advertisement)
+	if err := fileutil.WriteFileWithContext(ctx, advertisementFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644,
+		n.renderAdvertisement, opts.DryRun); err != nil {
+		return err
+	}
+	manifestsFile := filepath.Join(n.ManifestsDir, filename)
+	return fileutil.WriteFileWithContext(ctx, manifestsFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644,
+		n.renderTo, opts.DryRun)
+}
+
+// GetImageRepoMirror return ImageRepoMirror
+func (n *MetalLB) GetImageRepoMirror() string {
+	return n.ImageRepoMirror
+}
diff --git a/pkg/component/metallb/template.go b/pkg/component/metallb/template.go
new file mode 100644
index 0000000..65c7719
--- /dev/null
+++ b/pkg/component/metallb/template.go
@@ -0,0 +1,2132 @@
+package metallb
+
+const ipAddressPoolTemplate = `
+apiVersion: metallb.io/v1beta1
+kind: IPAddressPool
+metadata:
+  name: first-pool
+  namespace: metallb-system
+spec:
+  addresses:
+  {{- range .Addresses }}
+  - {{ . }}
+  {{- end }}
+`
+
+const advertisementTemplate = `
+apiVersion: metallb.io/v1beta1
+kind: {{.Mode}}Advertisement
+metadata:
+  name: local
+  namespace: metallb-system
+`
+
+const manifestTemplate = `
+apiVersion: v1
+kind: Namespace
+metadata:
+  labels:
+    pod-security.kubernetes.io/audit: privileged
+    pod-security.kubernetes.io/enforce: privileged
+    pod-security.kubernetes.io/warn: privileged
+  name: metallb-system
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  name: addresspools.metallb.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
+        service:
+          name: webhook-service
+          namespace: metallb-system
+          path: /convert
+      conversionReviewVersions:
+      - v1alpha1
+      - v1beta1
+  group: metallb.io
+  names:
+    kind: AddressPool
+    listKind: AddressPoolList
+    plural: addresspools
+    singular: addresspool
+  scope: Namespaced
+  versions:
+  - deprecated: true
+    deprecationWarning: metallb.io v1alpha1 AddressPool is deprecated
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: AddressPool is the Schema for the addresspools 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: AddressPoolSpec defines the desired state of AddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              bgpAdvertisements:
+                description: When an IP is allocated from this pool, how should it
+                  be translated into BGP announcements?
+                items:
+                  properties:
+                    aggregationLength:
+                      default: 32
+                      description: The aggregation-length advertisement option lets
+                        you “roll up” the /32s into a larger prefix.
+                      format: int32
+                      minimum: 1
+                      type: integer
+                    aggregationLengthV6:
+                      default: 128
+                      description: Optional, defaults to 128 (i.e. no aggregation)
+                        if not specified.
+                      format: int32
+                      type: integer
+                    communities:
+                      description: BGP communities
+                      items:
+                        type: string
+                      type: array
+                    localPref:
+                      description: BGP LOCAL_PREF attribute which is used by BGP best
+                        path algorithm, Path with higher localpref is preferred over
+                        one with lower localpref.
+                      format: int32
+                      type: integer
+                  type: object
+                type: array
+              protocol:
+                description: Protocol can be used to select how the announcement is
+                  done.
+                enum:
+                - layer2
+                - bgp
+                type: string
+            required:
+            - addresses
+            - protocol
+            type: object
+          status:
+            description: AddressPoolStatus defines the observed state of AddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - deprecated: true
+    deprecationWarning: metallb.io v1beta1 AddressPool is deprecated, consider using
+      IPAddressPool
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: AddressPool represents a pool of IP addresses that can be allocated
+          to LoadBalancer services. AddressPool is deprecated and being replaced by
+          IPAddressPool.
+        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: AddressPoolSpec defines the desired state of AddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              bgpAdvertisements:
+                description: Drives how an IP allocated from this pool should translated
+                  into BGP announcements.
+                items:
+                  properties:
+                    aggregationLength:
+                      default: 32
+                      description: The aggregation-length advertisement option lets
+                        you “roll up” the /32s into a larger prefix.
+                      format: int32
+                      minimum: 1
+                      type: integer
+                    aggregationLengthV6:
+                      default: 128
+                      description: Optional, defaults to 128 (i.e. no aggregation)
+                        if not specified.
+                      format: int32
+                      type: integer
+                    communities:
+                      description: BGP communities to be associated with the given
+                        advertisement.
+                      items:
+                        type: string
+                      type: array
+                    localPref:
+                      description: BGP LOCAL_PREF attribute which is used by BGP best
+                        path algorithm, Path with higher localpref is preferred over
+                        one with lower localpref.
+                      format: int32
+                      type: integer
+                  type: object
+                type: array
+              protocol:
+                description: Protocol can be used to select how the announcement is
+                  done.
+                enum:
+                - layer2
+                - bgp
+                type: string
+            required:
+            - addresses
+            - protocol
+            type: object
+          status:
+            description: AddressPoolStatus defines the observed state of AddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: bfdprofiles.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: BFDProfile
+    listKind: BFDProfileList
+    plural: bfdprofiles
+    singular: bfdprofile
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.passiveMode
+      name: Passive Mode
+      type: boolean
+    - jsonPath: .spec.transmitInterval
+      name: Transmit Interval
+      type: integer
+    - jsonPath: .spec.receiveInterval
+      name: Receive Interval
+      type: integer
+    - jsonPath: .spec.detectMultiplier
+      name: Multiplier
+      type: integer
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BFDProfile represents the settings of the bfd session that can
+          be optionally associated with a BGP session.
+        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: BFDProfileSpec defines the desired state of BFDProfile.
+            properties:
+              detectMultiplier:
+                description: Configures the detection multiplier to determine packet
+                  loss. The remote transmission interval will be multiplied by this
+                  value to determine the connection loss detection timer.
+                format: int32
+                maximum: 255
+                minimum: 2
+                type: integer
+              echoInterval:
+                description: Configures the minimal echo receive transmission interval
+                  that this system is capable of handling in milliseconds. Defaults
+                  to 50ms
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+              echoMode:
+                description: Enables or disables the echo transmission mode. This
+                  mode is disabled by default, and not supported on multi hops setups.
+                type: boolean
+              minimumTtl:
+                description: 'For multi hop sessions only: configure the minimum expected
+                  TTL for an incoming BFD control packet.'
+                format: int32
+                maximum: 254
+                minimum: 1
+                type: integer
+              passiveMode:
+                description: 'Mark session as passive: a passive session will not
+                  attempt to start the connection and will wait for control packets
+                  from peer before it begins replying.'
+                type: boolean
+              receiveInterval:
+                description: The minimum interval that this system is capable of receiving
+                  control packets in milliseconds. Defaults to 300ms.
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+              transmitInterval:
+                description: The minimum transmission interval (less jitter) that
+                  this system wants to use to send BFD control packets in milliseconds.
+                  Defaults to 300ms
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+            type: object
+          status:
+            description: BFDProfileStatus defines the observed state of BFDProfile.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: bgpadvertisements.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: BGPAdvertisement
+    listKind: BGPAdvertisementList
+    plural: bgpadvertisements
+    singular: bgpadvertisement
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.ipAddressPools
+      name: IPAddressPools
+      type: string
+    - jsonPath: .spec.ipAddressPoolSelectors
+      name: IPAddressPool Selectors
+      type: string
+    - jsonPath: .spec.peers
+      name: Peers
+      type: string
+    - jsonPath: .spec.nodeSelectors
+      name: Node Selectors
+      priority: 10
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BGPAdvertisement allows to advertise the IPs coming from the
+          selected IPAddressPools via BGP, setting the parameters of the BGP Advertisement.
+        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: BGPAdvertisementSpec defines the desired state of BGPAdvertisement.
+            properties:
+              aggregationLength:
+                default: 32
+                description: The aggregation-length advertisement option lets you
+                  “roll up” the /32s into a larger prefix. Defaults to 32. Works for
+                  IPv4 addresses.
+                format: int32
+                minimum: 1
+                type: integer
+              aggregationLengthV6:
+                default: 128
+                description: The aggregation-length advertisement option lets you
+                  “roll up” the /128s into a larger prefix. Defaults to 128. Works
+                  for IPv6 addresses.
+                format: int32
+                type: integer
+              communities:
+                description: The BGP communities to be associated with the announcement.
+                  Each item can be a community of the form 1234:1234 or the name of
+                  an alias defined in the Community CRD.
+                items:
+                  type: string
+                type: array
+              ipAddressPoolSelectors:
+                description: A selector for the IPAddressPools which would get advertised
+                  via this advertisement. If no IPAddressPool is selected by this
+                  or by the list, the advertisement is applied to all the IPAddressPools.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              ipAddressPools:
+                description: The list of IPAddressPools to advertise via this advertisement,
+                  selected by name.
+                items:
+                  type: string
+                type: array
+              localPref:
+                description: The BGP LOCAL_PREF attribute which is used by BGP best
+                  path algorithm, Path with higher localpref is preferred over one
+                  with lower localpref.
+                format: int32
+                type: integer
+              nodeSelectors:
+                description: NodeSelectors allows to limit the nodes to announce as
+                  next hops for the LoadBalancer IP. When empty, all the nodes having  are
+                  announced as next hops.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              peers:
+                description: Peers limits the bgppeer to advertise the ips of the
+                  selected pools to. When empty, the loadbalancer IP is announced
+                  to all the BGPPeers configured.
+                items:
+                  type: string
+                type: array
+            type: object
+          status:
+            description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  name: bgppeers.metallb.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
+        service:
+          name: webhook-service
+          namespace: metallb-system
+          path: /convert
+      conversionReviewVersions:
+      - v1beta1
+      - v1beta2
+  group: metallb.io
+  names:
+    kind: BGPPeer
+    listKind: BGPPeerList
+    plural: bgppeers
+    singular: bgppeer
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.peerAddress
+      name: Address
+      type: string
+    - jsonPath: .spec.peerASN
+      name: ASN
+      type: string
+    - jsonPath: .spec.bfdProfile
+      name: BFD Profile
+      type: string
+    - jsonPath: .spec.ebgpMultiHop
+      name: Multi Hops
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BGPPeer is the Schema for the peers 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: BGPPeerSpec defines the desired state of Peer.
+            properties:
+              bfdProfile:
+                type: string
+              ebgpMultiHop:
+                description: EBGP peer is multi-hops away
+                type: boolean
+              holdTime:
+                description: Requested BGP hold time, per RFC4271.
+                type: string
+              keepaliveTime:
+                description: Requested BGP keepalive time, per RFC4271.
+                type: string
+              myASN:
+                description: AS number to use for the local end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              nodeSelectors:
+                description: Only connect to this peer on nodes that match one of
+                  these selectors.
+                items:
+                  properties:
+                    matchExpressions:
+                      items:
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                          values:
+                            items:
+                              type: string
+                            minItems: 1
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        - values
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      type: object
+                  type: object
+                type: array
+              password:
+                description: Authentication password for routers enforcing TCP MD5
+                  authenticated sessions
+                type: string
+              peerASN:
+                description: AS number to expect from the remote end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              peerAddress:
+                description: Address to dial when establishing the session.
+                type: string
+              peerPort:
+                description: Port to dial when establishing the session.
+                maximum: 16384
+                minimum: 0
+                type: integer
+              routerID:
+                description: BGP router ID to advertise to the peer
+                type: string
+              sourceAddress:
+                description: Source address to use when establishing the session.
+                type: string
+            required:
+            - myASN
+            - peerASN
+            - peerAddress
+            type: object
+          status:
+            description: BGPPeerStatus defines the observed state of Peer.
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.peerAddress
+      name: Address
+      type: string
+    - jsonPath: .spec.peerASN
+      name: ASN
+      type: string
+    - jsonPath: .spec.bfdProfile
+      name: BFD Profile
+      type: string
+    - jsonPath: .spec.ebgpMultiHop
+      name: Multi Hops
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BGPPeer is the Schema for the peers 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: BGPPeerSpec defines the desired state of Peer.
+            properties:
+              bfdProfile:
+                description: The name of the BFD Profile to be used for the BFD session
+                  associated to the BGP session. If not set, the BFD session won't
+                  be set up.
+                type: string
+              ebgpMultiHop:
+                description: To set if the BGPPeer is multi-hops away. Needed for
+                  FRR mode only.
+                type: boolean
+              holdTime:
+                description: Requested BGP hold time, per RFC4271.
+                type: string
+              keepaliveTime:
+                description: Requested BGP keepalive time, per RFC4271.
+                type: string
+              myASN:
+                description: AS number to use for the local end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              nodeSelectors:
+                description: Only connect to this peer on nodes that match one of
+                  these selectors.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              password:
+                description: Authentication password for routers enforcing TCP MD5
+                  authenticated sessions
+                type: string
+              passwordSecret:
+                description: passwordSecret is name of the authentication secret for
+                  BGP Peer. the secret must be of type "kubernetes.io/basic-auth",
+                  and created in the same namespace as the MetalLB deployment. The
+                  password is stored in the secret as the key "password".
+                properties:
+                  name:
+                    description: name is unique within a namespace to reference a
+                      secret resource.
+                    type: string
+                  namespace:
+                    description: namespace defines the space within which the secret
+                      name must be unique.
+                    type: string
+                type: object
+              peerASN:
+                description: AS number to expect from the remote end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              peerAddress:
+                description: Address to dial when establishing the session.
+                type: string
+              peerPort:
+                default: 179
+                description: Port to dial when establishing the session.
+                maximum: 16384
+                minimum: 0
+                type: integer
+              routerID:
+                description: BGP router ID to advertise to the peer
+                type: string
+              sourceAddress:
+                description: Source address to use when establishing the session.
+                type: string
+            required:
+            - myASN
+            - peerASN
+            - peerAddress
+            type: object
+          status:
+            description: BGPPeerStatus defines the observed state of Peer.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: communities.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: Community
+    listKind: CommunityList
+    plural: communities
+    singular: community
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Community is a collection of aliases for communities. Users can
+          define named aliases to be used in the BGPPeer CRD.
+        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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: The name of the alias for the community.
+                      type: string
+                    value:
+                      description: The BGP community value corresponding to the given
+                        name.
+                      type: string
+                  type: object
+                type: array
+            type: object
+          status:
+            description: CommunityStatus defines the observed state of Community.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: ipaddresspools.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: IPAddressPool
+    listKind: IPAddressPoolList
+    plural: ipaddresspools
+    singular: ipaddresspool
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.autoAssign
+      name: Auto Assign
+      type: boolean
+    - jsonPath: .spec.avoidBuggyIPs
+      name: Avoid Buggy IPs
+      type: boolean
+    - jsonPath: .spec.addresses
+      name: Addresses
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: IPAddressPool represents a pool of IP addresses that can be allocated
+          to LoadBalancer services.
+        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: IPAddressPoolSpec defines the desired state of IPAddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              avoidBuggyIPs:
+                default: false
+                description: AvoidBuggyIPs prevents addresses ending with .0 and .255
+                  to be used by a pool.
+                type: boolean
+            required:
+            - addresses
+            type: object
+          status:
+            description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: l2advertisements.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: L2Advertisement
+    listKind: L2AdvertisementList
+    plural: l2advertisements
+    singular: l2advertisement
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.ipAddressPools
+      name: IPAddressPools
+      type: string
+    - jsonPath: .spec.ipAddressPoolSelectors
+      name: IPAddressPool Selectors
+      type: string
+    - jsonPath: .spec.interfaces
+      name: Interfaces
+      type: string
+    - jsonPath: .spec.nodeSelectors
+      name: Node Selectors
+      priority: 10
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: L2Advertisement allows to advertise the LoadBalancer IPs provided
+          by the selected pools via L2.
+        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: L2AdvertisementSpec defines the desired state of L2Advertisement.
+            properties:
+              interfaces:
+                description: A list of interfaces to announce from. The LB IP will
+                  be announced only from these interfaces. If the field is not set,
+                  we advertise from all the interfaces on the host.
+                items:
+                  type: string
+                type: array
+              ipAddressPoolSelectors:
+                description: A selector for the IPAddressPools which would get advertised
+                  via this advertisement. If no IPAddressPool is selected by this
+                  or by the list, the advertisement is applied to all the IPAddressPools.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              ipAddressPools:
+                description: The list of IPAddressPools to advertise via this advertisement,
+                  selected by name.
+                items:
+                  type: string
+                type: array
+              nodeSelectors:
+                description: NodeSelectors allows to limit the nodes to announce as
+                  next hops for the LoadBalancer IP. When empty, all the nodes having  are
+                  announced as next hops.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+            type: object
+          status:
+            description: L2AdvertisementStatus defines the observed state of L2Advertisement.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: metallb
+  name: speaker
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - ""
+  resourceNames:
+  - memberlist
+  resources:
+  - secrets
+  verbs:
+  - list
+- apiGroups:
+  - apps
+  resourceNames:
+  - controller
+  resources:
+  - deployments
+  verbs:
+  - get
+- apiGroups:
+  - metallb.io
+  resources:
+  - bgppeers
+  verbs:
+  - get
+  - list
+- apiGroups:
+  - metallb.io
+  resources:
+  - addresspools
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - bfdprofiles
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - bgpadvertisements
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - l2advertisements
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: metallb
+  name: pod-lister
+  namespace: metallb-system
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - list
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - addresspools
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - bfdprofiles
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - bgppeers
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - l2advertisements
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - bgpadvertisements
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:controller
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - services/status
+  verbs:
+  - update
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - policy
+  resourceNames:
+  - controller
+  resources:
+  - podsecuritypolicies
+  verbs:
+  - use
+- apiGroups:
+  - admissionregistration.k8s.io
+  resources:
+  - validatingwebhookconfigurations
+  - mutatingwebhookconfigurations
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - apiextensions.k8s.io
+  resources:
+  - customresourcedefinitions
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:speaker
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  - endpoints
+  - nodes
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - discovery.k8s.io
+  resources:
+  - endpointslices
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - policy
+  resourceNames:
+  - speaker
+  resources:
+  - podsecuritypolicies
+  verbs:
+  - use
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: controller
+subjects:
+- kind: ServiceAccount
+  name: controller
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: pod-lister
+  namespace: metallb-system
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: pod-lister
+subjects:
+- kind: ServiceAccount
+  name: speaker
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: metallb-system:controller
+subjects:
+- kind: ServiceAccount
+  name: controller
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:speaker
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: metallb-system:speaker
+subjects:
+- kind: ServiceAccount
+  name: speaker
+  namespace: metallb-system
+---
+{{- if eq .Mode "BGP"}}
+apiVersion: v1
+data:
+  daemons: |
+    # This file tells the frr package which daemons to start.
+    #
+    # Sample configurations for these daemons can be found in
+    # /usr/share/doc/frr/examples/.
+    #
+    # ATTENTION:
+    #
+    # When activating a daemon for the first time, a config file, even if it is
+    # empty, has to be present *and* be owned by the user and group "frr", else
+    # the daemon will not be started by /etc/init.d/frr. The permissions should
+    # be u=rw,g=r,o=.
+    # When using "vtysh" such a config file is also needed. It should be owned by
+    # group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
+    #
+    # The watchfrr and zebra daemons are always started.
+    #
+    bgpd=yes
+    ospfd=no
+    ospf6d=no
+    ripd=no
+    ripngd=no
+    isisd=no
+    pimd=no
+    ldpd=no
+    nhrpd=no
+    eigrpd=no
+    babeld=no
+    sharpd=no
+    pbrd=no
+    bfdd=yes
+    fabricd=no
+    vrrpd=no
+
+    #
+    # If this option is set the /etc/init.d/frr script automatically loads
+    # the config via "vtysh -b" when the servers are started.
+    # Check /etc/pam.d/frr if you intend to use "vtysh"!
+    #
+    vtysh_enable=yes
+    zebra_options="  -A 127.0.0.1 -s 90000000"
+    bgpd_options="   -A 127.0.0.1 -p 0"
+    ospfd_options="  -A 127.0.0.1"
+    ospf6d_options=" -A ::1"
+    ripd_options="   -A 127.0.0.1"
+    ripngd_options=" -A ::1"
+    isisd_options="  -A 127.0.0.1"
+    pimd_options="   -A 127.0.0.1"
+    ldpd_options="   -A 127.0.0.1"
+    nhrpd_options="  -A 127.0.0.1"
+    eigrpd_options=" -A 127.0.0.1"
+    babeld_options=" -A 127.0.0.1"
+    sharpd_options=" -A 127.0.0.1"
+    pbrd_options="   -A 127.0.0.1"
+    staticd_options="-A 127.0.0.1"
+    bfdd_options="   -A 127.0.0.1"
+    fabricd_options="-A 127.0.0.1"
+    vrrpd_options="  -A 127.0.0.1"
+
+    # configuration profile
+    #
+    #frr_profile="traditional"
+    #frr_profile="datacenter"
+
+    #
+    # This is the maximum number of FD's that will be available.
+    # Upon startup this is read by the control files and ulimit
+    # is called. Uncomment and use a reasonable value for your
+    # setup if you are expecting a large number of peers in
+    # say BGP.
+    #MAX_FDS=1024
+
+    # The list of daemons to watch is automatically generated by the init script.
+    #watchfrr_options=""
+
+    # for debugging purposes, you can specify a "wrap" command to start instead
+    # of starting the daemon directly, e.g. to use valgrind on ospfd:
+    #   ospfd_wrap="/usr/bin/valgrind"
+    # or you can use "all_wrap" for all daemons, e.g. to use perf record:
+    #   all_wrap="/usr/bin/perf record --call-graph -"
+    # the normal daemon command is added to this at the end.
+  frr.conf: |
+    ! This file gets overriden the first time the speaker renders a config.
+    ! So anything configured here is only temporary.
+    frr version 7.5.1
+    frr defaults traditional
+    hostname Router
+    line vty
+    log file /etc/frr/frr.log informational
+  vtysh.conf: |
+    service integrated-vtysh-config
+kind: ConfigMap
+metadata:
+  name: frr-startup
+  namespace: metallb-system
+---
+{{- end }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: webhook-server-cert
+  namespace: metallb-system
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: webhook-service
+  namespace: metallb-system
+spec:
+  ports:
+  - port: 443
+    targetPort: 9443
+  selector:
+    component: controller
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: metallb
+    component: controller
+  name: controller
+  namespace: metallb-system
+spec:
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app: metallb
+      component: controller
+  template:
+    metadata:
+      annotations:
+        prometheus.io/port: "7472"
+        prometheus.io/scrape: "true"
+      labels:
+        app: metallb
+        component: controller
+    spec:
+      containers:
+      - args:
+        - --port=7472
+        - --log-level=info
+        env:
+        {{- if eq .Mode "BGP"}}
+        - name: METALLB_BGP_TYPE
+          value: frr
+        {{- end }}
+        - name: METALLB_ML_SECRET_NAME
+          value: memberlist
+        - name: METALLB_DEPLOYMENT
+          value: controller
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/metallb-controller:{{.Version}}
+        livenessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        name: controller
+        ports:
+        - containerPort: 7472
+          name: monitoring
+        - containerPort: 9443
+          name: webhook-server
+          protocol: TCP
+        readinessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+            - all
+          readOnlyRootFilesystem: true
+        volumeMounts:
+        - mountPath: /tmp/k8s-webhook-server/serving-certs
+          name: cert
+          readOnly: true
+      nodeSelector:
+        kubernetes.io/os: linux
+      securityContext:
+        fsGroup: 65534
+        runAsNonRoot: true
+        runAsUser: 65534
+      serviceAccountName: controller
+      terminationGracePeriodSeconds: 0
+      volumes:
+      - name: cert
+        secret:
+          defaultMode: 420
+          secretName: webhook-server-cert
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  labels:
+    app: metallb
+    component: speaker
+  name: speaker
+  namespace: metallb-system
+spec:
+  selector:
+    matchLabels:
+      app: metallb
+      component: speaker
+  template:
+    metadata:
+      annotations:
+        prometheus.io/port: "7472"
+        prometheus.io/scrape: "true"
+      labels:
+        app: metallb
+        component: speaker
+    spec:
+      containers:
+      {{- if eq .Mode "BGP"}}
+      - command:
+        - /bin/sh
+        - -c
+        - |
+          /sbin/tini -- /usr/lib/frr/docker-start &
+          attempts=0
+          until [[ -f /etc/frr/frr.log || $attempts -eq 60 ]]; do
+            sleep 1
+            attempts=$(( $attempts + 1 ))
+          done
+          tail -f /etc/frr/frr.log
+        env:
+        - name: TINI_SUBREAPER
+          value: "true"
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/frr:v7.5.1
+        name: frr
+        securityContext:
+          capabilities:
+            add:
+            - NET_ADMIN
+            - NET_RAW
+            - SYS_ADMIN
+            - NET_BIND_SERVICE
+        volumeMounts:
+        - mountPath: /var/run/frr
+          name: frr-sockets
+        - mountPath: /etc/frr
+          name: frr-conf
+      - command:
+        - /etc/frr_reloader/frr-reloader.sh
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/frr:v7.5.1
+        name: reloader
+        volumeMounts:
+        - mountPath: /var/run/frr
+          name: frr-sockets
+        - mountPath: /etc/frr
+          name: frr-conf
+        - mountPath: /etc/frr_reloader
+          name: reloader
+      - args:
+        - --metrics-port=7473
+        command:
+        - /etc/frr_metrics/frr-metrics
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/frr:v7.5.1
+        name: frr-metrics
+        ports:
+        - containerPort: 7473
+          name: monitoring
+        volumeMounts:
+        - mountPath: /var/run/frr
+          name: frr-sockets
+        - mountPath: /etc/frr
+          name: frr-conf
+        - mountPath: /etc/frr_metrics
+          name: metrics
+      {{- end }}
+      - args:
+        - --port=7472
+        - --log-level=info
+        env:
+        {{- if eq .Mode "BGP"}}
+        - name: FRR_CONFIG_FILE
+          value: /etc/frr_reloader/frr.conf
+        - name: FRR_RELOADER_PID_FILE
+          value: /etc/frr_reloader/reloader.pid
+        - name: METALLB_BGP_TYPE
+          value: frr
+        {{- end }}
+        - name: METALLB_NODE_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+        - name: METALLB_HOST
+          valueFrom:
+            fieldRef:
+              fieldPath: status.hostIP
+        - name: METALLB_ML_BIND_ADDR
+          valueFrom:
+            fieldRef:
+              fieldPath: status.podIP
+        - name: METALLB_ML_LABELS
+          value: app=metallb,component=speaker
+        - name: METALLB_ML_SECRET_KEY
+          valueFrom:
+            secretKeyRef:
+              key: secretkey
+              name: memberlist
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/metallb-speaker:{{.Version}}
+        livenessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        name: speaker
+        ports:
+        - containerPort: 7472
+          name: monitoring
+        - containerPort: 7946
+          name: memberlist-tcp
+        - containerPort: 7946
+          name: memberlist-udp
+          protocol: UDP
+        readinessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            add:
+            - NET_RAW
+            drop:
+            - ALL
+          readOnlyRootFilesystem: true
+        {{- if eq .Mode "BGP"}}
+        volumeMounts:
+        - mountPath: /etc/frr_reloader
+          name: reloader
+        {{- end }}
+      hostNetwork: true
+      {{- if eq .Mode "BGP"}}
+      initContainers:
+      - command:
+        - /bin/sh
+        - -c
+        - cp -rLf /tmp/frr/* /etc/frr/
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/frr:v7.5.1
+        name: cp-frr-files
+        securityContext:
+          runAsGroup: 101
+          runAsUser: 100
+        volumeMounts:
+        - mountPath: /tmp/frr
+          name: frr-startup
+        - mountPath: /etc/frr
+          name: frr-conf
+      - command:
+        - /bin/sh
+        - -c
+        - cp -f /frr-reloader.sh /etc/frr_reloader/
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/metallb-speaker:{{.Version}}
+        name: cp-reloader
+        volumeMounts:
+        - mountPath: /etc/frr_reloader
+          name: reloader
+      - command:
+        - /bin/sh
+        - -c
+        - cp -f /frr-metrics /etc/frr_metrics/
+        image: {{with .ImageRepoMirror}}{{.}}/{{end}}caas4/metallb-speaker:{{.Version}}
+        name: cp-metrics
+        volumeMounts:
+        - mountPath: /etc/frr_metrics
+          name: metrics
+      {{- end }}
+      nodeSelector:
+        kubernetes.io/os: linux
+      serviceAccountName: speaker
+      {{- if eq .Mode "BGP"}}
+      shareProcessNamespace: true
+      {{- end }}
+      terminationGracePeriodSeconds: 2
+      tolerations:
+      - effect: NoSchedule
+        key: node-role.kubernetes.io/master
+        operator: Exists
+      - effect: NoSchedule
+        key: node-role.kubernetes.io/control-plane
+        operator: Exists
+      {{- if eq .Mode "BGP"}}
+      volumes:
+      - emptyDir: {}
+        name: frr-sockets
+      - configMap:
+          name: frr-startup
+        name: frr-startup
+      - emptyDir: {}
+        name: frr-conf
+      - emptyDir: {}
+        name: reloader
+      - emptyDir: {}
+        name: metrics
+      {{- end }}
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  creationTimestamp: null
+  name: metallb-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta2-bgppeer
+  failurePolicy: Fail
+  name: bgppeersvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta2
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - bgppeers
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-addresspool
+  failurePolicy: Fail
+  name: addresspoolvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - addresspools
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-bfdprofile
+  failurePolicy: Fail
+  name: bfdprofilevalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - DELETE
+    resources:
+    - bfdprofiles
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-bgpadvertisement
+  failurePolicy: Fail
+  name: bgpadvertisementvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - bgpadvertisements
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-community
+  failurePolicy: Fail
+  name: communityvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - communities
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-ipaddresspool
+  failurePolicy: Fail
+  name: ipaddresspoolvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - ipaddresspools
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: metallb-system
+      path: /validate-metallb-io-v1beta1-l2advertisement
+  failurePolicy: Fail
+  name: l2advertisementvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - l2advertisements
+  sideEffects: None
+`
diff --git a/pkg/component/validation/validation.go b/pkg/component/validation/validation.go
index 7a11917..43edd8f 100644
--- a/pkg/component/validation/validation.go
+++ b/pkg/component/validation/validation.go
@@ -28,6 +28,7 @@ var (
 	ErrInvalidSCName        = errors.New("invalid name of storage class")
 	ErrInvalidNamespace     = errors.New("invalid namespace")
 	ErrInvalidReclaimPolicy = errors.New("invalid reclaim policy")
+	ErrInvalidLBMode        = errors.New("invalid load balancer mode")
 )
 
 const (
@@ -70,3 +71,10 @@ func MatchKubernetesReclaimPolicy(policy string) error {
 	}
 	return ErrInvalidReclaimPolicy
 }
+
+func MatchLoadBalancerMode(mode string) error {
+	if mode == "L2" || mode == "BGP" {
+		return nil
+	}
+	return ErrInvalidLBMode
+}


Test patch:
diff --git a/pkg/component/metallb/metallb_test.go b/pkg/component/metallb/metallb_test.go
new file mode 100644
index 0000000..9981361
--- /dev/null
+++ b/pkg/component/metallb/metallb_test.go
@@ -0,0 +1,75 @@
+package metallb
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var lb = &MetalLB{
+	ImageRepoMirror: "192.168.10.10:5000",
+	ManifestsDir:    "/tmp/.metallb",
+	Mode:            "BGP",
+	Addresses:       []string{"192.168.20.20-192.168.20.30"},
+	Version:         "v0.13.7",
+}
+
+func TestRenderTo(t *testing.T) {
+	sb := &strings.Builder{}
+	if err := lb.renderTo(sb); err != nil {
+		assert.FailNow(t, "deploy template render failed, err: %v", err)
+	}
+	// BGP mode should be deployed FRR
+	if !strings.Contains(sb.String(), "frr-startup") {
+		t.Error("BGP mode should be deployed FRR")
+	}
+
+	nlb := *lb
+	nlb.Mode = "L2"
+	sb1 := &strings.Builder{}
+	if err := nlb.renderTo(sb1); err != nil {
+		assert.FailNow(t, "deploy template render failed, err: %v", err)
+	}
+	// L2 mode should not be deployed FRR
+	if strings.Contains(sb1.String(), "frr-startup") {
+		t.Error("L2 mode should not be deployed FRR")
+	}
+}
+
+func TestRenderIPAddressPool(t *testing.T) {
+	expected := `
+apiVersion: metallb.io/v1beta1
+kind: IPAddressPool
+metadata:
+  name: first-pool
+  namespace: metallb-system
+spec:
+  addresses:
+  - 192.168.20.20-192.168.20.30
+`
+	sb := &strings.Builder{}
+	if err := lb.renderIPAddressPool(sb); err != nil {
+		assert.FailNow(t, "ip address pool template render failed, err: %v", err)
+	}
+	if !assert.Equal(t, expected, sb.String()) {
+		t.Errorf("expected is not the same as actual")
+	}
+}
+
+func TestRenderAdvertisement(t *testing.T) {
+	expected := `
+apiVersion: metallb.io/v1beta1
+kind: BGPAdvertisement
+metadata:
+  name: local
+  namespace: metallb-system
+`
+	sb := &strings.Builder{}
+	if err := lb.renderAdvertisement(sb); err != nil {
+		assert.FailNow(t, "advertisement template render failed, err: %v", err)
+	}
+	if !assert.Equal(t, expected, sb.String()) {
+		t.Errorf("expected is not the same as actual")
+	}
+}