Repository: metallb/metallb
Pull request #1253 (2022-03-09 09:40:07)

Add Community CRD
In BGP, "community" is an optional attribute that can be passed to the
other BGP peers as an attached tag, that govern a certain behavior.
for more info, you can read [here](https://en.wikipedia.org/wiki/Border_Gateway_Protocol#Communities)

Currently, the MetalLB configmap implementation allows us
to create aliases for communities.
As part of the move to CRD based implementation, this commit
introduces the community CRD to reach feature parity.

The new community CRD can hold a collection of community definitions.
Example of the community YAML:
```
apiVersion: metallb.io/v1beta1
kind: Community
metadata:
  name: metallb-communities
  namespace: metallb-system
spec:
  communities:
  - name: NO_EXPORT
    value: 65535:65281
  - name: NO_ADVERTISE
    value: 65535:65282
```

sabinaaledort: /lgtm
fedepaol: @liornoy can you add an e2e tests that validates the CRD is working properly?
liornoy: > @liornoy can you add an e2e tests that validates the CRD is working properly?

Yes, sure. I think I prefer waiting for the CRD part 2 pr to merge and then rebase rather than doing it now and making you rebase. Plus your pr includes a new infra for the e2e with the new CRDs that I need.
msherif1234: Hmm we have discussed this in the past and the decision was the avoid CRD overhead for this config and just use pass by value similar to what we have in the released code ? why we changing this now ?

fedepaol: > Hmm we have discussed this in the past and the decision was the avoid CRD overhead for this config and just use pass by value similar to what we have in the released code ? why we changing this now ?

This is to have feature parity with what metallb offers today. We don't want to change the way we configure metallb and reduce the things a user can do.
msherif1234: > > Hmm we have discussed this in the past and the decision was the avoid CRD overhead for this config and just use pass by value similar to what we have in the released code ? why we changing this now ?
> 
> This is to have feature parity with what metallb offers today. We don't want to change the way we configure metallb and reduce the things a user can do.

again that was what we discussed with Russel in the past and that was decision at the time he wasn't favor of spinning CR for this config just FYI 
fedepaol: > > > Hmm we have discussed this in the past and the decision was the avoid CRD overhead for this config and just use pass by value similar to what we have in the released code ? why we changing this now ?
> > 
> > 
> > This is to have feature parity with what metallb offers today. We don't want to change the way we configure metallb and reduce the things a user can do.
> 
> again that was what we discussed with Russel in the past and that was decision at the time he wasn't favor of spinning CR for this config just FYI

I understand, but that was when the CRDs were an alternative to the configmap. Now we are replacing the configmap, and I think we must offer the same degrees of configurability the users have today.

Also, from a user's perspective this is not adding any overhead, since they can still use the communities in the number format as we currently support in the operator, or add the aliases.

fedepaol: @liornoy this needs to be rebased
fedepaol: Just one last thing to change, looks great!
fedepaol: LGTM

Patch:
diff --git a/api/v1beta1/community_types.go b/api/v1beta1/community_types.go
new file mode 100644
index 00000000..7e403ab6
--- /dev/null
+++ b/api/v1beta1/community_types.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022.
+
+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 v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type CommunityAlias struct {
+	// Name or identifier for the community.
+	Name string `json:"name,omitempty"`
+	// BGP community value.
+	Value string `json:"value,omitempty"`
+}
+
+// CommunitySpec defines the desired state of Community.
+type CommunitySpec struct {
+	Communities []CommunityAlias `json:"communities,omitempty"`
+}
+
+// CommunityStatus defines the observed state of Community.
+type CommunityStatus struct {
+	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+	// Important: Run "make" to regenerate code after modifying this file
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+
+// Community is the Schema for the communities API.
+type Community struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   CommunitySpec   `json:"spec,omitempty"`
+	Status CommunityStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// CommunityList contains a list of Community.
+type CommunityList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []Community `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&Community{}, &CommunityList{})
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index cb870293..c3a31ed7 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -724,3 +724,92 @@ func (in *NodeSelector) DeepCopy() *NodeSelector {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Community) DeepCopyInto(out *Community) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Community.
+func (in *Community) DeepCopy() *Community {
+	if in == nil {
+		return nil
+	}
+	out := new(Community)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Community) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CommunityList) DeepCopyInto(out *CommunityList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Community, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunityList.
+func (in *CommunityList) DeepCopy() *CommunityList {
+	if in == nil {
+		return nil
+	}
+	out := new(CommunityList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CommunityList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CommunitySpec) DeepCopyInto(out *CommunitySpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunitySpec.
+func (in *CommunitySpec) DeepCopy() *CommunitySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(CommunitySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CommunityStatus) DeepCopyInto(out *CommunityStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunityStatus.
+func (in *CommunityStatus) DeepCopy() *CommunityStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(CommunityStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/charts/metallb/templates/crds.yaml b/charts/metallb/templates/crds.yaml
index d50a162c..3328ddaa 100644
--- a/charts/metallb/templates/crds.yaml
+++ b/charts/metallb/templates/crds.yaml
@@ -898,4 +898,67 @@ status:
     kind: ""
     plural: ""
   conditions: []
-  storedVersions: []
\ No newline at end of file
+  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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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: []
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
index 1040a524..b8488ea3 100644
--- a/charts/metallb/templates/rbac.yaml
+++ b/charts/metallb/templates/rbac.yaml
@@ -75,6 +75,9 @@ rules:
 - 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: Role
@@ -110,6 +113,9 @@ rules:
 - apiGroups: ["metallb.io"]
   resources: ["l2advertisements"]
   verbs: ["get", "list"]
+- apiGroups: ["metallb.io"]
+  resources: ["communities"]
+  verbs: ["get", "list"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/config/crd/bases/metallb.io_communities.yaml b/config/crd/bases/metallb.io_communities.yaml
new file mode 100644
index 00000000..3090f81c
--- /dev/null
+++ b/config/crd/bases/metallb.io_communities.yaml
@@ -0,0 +1,64 @@
+
+---
+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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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: []
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index d754a885..1b8dc74b 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -7,3 +7,4 @@ resources:
   - bases/metallb.io_bfdprofiles.yaml
   - bases/metallb.io_bgpadvertisements.yaml
   - bases/metallb.io_l2advertisements.yaml
+  - bases/metallb.io_communities.yaml
diff --git a/config/manifests/metallb-frr-with-webhooks.yaml b/config/manifests/metallb-frr-with-webhooks.yaml
index d8cdec65..27012b81 100644
--- a/config/manifests/metallb-frr-with-webhooks.yaml
+++ b/config/manifests/metallb-frr-with-webhooks.yaml
@@ -713,6 +713,69 @@ status:
 ---
 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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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
@@ -1061,6 +1124,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -1132,6 +1203,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 67d1c638..fbc5e375 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -689,6 +689,69 @@ status:
 ---
 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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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
@@ -1037,6 +1100,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -1108,6 +1179,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
diff --git a/config/manifests/metallb-native-with-webhooks.yaml b/config/manifests/metallb-native-with-webhooks.yaml
index 6a1dd7fb..2226f54b 100644
--- a/config/manifests/metallb-native-with-webhooks.yaml
+++ b/config/manifests/metallb-native-with-webhooks.yaml
@@ -713,6 +713,69 @@ status:
 ---
 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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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
@@ -1061,6 +1124,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -1132,6 +1203,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 291f3f12..2c961c9c 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -689,6 +689,69 @@ status:
 ---
 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 the Schema for the communities 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: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: Name or identifier for the community.
+                      type: string
+                    value:
+                      description: BGP community value.
+                      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
@@ -1037,6 +1100,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -1108,6 +1179,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - communities
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index c813f66a..057e6e5d 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -144,7 +144,14 @@ rules:
       - get
       - list
       - watch
-
+  - apiGroups:
+      - metallb.io
+    resources:
+      - communities
+    verbs:
+      - get
+      - list
+      - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -217,6 +224,14 @@ rules:
       - get
       - list
       - watch
+  - apiGroups:
+      - metallb.io
+    resources:
+      - communities
+    verbs:
+      - get
+      - list
+      - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/internal/config/config.go b/internal/config/config.go
index 7a90f535..3cdc4fc7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -40,6 +40,7 @@ type ClusterResources struct {
 	BGPAdvs            []metallbv1beta1.BGPAdvertisement `json:"bgpadvertisements"`
 	L2Advs             []metallbv1beta1.L2Advertisement  `json:"l2advertisements"`
 	LegacyAddressPools []metallbv1beta1.AddressPool      `json:"legacyaddresspools"`
+	Communities        []metallbv1beta1.Community        `json:"communities"`
 	PasswordSecrets    map[string]corev1.Secret          `json:"passwordsecrets"`
 	Nodes              []corev1.Node                     `json:"nodes"`
 }
@@ -207,21 +208,14 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 		cfg.Peers = append(cfg.Peers, peer)
 	}
 
-	communities := map[string]uint32{}
-	// TODO CRDs add a CRD for communities
-	/*
-		for n, v := range rawConfig.BGPCommunities {
-			c, err := ParseCommunity(v)
-			if err != nil {
-				return nil, fmt.Errorf("parsing community %q: %s", n, err)
-			}
-			communities[n] = c
-		}
-	*/
+	communities, err := communitiesFromCrs(resources.Communities)
+	if err != nil {
+		return nil, err
+	}
 
 	var allCIDRs []*net.IPNet
 	for _, p := range resources.Pools {
-		pool, err := addressPoolFromCR(p, communities)
+		pool, err := addressPoolFromCR(p)
 		if err != nil {
 			return nil, fmt.Errorf("parsing address pool %s: %s", p.Name, err)
 		}
@@ -306,7 +300,7 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 		}
 		pool, err := addressPoolFromLegacyCR(p, communities, allNodes)
 		if err != nil {
-			return nil, fmt.Errorf("parsing address pool %s: %s", p.Name, err)
+			return nil, errors.Wrapf(err, "parsing address pool %s", p.Name)
 		}
 
 		// Check that the pool isn't already defined
@@ -330,6 +324,23 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 	return cfg, nil
 }
 
+func communitiesFromCrs(cs []metallbv1beta1.Community) (map[string]uint32, error) {
+	communities := map[string]uint32{}
+	for _, c := range cs {
+		for _, communityAlias := range c.Spec.Communities {
+			v, err := ParseCommunity(communityAlias.Value)
+			if err != nil {
+				return nil, fmt.Errorf("parsing community %q: %s", communityAlias.Name, err)
+			}
+			if _, ok := communities[communityAlias.Name]; ok {
+				return nil, fmt.Errorf("duplicate definition of community %q", communityAlias.Name)
+			}
+			communities[communityAlias.Name] = v
+		}
+	}
+	return communities, nil
+}
+
 func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secret) (*Peer, error) {
 	if p.Spec.MyASN == 0 {
 		return nil, errors.New("missing local ASN")
@@ -437,7 +448,7 @@ func passwordForPeer(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1
 	return password, nil
 }
 
-func addressPoolFromCR(p metallbv1beta1.IPAddressPool, bgpCommunities map[string]uint32) (*Pool, error) {
+func addressPoolFromCR(p metallbv1beta1.IPAddressPool) (*Pool, error) {
 	if p.Name == "" {
 		return nil, errors.New("missing pool name")
 	}
@@ -600,15 +611,11 @@ func bgpAdvertisementFromCR(crdAd metallbv1beta1.BGPAdvertisement, communities m
 	}
 
 	for _, c := range crdAd.Spec.Communities {
-		if v, ok := communities[c]; ok {
-			ad.Communities[v] = true
-		} else {
-			v, err := ParseCommunity(c)
-			if err != nil {
-				return nil, fmt.Errorf("invalid community %q in BGP advertisement: %s", c, err)
-			}
-			ad.Communities[v] = true
+		v, err := getCommunityValue(c, communities)
+		if err != nil {
+			return nil, errors.Wrapf(err, "invalid community %q in BGP advertisement", c)
 		}
+		ad.Communities[v] = true
 	}
 
 	selected, err := selectedNodes(nodes, crdAd.Spec.NodeSelectors)
@@ -681,23 +688,37 @@ func bgpAdvertisementsFromLegacyCR(ads []metallbv1beta1.LegacyBgpAdvertisement,
 		ad.LocalPref = crdAd.LocalPref
 
 		for _, c := range crdAd.Communities {
-			if v, ok := communities[c]; ok {
-				ad.Communities[v] = true
-			} else {
-				v, err := ParseCommunity(c)
-				if err != nil {
-					return nil, fmt.Errorf("invalid community %q in BGP advertisement: %s", c, err)
-				}
-				ad.Communities[v] = true
+			v, err := getCommunityValue(c, communities)
+			if err != nil {
+				return nil, errors.Wrapf(err, "invalid community %q in BGP advertisement", c)
 			}
+			ad.Communities[v] = true
 		}
-
 		ret = append(ret, ad)
 	}
 
 	return ret, nil
 }
 
+func getCommunityValue(community string, communities map[string]uint32) (uint32, error) {
+	if v, ok := communities[community]; ok {
+		return v, nil
+	}
+
+	v, err := ParseCommunity(community)
+	if errors.Is(err, invalidCommunityValue) {
+		return 0, err
+	}
+
+	// Return TransientError on invalidCommunityFormat, in case it refers
+	// a Community resource that doesn't exist yet.
+	if errors.Is(err, invalidCommunityFormat) {
+		return 0, TransientError{err.Error()}
+	}
+
+	return v, nil
+}
+
 func validateHoldTime(ht time.Duration) error {
 	rounded := time.Duration(int(ht.Seconds())) * time.Second
 	if rounded != 0 && rounded < 3*time.Second {
@@ -727,18 +748,21 @@ func validateBGPAdvPerPool(adv *BGPAdvertisement, pool *Pool) error {
 	return nil
 }
 
+var invalidCommunityValue = errors.New("invalid community value")
+var invalidCommunityFormat = errors.New("invalid community format")
+
 func ParseCommunity(c string) (uint32, error) {
 	fs := strings.Split(c, ":")
 	if len(fs) != 2 {
-		return 0, fmt.Errorf("invalid community string %q", c)
+		return 0, fmt.Errorf("%w: %s", invalidCommunityFormat, c)
 	}
 	a, err := strconv.ParseUint(fs[0], 10, 16)
 	if err != nil {
-		return 0, fmt.Errorf("invalid first section of community %q: %s", fs[0], err)
+		return 0, fmt.Errorf("%w: invalid first section of community %q: %s", invalidCommunityValue, fs[0], err)
 	}
 	b, err := strconv.ParseUint(fs[1], 10, 16)
 	if err != nil {
-		return 0, fmt.Errorf("invalid second section of community %q: %s", fs[1], err)
+		return 0, fmt.Errorf("%w: invalid second section of community %q: %s", invalidCommunityValue, fs[1], err)
 	}
 
 	return (uint32(a) << 16) + uint32(b), nil
diff --git a/internal/k8s/controllers/config_controller.go b/internal/k8s/controllers/config_controller.go
index a12f268e..202a8635 100644
--- a/internal/k8s/controllers/config_controller.go
+++ b/internal/k8s/controllers/config_controller.go
@@ -84,6 +84,12 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, err
 	}
 
+	var communities metallbv1beta1.CommunityList
+	if err := r.List(ctx, &communities, client.InNamespace(r.Namespace)); err != nil {
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get communities", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	secrets, err := r.getSecrets(ctx)
 	if err != nil {
 		return ctrl.Result{}, err
@@ -102,6 +108,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		L2Advs:             l2Advertisements.Items,
 		BGPAdvs:            bgpAdvertisements.Items,
 		LegacyAddressPools: addressPools.Items,
+		Communities:        communities.Items,
 		PasswordSecrets:    secrets,
 		Nodes:              nodes.Items,
 	}
@@ -142,6 +149,7 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Watches(&source.Kind{Type: &metallbv1beta1.L2Advertisement{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.BFDProfile{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.AddressPool{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &metallbv1beta1.Community{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}).
 		Complete(r)
 }
diff --git a/internal/k8s/controllers/dump.go b/internal/k8s/controllers/dump.go
index 630c22e3..15843ca9 100644
--- a/internal/k8s/controllers/dump.go
+++ b/internal/k8s/controllers/dump.go
@@ -18,6 +18,7 @@ func dumpClusterResources(c *config.ClusterResources) string {
 		L2Advs:             c.L2Advs,
 		BGPAdvs:            c.BGPAdvs,
 		LegacyAddressPools: c.LegacyAddressPools,
+		Communities:        c.Communities,
 	}
 	withNoSecret.PasswordSecrets = make(map[string]corev1.Secret)
 	for k, s := range c.PasswordSecrets {
diff --git a/internal/k8s/controllers/pool_controller.go b/internal/k8s/controllers/pool_controller.go
index b043d63d..52029469 100644
--- a/internal/k8s/controllers/pool_controller.go
+++ b/internal/k8s/controllers/pool_controller.go
@@ -57,9 +57,16 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 		return ctrl.Result{}, err
 	}
 
+	var communities metallbv1beta1.CommunityList
+	if err := r.List(ctx, &communities, client.InNamespace(r.Namespace)); err != nil {
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "message", "failed to get communities", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	resources := config.ClusterResources{
 		Pools:              ipAddressPools.Items,
 		LegacyAddressPools: addressPools.Items,
+		Communities:        communities.Items,
 	}
 
 	level.Debug(r.Logger).Log("controller", "PoolReconciler", "metallb CRs", spew.Sdump(resources))
@@ -93,5 +100,6 @@ func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&metallbv1beta1.IPAddressPool{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.AddressPool{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &metallbv1beta1.Community{}}, &handler.EnqueueRequestForObject{}).
 		Complete(r)
 }
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 1025db8d..27c8cd74 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -17,7 +17,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 
-	metallbv1alpha1 "go.universe.tf/metallb/api/v1alpha1"
 	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 
@@ -57,7 +56,6 @@ var (
 func init() {
 	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
 
-	utilruntime.Must(metallbv1alpha1.AddToScheme(scheme))
 	utilruntime.Must(metallbv1beta1.AddToScheme(scheme))
 	utilruntime.Must(metallbv1beta2.AddToScheme(scheme))
 
@@ -117,7 +115,6 @@ func New(cfg *Config) (*Client, error) {
 		LeaderElection: false,
 		NewCache: cache.BuilderWithOptions(cache.Options{
 			SelectorsByObject: map[client.Object]cache.ObjectSelector{
-				&metallbv1alpha1.AddressPool{}:     namespaceSelector,
 				&metallbv1beta1.AddressPool{}:      namespaceSelector,
 				&metallbv1beta1.BFDProfile{}:       namespaceSelector,
 				&metallbv1beta1.BGPAdvertisement{}: namespaceSelector,
@@ -125,6 +122,7 @@ func New(cfg *Config) (*Client, error) {
 				&metallbv1beta1.IPAddressPool{}:    namespaceSelector,
 				&metallbv1beta1.L2Advertisement{}:  namespaceSelector,
 				&metallbv1beta2.BGPPeer{}:          namespaceSelector,
+				&metallbv1beta1.Community{}:        namespaceSelector,
 				&corev1.Secret{}:                   namespaceSelector,
 			},
 		}),


Test patch:
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index 43bc224a..37b123b8 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -56,7 +56,6 @@ const (
 	v6PoolAddresses       = "fc00:f853:0ccd:e799::/124"
 	CommunityNoAdv        = "65535:65282" // 0xFFFFFF02: NO_ADVERTISE
 	CommunityGracefulShut = "65535:0"     // GRACEFUL_SHUTDOWN
-	IPLocalPref           = uint32(300)
 	SpeakerContainerName  = "speaker"
 )
 
@@ -70,6 +69,17 @@ var _ = ginkgo.Describe("BGP", func() {
 			Name: "empty",
 		},
 	}
+	noAdvCommunity := metallbv1beta1.Community{
+		ObjectMeta: metav1.ObjectMeta{Name: "community1"},
+		Spec: metallbv1beta1.CommunitySpec{
+			Communities: []metallbv1beta1.CommunityAlias{
+				{
+					Name:  "NO_ADVERTISE",
+					Value: CommunityNoAdv,
+				},
+			},
+		},
+	}
 
 	ginkgo.AfterEach(func() {
 		if ginkgo.CurrentGinkgoTestDescription().Failed {
@@ -957,7 +967,8 @@ var _ = ginkgo.Describe("BGP", func() {
 			table.Entry("IPV6", ipfamily.IPv6))
 
 		table.DescribeTable("configure bgp advertisement and verify it gets propagated",
-			func(rangeWithAdvertisement string, rangeWithoutAdvertisement string, advertisement metallbv1beta1.BGPAdvertisement, legacy bool, ipFamily ipfamily.Family) {
+			func(rangeWithAdvertisement string, rangeWithoutAdvertisement string, advertisement metallbv1beta1.BGPAdvertisement, legacy bool,
+				ipFamily ipfamily.Family, communities []metallbv1beta1.Community) {
 				emptyAdvertisement := metallbv1beta1.BGPAdvertisement{
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "empty",
@@ -985,7 +996,8 @@ var _ = ginkgo.Describe("BGP", func() {
 				}
 
 				resources := metallbconfig.ClusterResources{
-					Peers: metallb.PeersForContainers(FRRContainers, ipFamily),
+					Peers:       metallb.PeersForContainers(FRRContainers, ipFamily),
+					Communities: communities,
 				}
 
 				if !legacy {
@@ -1046,7 +1058,12 @@ var _ = ginkgo.Describe("BGP", func() {
 					validateService(cs, svcNoAdvertisement, allNodes.Items, c)
 					Eventually(func() error {
 						for _, community := range advertisement.Spec.Communities {
-							routes, err := frr.RoutesForCommunity(c, community, ipFamily)
+							// Get community value for test cases with Community CRD.
+							communityValue, err := communityForAlias(community, communities)
+							if err != nil {
+								communityValue = community
+							}
+							routes, err := frr.RoutesForCommunity(c, communityValue, ipFamily)
 							if err != nil {
 								return err
 							}
@@ -1102,7 +1119,8 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv4),
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{}),
 			table.Entry("IPV4 - localpref",
 				"192.168.10.0/24",
 				"192.168.16.0/24",
@@ -1114,7 +1132,8 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv4),
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{}),
 			table.Entry("IPV4 - community",
 				"192.168.10.0/24",
 				"192.168.16.0/24",
@@ -1126,7 +1145,22 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv4),
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{}),
+			table.Entry("IPV4 - community from CRD",
+				"192.168.10.0/24",
+				"192.168.16.0/24",
+				metallbv1beta1.BGPAdvertisement{
+					ObjectMeta: metav1.ObjectMeta{Name: "advertisement"},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						Communities:    []string{"NO_ADVERTISE"},
+						LocalPref:      50,
+						IPAddressPools: []string{"bgp-with-advertisement"},
+					},
+				},
+				false,
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{noAdvCommunity}),
 			table.Entry("IPV4 - community and localpref - legacy",
 				"192.168.10.0/24",
 				"192.168.16.0/24",
@@ -1139,7 +1173,21 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				true,
-				ipfamily.IPv4),
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{}),
+			table.Entry("IPV4 - community from CRD - legacy",
+				"192.168.10.0/24",
+				"192.168.16.0/24",
+				metallbv1beta1.BGPAdvertisement{
+					ObjectMeta: metav1.ObjectMeta{Name: "advertisement"},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						Communities:    []string{"NO_ADVERTISE"},
+						IPAddressPools: []string{"bgp-with-advertisement"},
+					},
+				},
+				true,
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{noAdvCommunity}),
 			table.Entry("IPV4 - localpref - legacy",
 				"192.168.10.0/24",
 				"192.168.16.0/24",
@@ -1151,7 +1199,8 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				true,
-				ipfamily.IPv4),
+				ipfamily.IPv4,
+				[]metallbv1beta1.Community{}),
 			table.Entry("IPV6 - community and localpref",
 				"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 				"fc00:f853:0ccd:e799::19-fc00:f853:0ccd:e799::26",
@@ -1164,7 +1213,8 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv6),
+				ipfamily.IPv6,
+				[]metallbv1beta1.Community{}),
 			table.Entry("IPV6 - community",
 				"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 				"fc00:f853:0ccd:e799::19-fc00:f853:0ccd:e799::26",
@@ -1176,7 +1226,34 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv6),
+				ipfamily.IPv6,
+				[]metallbv1beta1.Community{}),
+			table.Entry("IPV6 - community from CRD",
+				"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
+				"fc00:f853:0ccd:e799::19-fc00:f853:0ccd:e799::26",
+				metallbv1beta1.BGPAdvertisement{
+					ObjectMeta: metav1.ObjectMeta{Name: "advertisement"},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						Communities:    []string{"NO_ADVERTISE"},
+						IPAddressPools: []string{"bgp-with-advertisement"},
+					},
+				},
+				false,
+				ipfamily.IPv6,
+				[]metallbv1beta1.Community{noAdvCommunity}),
+			table.Entry("IPV6 - community from CRD - legacy",
+				"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
+				"fc00:f853:0ccd:e799::19-fc00:f853:0ccd:e799::26",
+				metallbv1beta1.BGPAdvertisement{
+					ObjectMeta: metav1.ObjectMeta{Name: "advertisement"},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						Communities:    []string{"NO_ADVERTISE"},
+						IPAddressPools: []string{"bgp-with-advertisement"},
+					},
+				},
+				true,
+				ipfamily.IPv6,
+				[]metallbv1beta1.Community{noAdvCommunity}),
 			table.Entry("IPV6 - localpref",
 				"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 				"fc00:f853:0ccd:e799::19-fc00:f853:0ccd:e799::26",
@@ -1188,7 +1265,8 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 				false,
-				ipfamily.IPv6))
+				ipfamily.IPv6,
+				[]metallbv1beta1.Community{}))
 
 	})
 
@@ -1397,3 +1475,16 @@ func substringCount(substr string) interface{} {
 		return strings.Count(action, substr)
 	}
 }
+
+// communityForAlias checks if the given community name exists in the community crs,
+// and if so, returns the value of the matching community.
+func communityForAlias(communityName string, cs []metallbv1beta1.Community) (string, error) {
+	for _, c := range cs {
+		for _, communityAlias := range c.Spec.Communities {
+			if communityName == communityAlias.Name {
+				return communityAlias.Value, nil
+			}
+		}
+	}
+	return "", fmt.Errorf("community name %s not found in Community CRs", communityName)
+}
diff --git a/e2etest/pkg/config/update.go b/e2etest/pkg/config/update.go
index 5cc556d8..9fde5eca 100644
--- a/e2etest/pkg/config/update.go
+++ b/e2etest/pkg/config/update.go
@@ -105,6 +105,12 @@ func (o beta1Updater) Update(r config.ClusterResources) error {
 		key = key + 1
 	}
 
+	for _, community := range r.Communities {
+		objects[key] = community.DeepCopy()
+		oldValues[key] = community.DeepCopy()
+		key = key + 1
+	}
+
 	// Iterating over the map will return the items in a random order.
 	for i, obj := range objects {
 		obj.SetNamespace(o.namespace)
@@ -130,6 +136,9 @@ func (o beta1Updater) Update(r config.ClusterResources) error {
 			case *metallbv1beta1.L2Advertisement:
 				old := oldValues[i].(*metallbv1beta1.L2Advertisement)
 				toChange.Spec = *old.Spec.DeepCopy()
+			case *metallbv1beta1.Community:
+				old := oldValues[i].(*metallbv1beta1.Community)
+				toChange.Spec = *old.Spec.DeepCopy()
 			}
 
 			return nil
@@ -166,6 +175,10 @@ func (o beta1Updater) Clean() error {
 	if err != nil {
 		return err
 	}
+	err = o.cli.DeleteAllOf(context.Background(), &metallbv1beta1.Community{}, client.InNamespace(o.namespace))
+	if err != nil {
+		return err
+	}
 	return nil
 }
 
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 53fc7071..618b629a 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -34,6 +34,16 @@ func ipnet(s string) *net.IPNet {
 }
 
 func TestParse(t *testing.T) {
+	testAdvName := "testAdv"
+	testPoolName := "testPool"
+	testPool := v1beta1.IPAddressPool{
+		ObjectMeta: v1.ObjectMeta{Name: testPoolName},
+		Spec: v1beta1.IPAddressPoolSpec{
+			Addresses: []string{
+				"10.20.0.0/16",
+			},
+		},
+	}
 	tests := []struct {
 		desc string
 		crs  ClusterResources
@@ -49,9 +59,6 @@ func TestParse(t *testing.T) {
 		},
 
 		{
-			// TODO CRD Add communities
-			//			bgp-communities:
-			//  bar: 64512:1234
 			desc: "config using all features",
 			crs: ClusterResources{
 				Peers: []v1beta2.BGPPeer{
@@ -152,7 +159,7 @@ func TestParse(t *testing.T) {
 						Spec: v1beta1.BGPAdvertisementSpec{
 							AggregationLength: pointer.Int32Ptr(32),
 							LocalPref:         uint32(100),
-							Communities:       []string{ /* TODO CRD Add communities"bar", */ "1234:2345"},
+							Communities:       []string{"bar"},
 							IPAddressPools:    []string{"pool1"},
 							Peers:             []string{"peer1"},
 						},
@@ -191,6 +198,21 @@ func TestParse(t *testing.T) {
 						},
 					},
 				},
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "64512:1234",
+								},
+							},
+						},
+					},
+				},
 			},
 			want: &Config{
 				Peers: []*Peer{
@@ -229,8 +251,7 @@ func TestParse(t *testing.T) {
 								AggregationLengthV6: 128,
 								LocalPref:           100,
 								Communities: map[uint32]bool{
-									//0xfc0004d2: true,
-									0x04D20929: true,
+									0xfc0004d2: true,
 								},
 								Nodes: map[string]bool{},
 								Peers: []string{"peer1"},
@@ -813,41 +834,191 @@ func TestParse(t *testing.T) {
 			},
 		},
 		{
-			desc: "bad community literal (wrong format)",
+			desc: "bad community literal (wrong format) - in BGP adv",
 			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{testPool},
 				BGPAdvs: []v1beta1.BGPAdvertisement{
 					{
+						ObjectMeta: v1.ObjectMeta{Name: testAdvName},
 						Spec: v1beta1.BGPAdvertisementSpec{
-							Communities: []string{
-								"1234",
-							},
+							Communities:    []string{"1234"},
+							IPAddressPools: []string{testPoolName},
 						},
 					},
 				},
 			},
 		},
 		{
-			desc: "bad community literal (asn part doesn't fit)",
+			desc: "bad community literal (asn part doesn't fit) - in BGP adv",
 			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{testPool},
 				BGPAdvs: []v1beta1.BGPAdvertisement{
 					{
+						ObjectMeta: v1.ObjectMeta{Name: testAdvName},
 						Spec: v1beta1.BGPAdvertisementSpec{
-							Communities: []string{
-								"99999999:1",
-							},
+							Communities:    []string{"99999999:1"},
+							IPAddressPools: []string{testPoolName},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "bad community literal (community# part doesn't fit) - in BGP adv",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{testPool},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{Name: testAdvName},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							Communities:    []string{"1:99999999"},
+							IPAddressPools: []string{testPoolName},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "bad community ref (unknown ref) - in BGP adv",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{testPool},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{Name: testAdvName},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							Communities:    []string{"community"},
+							IPAddressPools: []string{testPoolName},
 						},
 					},
 				},
 			},
 		},
 		{
-			desc: "bad community literal (community# part doesn't fit)",
+			desc: "duplicate community literal - in BGP adv",
 			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{testPool},
 				BGPAdvs: []v1beta1.BGPAdvertisement{
 					{
+						ObjectMeta: v1.ObjectMeta{Name: testAdvName},
 						Spec: v1beta1.BGPAdvertisementSpec{
-							Communities: []string{
-								"1:99999999",
+							Communities:    []string{"1234:5678", "1234:5678"},
+							IPAddressPools: []string{testPoolName},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "bad community literal (wrong format) - in the community CR",
+			crs: ClusterResources{
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "1234",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "bad community literal (asn part doesn't fit) - in the community CR",
+			crs: ClusterResources{
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "99999999:1",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "bad community literal (community# part doesn't fit) - in the community CR",
+			crs: ClusterResources{
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "1:99999999",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "duplicate communities definition (in 2 different crs)",
+			crs: ClusterResources{
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community1",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "1234:5678",
+								},
+							},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community2",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "1234:5678",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "duplicate communities definition (in the same cr)",
+			crs: ClusterResources{
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "1234:5678",
+								},
+								{
+									Name:  "bar",
+									Value: "1234:5678",
+								},
 							},
 						},
 					},
@@ -1411,6 +1582,69 @@ func TestParse(t *testing.T) {
 			},
 		},
 
+		{
+			desc: "config legacy pool with bgp communities crd",
+			crs: ClusterResources{
+				LegacyAddressPools: []v1beta1.AddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "legacybgppool1",
+						},
+						Spec: v1beta1.AddressPoolSpec{
+							Addresses: []string{
+								"10.40.0.0/16",
+								"10.60.0.0/24",
+							},
+							Protocol:      string(BGP),
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+							BGPAdvertisements: []v1beta1.LegacyBgpAdvertisement{
+								{
+									AggregationLength: pointer.Int32Ptr(32),
+									LocalPref:         uint32(100),
+									Communities:       []string{"bar"},
+								},
+							},
+						},
+					},
+				},
+				Communities: []v1beta1.Community{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "community",
+						},
+						Spec: v1beta1.CommunitySpec{
+							Communities: []v1beta1.CommunityAlias{
+								{
+									Name:  "bar",
+									Value: "64512:1234",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: map[string]*Pool{
+					"legacybgppool1": {
+						CIDR:          []*net.IPNet{ipnet("10.40.0.0/16"), ipnet("10.60.0.0/24")},
+						AvoidBuggyIPs: true,
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								LocalPref:           100,
+								Communities: map[uint32]bool{
+									0xfc0004d2: true,
+								},
+								Nodes: map[string]bool{},
+							},
+						},
+					},
+				},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
 		{
 			desc: "config mixing legacy pools with IP pools with overlapping ips",
 			crs: ClusterResources{
@@ -1791,31 +2025,34 @@ func TestParse(t *testing.T) {
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
-		/* TODO Add communities CRD
 		{
-			desc: "Duplicate communities definition",
+			desc: "config legacy pool with invalid community",
 			crs: ClusterResources{
-				Pools: []v1beta1.IPAddressPool{
+				LegacyAddressPools: []v1beta1.AddressPool{
 					{
-						ObjectMeta: v1.ObjectMeta{Name: "pool1"},
-						Spec: v1beta1.IPAddressPoolSpec{
-							Protocol: "bgp",
+						ObjectMeta: v1.ObjectMeta{
+							Name: "legacybgppool1",
+						},
+						Spec: v1beta1.AddressPoolSpec{
 							Addresses: []string{
-								"10.20.0.0/16",
+								"10.40.0.0/16",
+								"10.60.0.0/24",
 							},
-							BGPAdvertisements: []v1beta1.BgpAdvertisement{
-								{
-									AggregationLength: pointer.Int32Ptr(26),
-								},
+							Protocol:      string(BGP),
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+							BGPAdvertisements: []v1beta1.LegacyBgpAdvertisement{
 								{
-									AggregationLength: pointer.Int32Ptr(26),
+									AggregationLength: pointer.Int32Ptr(32),
+									LocalPref:         uint32(100),
+									Communities:       []string{"1234"},
 								},
 							},
 						},
 					},
 				},
 			},
-		},*/
+		},
 	}
 
 	for _, test := range tests {