diff --git a/PROJECT b/PROJECT index 4a77a99c..e0687e7d 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: openstack.org layout: - go.kubebuilder.io/v3 @@ -116,4 +120,13 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openstack.org + group: network + kind: BGPConfiguration + path: github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1 + version: v1beta1 version: "3" diff --git a/apis/bases/network.openstack.org_bgpconfigurations.yaml b/apis/bases/network.openstack.org_bgpconfigurations.yaml new file mode 100644 index 00000000..7461c9ec --- /dev/null +++ b/apis/bases/network.openstack.org_bgpconfigurations.yaml @@ -0,0 +1,157 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: bgpconfigurations.network.openstack.org +spec: + group: network.openstack.org + names: + kind: BGPConfiguration + listKind: BGPConfigurationList + plural: bgpconfigurations + singular: bgpconfiguration + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: BGPConfiguration is the Schema for the bgpconfigurations 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: BGPConfigurationSpec defines the desired state of BGPConfiguration + properties: + frrConfigurationNamespace: + default: metallb-system + description: FRRConfigurationNamespace - namespace where to create + the FRRConfiguration. Defaults to metallb-system. + type: string + frrNodeConfigurationSelector: + description: |- + FRRNodeConfigurationSelector - per default the FRRConfiguration per node within the FRRConfigurationNamespace + gets queried using the FRRConfiguration.spec.NodeSelector `kubernetes.io/hostname: worker-0`. In case a more + specific + items: + description: FRRNodeConfigurationSelectorType - + properties: + frrConfigurationNamespace: + description: NodeName - name of the node object as seen by + running the `oc get nodes` command + type: string + nodeSelector: + description: NodeSelector to identify the correct FRRConfiguration + from spec.nodeSelector + 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 + x-kubernetes-map-type: atomic + type: object + type: array + type: object + status: + description: BGPConfigurationStatus defines the observed state of BGPConfiguration + properties: + conditions: + description: Conditions + items: + description: Condition defines an observation of a API resource + operational state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. + type: string + severity: + description: |- + Severity provides a classification of Reason code, so the current situation is immediately + understandable and could act accordingly. + It is meant for situations where Status=False and it should be indicated if it is just + informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue + and no actions to automatically resolve the issue can/should be done). + For conditions where Status=Unknown or Status=True the Severity should be SeverityNone. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apis/go.mod b/apis/go.mod index d52171a7..e64ec3b7 100644 --- a/apis/go.mod +++ b/apis/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.20.1 github.com/onsi/gomega v1.34.1 - github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc + github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 k8s.io/api v0.29.10 k8s.io/apimachinery v0.29.10 @@ -19,14 +19,14 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/swag v0.22.9 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -47,14 +47,14 @@ require ( github.com/openshift/api v3.9.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.51.1 // indirect + github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect @@ -69,7 +69,7 @@ require ( k8s.io/apiextensions-apiserver v0.29.10 // indirect k8s.io/component-base v0.29.10 // indirect k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/apis/go.sum b/apis/go.sum index dcd73ec3..9bf313fa 100644 --- a/apis/go.sum +++ b/apis/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= -github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= +github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= @@ -18,12 +18,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= -github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -76,28 +76,28 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/openshift/api v0.0.0-20240830023148-b7d0481c9094 h1:J1wuGhVxpsHykZBa6Beb1gQ96Ptej9AE/BvwCBiRj1E= github.com/openshift/api v0.0.0-20240830023148-b7d0481c9094/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc h1:Ufa/q/nC9wmKblvsc0kJppsXHOJoY4fbUamb3ItWCOk= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f h1:uCdmEcic/axWXzAHXsrrP/mkaQVJJnXgQB+6corsLm0= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= +github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -124,8 +124,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -192,8 +192,8 @@ k8s.io/component-base v0.29.10 h1:YQrQ/bpzGPGqIPEPaBzxjH0/1DJOI+yZPZNbbz7ZCBY= k8s.io/component-base v0.29.10/go.mod h1:IbwsBob2DnYiAONsSHIuYenchqcDycbHSLHrXshuLgM= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 h1:qVoMaQV5t62UUvHe16Q3eb2c5HPzLHYzsi0Tu/xLndo= +k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.17.6 h1:12IXsozEsIXWAMRpgRlYS1jjAHQXHtWEOMdULh3DbEw= diff --git a/apis/network/v1beta1/bgpconfiguration_types.go b/apis/network/v1beta1/bgpconfiguration_types.go new file mode 100644 index 00000000..ecbdaf92 --- /dev/null +++ b/apis/network/v1beta1/bgpconfiguration_types.go @@ -0,0 +1,78 @@ +/* +Copyright 2023. + +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 ( + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FRRNodeConfigurationSelectorType - +type FRRNodeConfigurationSelectorType struct { + // +kubebuilder:validation:Optional + // NodeName - name of the node object as seen by running the `oc get nodes` command + NodeName string `json:"frrConfigurationNamespace,omitempty"` + + // +kubebuilder:validation:Optional + // NodeSelector to identify the correct FRRConfiguration from spec.nodeSelector + NodeSelector metav1.LabelSelector `json:"nodeSelector,omitempty"` +} + +// BGPConfigurationSpec defines the desired state of BGPConfiguration +type BGPConfigurationSpec struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default="metallb-system" + // FRRConfigurationNamespace - namespace where to create the FRRConfiguration. Defaults to metallb-system. + FRRConfigurationNamespace string `json:"frrConfigurationNamespace"` + + // +kubebuilder:validation:Optional + // FRRNodeConfigurationSelector - per default the FRRConfiguration per node within the FRRConfigurationNamespace + // gets queried using the FRRConfiguration.spec.NodeSelector `kubernetes.io/hostname: worker-0`. In case a more + // specific + FRRNodeConfigurationSelector []FRRNodeConfigurationSelectorType `json:"frrNodeConfigurationSelector,omitempty"` +} + +// BGPConfigurationStatus defines the observed state of BGPConfiguration +type BGPConfigurationStatus struct { + // Conditions + Conditions condition.Conditions `json:"conditions,omitempty" optional:"true"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// BGPConfiguration is the Schema for the bgpconfigurations API +type BGPConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BGPConfigurationSpec `json:"spec,omitempty"` + Status BGPConfigurationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// BGPConfigurationList contains a list of BGPConfiguration +type BGPConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BGPConfiguration `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BGPConfiguration{}, &BGPConfigurationList{}) +} diff --git a/apis/network/v1beta1/zz_generated.deepcopy.go b/apis/network/v1beta1/zz_generated.deepcopy.go index bd68d0f7..41cb3fa8 100644 --- a/apis/network/v1beta1/zz_generated.deepcopy.go +++ b/apis/network/v1beta1/zz_generated.deepcopy.go @@ -41,6 +41,109 @@ func (in *AllocationRange) DeepCopy() *AllocationRange { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BGPConfiguration) DeepCopyInto(out *BGPConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BGPConfiguration. +func (in *BGPConfiguration) DeepCopy() *BGPConfiguration { + if in == nil { + return nil + } + out := new(BGPConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BGPConfiguration) 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 *BGPConfigurationList) DeepCopyInto(out *BGPConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BGPConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BGPConfigurationList. +func (in *BGPConfigurationList) DeepCopy() *BGPConfigurationList { + if in == nil { + return nil + } + out := new(BGPConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BGPConfigurationList) 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 *BGPConfigurationSpec) DeepCopyInto(out *BGPConfigurationSpec) { + *out = *in + if in.FRRNodeConfigurationSelector != nil { + in, out := &in.FRRNodeConfigurationSelector, &out.FRRNodeConfigurationSelector + *out = make([]FRRNodeConfigurationSelectorType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BGPConfigurationSpec. +func (in *BGPConfigurationSpec) DeepCopy() *BGPConfigurationSpec { + if in == nil { + return nil + } + out := new(BGPConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BGPConfigurationStatus) DeepCopyInto(out *BGPConfigurationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(condition.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BGPConfigurationStatus. +func (in *BGPConfigurationStatus) DeepCopy() *BGPConfigurationStatus { + if in == nil { + return nil + } + out := new(BGPConfigurationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSData) DeepCopyInto(out *DNSData) { *out = *in @@ -372,6 +475,22 @@ func (in *DNSMasqStatus) DeepCopy() *DNSMasqStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FRRNodeConfigurationSelectorType) DeepCopyInto(out *FRRNodeConfigurationSelectorType) { + *out = *in + in.NodeSelector.DeepCopyInto(&out.NodeSelector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FRRNodeConfigurationSelectorType. +func (in *FRRNodeConfigurationSelectorType) DeepCopy() *FRRNodeConfigurationSelectorType { + if in == nil { + return nil + } + out := new(FRRNodeConfigurationSelectorType) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPAddress) DeepCopyInto(out *IPAddress) { *out = *in diff --git a/config/crd/bases/network.openstack.org_bgpconfigurations.yaml b/config/crd/bases/network.openstack.org_bgpconfigurations.yaml new file mode 100644 index 00000000..7461c9ec --- /dev/null +++ b/config/crd/bases/network.openstack.org_bgpconfigurations.yaml @@ -0,0 +1,157 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: bgpconfigurations.network.openstack.org +spec: + group: network.openstack.org + names: + kind: BGPConfiguration + listKind: BGPConfigurationList + plural: bgpconfigurations + singular: bgpconfiguration + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: BGPConfiguration is the Schema for the bgpconfigurations 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: BGPConfigurationSpec defines the desired state of BGPConfiguration + properties: + frrConfigurationNamespace: + default: metallb-system + description: FRRConfigurationNamespace - namespace where to create + the FRRConfiguration. Defaults to metallb-system. + type: string + frrNodeConfigurationSelector: + description: |- + FRRNodeConfigurationSelector - per default the FRRConfiguration per node within the FRRConfigurationNamespace + gets queried using the FRRConfiguration.spec.NodeSelector `kubernetes.io/hostname: worker-0`. In case a more + specific + items: + description: FRRNodeConfigurationSelectorType - + properties: + frrConfigurationNamespace: + description: NodeName - name of the node object as seen by + running the `oc get nodes` command + type: string + nodeSelector: + description: NodeSelector to identify the correct FRRConfiguration + from spec.nodeSelector + 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 + x-kubernetes-map-type: atomic + type: object + type: array + type: object + status: + description: BGPConfigurationStatus defines the observed state of BGPConfiguration + properties: + conditions: + description: Conditions + items: + description: Condition defines an observation of a API resource + operational state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. + type: string + severity: + description: |- + Severity provides a classification of Reason code, so the current situation is immediately + understandable and could act accordingly. + It is meant for situations where Status=False and it should be indicated if it is just + informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue + and no actions to automatically resolve the issue can/should be done). + For conditions where Status=Unknown or Status=True the Severity should be SeverityNone. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 7a56abf6..5719edfd 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,6 +11,7 @@ resources: - bases/network.openstack.org_netconfigs.yaml - bases/network.openstack.org_ipsets.yaml - bases/network.openstack.org_reservations.yaml +- bases/network.openstack.org_bgpconfigurations.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -24,6 +25,7 @@ patchesStrategicMerge: #- patches/webhook_in_netconfigs.yaml #- patches/webhook_in_reservations.yaml #- patches/webhook_in_ipsets.yaml +#- patches/webhook_in_bgpconfigurations.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -36,6 +38,7 @@ patchesStrategicMerge: #- patches/cainjection_in_netconfigs.yaml #- patches/cainjection_in_reservations.yaml #- patches/cainjection_in_ipsets.yaml +#- patches/cainjection_in_bgpconfigurations.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_network_bgpconfigurations.yaml b/config/crd/patches/cainjection_in_network_bgpconfigurations.yaml new file mode 100644 index 00000000..c10bd3ba --- /dev/null +++ b/config/crd/patches/cainjection_in_network_bgpconfigurations.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: bgpconfigurations.network.openstack.org diff --git a/config/crd/patches/webhook_in_network_bgpconfigurations.yaml b/config/crd/patches/webhook_in_network_bgpconfigurations.yaml new file mode 100644 index 00000000..ce55faa0 --- /dev/null +++ b/config/crd/patches/webhook_in_network_bgpconfigurations.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bgpconfigurations.network.openstack.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/network_bgpconfiguration_editor_role.yaml b/config/rbac/network_bgpconfiguration_editor_role.yaml new file mode 100644 index 00000000..aebb897a --- /dev/null +++ b/config/rbac/network_bgpconfiguration_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit bgpconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: bgpconfiguration-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: infra-operator + app.kubernetes.io/part-of: infra-operator + app.kubernetes.io/managed-by: kustomize + name: bgpconfiguration-editor-role +rules: +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations/status + verbs: + - get diff --git a/config/rbac/network_bgpconfiguration_viewer_role.yaml b/config/rbac/network_bgpconfiguration_viewer_role.yaml new file mode 100644 index 00000000..7f0fc43f --- /dev/null +++ b/config/rbac/network_bgpconfiguration_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view bgpconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: bgpconfiguration-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: infra-operator + app.kubernetes.io/part-of: infra-operator + app.kubernetes.io/managed-by: kustomize + name: bgpconfiguration-viewer-role +rules: +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations + verbs: + - get + - list + - watch +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ea6d0acf..4339ccbf 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -70,6 +70,9 @@ rules: verbs: - get - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -94,6 +97,19 @@ rules: - patch - update - watch +- apiGroups: + - frrk8s.metallb.io + resources: + - frrconfigurations + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch - apiGroups: - instanceha.openstack.org resources: @@ -156,6 +172,32 @@ rules: - get - patch - update +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations/finalizers + verbs: + - update +- apiGroups: + - network.openstack.org + resources: + - bgpconfigurations/status + verbs: + - get + - patch + - update - apiGroups: - network.openstack.org resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 7f7b6ecc..0415831f 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -9,4 +9,5 @@ resources: - network_v1beta1_netconfig.yaml - network_v1beta1_ipset.yaml - network_v1beta1_reservation.yaml +- network_v1beta1_bgpconfiguration.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/network_v1beta1_bgpconfiguration.yaml b/config/samples/network_v1beta1_bgpconfiguration.yaml new file mode 100644 index 00000000..7e3cee44 --- /dev/null +++ b/config/samples/network_v1beta1_bgpconfiguration.yaml @@ -0,0 +1,12 @@ +apiVersion: network.openstack.org/v1beta1 +kind: BGPConfiguration +metadata: + labels: + app.kubernetes.io/name: bgpconfiguration + app.kubernetes.io/instance: bgpconfiguration-sample + app.kubernetes.io/part-of: infra-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: infra-operator + name: bgpconfiguration-sample +spec: + frrConfigurationNamespace: metallb-system diff --git a/controllers/network/bgpconfiguration_controller.go b/controllers/network/bgpconfiguration_controller.go new file mode 100644 index 00000000..5324953d --- /dev/null +++ b/controllers/network/bgpconfiguration_controller.go @@ -0,0 +1,617 @@ +/* +Copyright 2024. + +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 network + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/api/equality" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" + networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + labels "github.com/openstack-k8s-operators/lib-common/modules/common/labels" + nad "github.com/openstack-k8s-operators/lib-common/modules/common/networkattachment" + util "github.com/openstack-k8s-operators/lib-common/modules/common/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + bgp "github.com/openstack-k8s-operators/infra-operator/pkg/bgp" +) + +// BGPConfigurationReconciler reconciles a BGPConfiguration object +type BGPConfigurationReconciler struct { + client.Client + Kclient kubernetes.Interface + Scheme *runtime.Scheme +} + +// GetLogger returns a logger object with a prefix of "controller.name" and additional controller context fields +func (r *BGPConfigurationReconciler) GetLogger(ctx context.Context) logr.Logger { + return log.FromContext(ctx).WithName("Controllers").WithName("BGPConfiguration") +} + +//+kubebuilder:rbac:groups=network.openstack.org,resources=bgpconfigurations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=network.openstack.org,resources=bgpconfigurations/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=network.openstack.org,resources=bgpconfigurations/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=frrk8s.metallb.io,resources=frrconfigurations,verbs=get;list;watch;create;update;patch;delete;deletecollection +//+kubebuilder:rbac:groups=k8s.cni.cncf.io,resources=network-attachment-definitions,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the BGPConfiguration object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *BGPConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) { + Log := r.GetLogger(ctx) + + // Fetch the BGPConfiguration instance + instance := &networkv1.BGPConfiguration{} + err := r.Client.Get(ctx, req.NamespacedName, instance) + if err != nil { + if k8s_errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. + // For additional cleanup logic use finalizers. Return and don't requeue. + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + return ctrl.Result{}, err + } + + helper, err := helper.NewHelper( + instance, + r.Client, + r.Kclient, + r.Scheme, + Log, + ) + if err != nil { + return ctrl.Result{}, err + } + + // initialize status if Conditions is nil, but do not reset if it already + // exists + isNewInstance := instance.Status.Conditions == nil + if isNewInstance { + instance.Status.Conditions = condition.Conditions{} + } + + // Save a copy of the condtions so that we can restore the LastTransitionTime + // when a condition's state doesn't change. + savedConditions := instance.Status.Conditions.DeepCopy() + + // Always patch the instance status when exiting this function so we can + // persist any changes. + defer func() { + condition.RestoreLastTransitionTimes( + &instance.Status.Conditions, savedConditions) + if instance.Status.Conditions.IsUnknown(condition.ReadyCondition) { + instance.Status.Conditions.Set( + instance.Status.Conditions.Mirror(condition.ReadyCondition)) + } + err := helper.PatchInstance(ctx, instance) + if err != nil { + _err = err + return + } + }() + + // initialize status + cl := condition.CreateList( + condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage), + condition.UnknownCondition(condition.ServiceConfigReadyCondition, condition.InitReason, condition.ServiceConfigReadyInitMessage), + ) + + instance.Status.Conditions.Init(&cl) + + // If we're not deleting this and the service object doesn't have our finalizer, add it. + if instance.DeletionTimestamp.IsZero() && controllerutil.AddFinalizer(instance, helper.GetFinalizer()) || isNewInstance { + return ctrl.Result{}, nil + } + + // Handle service delete + if !instance.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, instance, helper) + } + + // Handle non-deleted clusters + return r.reconcileNormal(ctx, instance, helper) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BGPConfigurationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { + Log := r.GetLogger(ctx) + podFN := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + result := []reconcile.Request{} + + // For each Pod event get the list of all + // BGPConfiguration to trigger reconcile for the one in the same namespace + bgpConfigurationList := &networkv1.BGPConfigurationList{} + + listOpts := []client.ListOption{ + client.InNamespace(o.GetNamespace()), + } + if err := r.Client.List(ctx, bgpConfigurationList, listOpts...); err != nil { + Log.Error(err, "Unable to retrieve BGPConfigurationList in namespace %s", o.GetNamespace()) + return nil + } + + // For each BGPConfiguration instance create a reconcile request + for _, i := range bgpConfigurationList.Items { + name := client.ObjectKey{ + Namespace: o.GetNamespace(), + Name: i.Name, + } + result = append(result, reconcile.Request{NamespacedName: name}) + } + if len(result) > 0 { + Log.Info("Reconcile request for:", "result", result) + + return result + } + return nil + }) + + frrFN := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + result := []reconcile.Request{} + + // For each FRRConfiguration event get the namespace of its openstack deployment + // from the `bgpconfiguration.openstack.org/namespace: openstack` label, + // get a list of all BGPConfiguration and trigger the reconcile. + ownerNamespaceLabelSelector := labels.GetOwnerNameSpaceLabelSelector(labels.GetGroupLabel("bgpconfiguration")) + bgpConfigurationNamespace, ok := o.GetLabels()[ownerNamespaceLabelSelector] + if !ok { + Log.Info(fmt.Sprintf("label %s not found for %s: %+v", + ownerNamespaceLabelSelector, o.GetName(), o.GetLabels())) + return nil + } + + bgpConfigurationList := &networkv1.BGPConfigurationList{} + listOpts := []client.ListOption{ + client.InNamespace(bgpConfigurationNamespace), + } + if err := r.Client.List(ctx, bgpConfigurationList, listOpts...); err != nil { + Log.Error(err, "Unable to retrieve BGPConfigurationList in namespace %s", bgpConfigurationNamespace) + return nil + } + + // For each BGPConfiguration instance create a reconcile request + for _, i := range bgpConfigurationList.Items { + name := client.ObjectKey{ + Namespace: bgpConfigurationNamespace, + Name: i.Name, + } + result = append(result, reconcile.Request{NamespacedName: name}) + } + if len(result) > 0 { + Log.Info("Reconcile request for:", "result", result) + + return result + } + return nil + }) + + // 'UpdateFunc', 'DeleteFunc' and 'CreateFunc' used to judge if a event about the object is + // what we want. If that is true, the event will be processed by the reconciler. + pPod := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Skip if + // * no NAD annotation was configured on old object OR + // * no NAD annotation was configured on new object AND + // * the resourceVersion has not changed + + oldConfigured := true + if val, ok := e.ObjectOld.GetAnnotations()[k8s_networkv1.NetworkAttachmentAnnot]; !ok || len(val) == 0 { + oldConfigured = false + } + newConfigured := true + if val, ok := e.ObjectNew.GetAnnotations()[k8s_networkv1.NetworkAttachmentAnnot]; !ok || len(val) == 0 { + newConfigured = false + } + + return (oldConfigured || newConfigured) && e.ObjectOld.GetResourceVersion() != e.ObjectNew.GetResourceVersion() + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Skip if + // * NAD annotation annotation key is missing + // * there is no additional network configured + if val, ok := e.Object.GetAnnotations()[k8s_networkv1.NetworkAttachmentAnnot]; !ok || len(val) == 0 { + return false + } + + return true + }, + CreateFunc: func(e event.CreateEvent) bool { + // Skip if + // * NAD annotation annotation key is missing + // * there is no additional network configured + if val, ok := e.Object.GetAnnotations()[k8s_networkv1.NetworkAttachmentAnnot]; !ok || len(val) == 0 { + return false + } + + return true + }, + } + + // 'UpdateFunc', 'DeleteFunc' and 'CreateFunc' used to judge if a event about the object is + // what we want. If that is true, the event will be processed by the reconciler. + pFRR := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Skip if the FRRConfiguration + // * doesn't contain label `bgpconfiguration.openstack.org/namespace` + ownerNamespaceLabelSelector := labels.GetOwnerNameSpaceLabelSelector(labels.GetGroupLabel("bgpconfiguration")) + if _, ok := e.ObjectOld.GetLabels()[ownerNamespaceLabelSelector]; !ok { + return false + } + + return e.ObjectOld.GetResourceVersion() != e.ObjectNew.GetResourceVersion() + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Skip if the FRRConfiguration + // * doesn't contain label `bgpconfiguration.openstack.org/namespace` + ownerNamespaceLabelSelector := labels.GetOwnerNameSpaceLabelSelector(labels.GetGroupLabel("bgpconfiguration")) + if _, ok := e.Object.GetLabels()[ownerNamespaceLabelSelector]; !ok { + return false + } + + return true + }, + CreateFunc: func(e event.CreateEvent) bool { + // Skip if the FRRConfiguration + // * doesn't contain label `bgpconfiguration.openstack.org/namespace` + ownerNamespaceLabelSelector := labels.GetOwnerNameSpaceLabelSelector(labels.GetGroupLabel("bgpconfiguration")) + if _, ok := e.Object.GetLabels()[ownerNamespaceLabelSelector]; !ok { + return false + } + + return true + }, + } + + return ctrl.NewControllerManagedBy(mgr). + For(&networkv1.BGPConfiguration{}). + // Watch pods which have additional networks configured with k8s_networkv1.NetworkAttachmentAnnot annotation in the same namespace + Watches(&corev1.Pod{}, + podFN, + builder.WithPredicates(pPod)). + // Watch FRRConfiguration which have the `bgpconfiguration.openstack.org/namespace` label + Watches(&frrk8sv1.FRRConfiguration{}, + frrFN, + builder.WithPredicates(pFRR)). + Complete(r) +} + +func (r *BGPConfigurationReconciler) reconcileDelete(ctx context.Context, instance *networkv1.BGPConfiguration, helper *helper.Helper) (ctrl.Result, error) { + Log := r.GetLogger(ctx) + Log.Info("Reconciling Service delete") + + // Delete all FRRConfiguration in the Spec.FRRConfigurationNamespace namespace, + // which have the correct owner and ownernamespace label + err := r.Client.DeleteAllOf( + ctx, + &frrk8sv1.FRRConfiguration{}, + client.InNamespace(instance.Spec.FRRConfigurationNamespace), + client.MatchingLabels{ + labels.GetOwnerNameLabelSelector(labels.GetGroupLabel("bgpconfiguration")): instance.Name, + labels.GetOwnerNameSpaceLabelSelector(labels.GetGroupLabel("bgpconfiguration")): instance.Namespace, + }, + ) + if err != nil && !k8s_errors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("Error DeleteAllOf FRRConfiguration: %w", err) + } + + // Service is deleted so remove the finalizer. + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + Log.Info("Reconciled Service delete successfully") + + return ctrl.Result{}, nil +} + +func (r *BGPConfigurationReconciler) reconcileNormal(ctx context.Context, instance *networkv1.BGPConfiguration, helper *helper.Helper) (ctrl.Result, error) { + Log := r.GetLogger(ctx) + Log.Info("Reconciling Service") + + // Get a list of pods which are in the same namespace as the ctlplane + // to verify if a FRRConfiguration needs to be created for. + podList := &corev1.PodList{} + + listOpts := []client.ListOption{ + client.InNamespace(instance.Namespace), + } + if err := r.Client.List(ctx, podList, listOpts...); err != nil { + return ctrl.Result{}, fmt.Errorf("Unable to retrieve PodList %w", err) + } + + // get podDetail all pods which have additional interfaces configured + podNetworkDetailList, err := getPodNetworkDetails(ctx, helper, podList) + if err != nil { + return ctrl.Result{}, err + } + + // get all frr configs for the nodes pods are scheduled on + groupLabel := labels.GetGroupLabel("bgpconfiguration") + frrNodeConfigs := map[string]frrk8sv1.FRRConfiguration{} + for _, nodeName := range bgp.GetNodesRunningPods(podNetworkDetailList) { + frrConfigList := &frrk8sv1.FRRConfigurationList{} + listOpts := []client.ListOption{ + client.InNamespace(instance.Spec.FRRConfigurationNamespace), // defaults to metallb-system + } + if err := r.Client.List(ctx, frrConfigList, listOpts...); err != nil { + return ctrl.Result{}, fmt.Errorf("Unable to retrieve FRRConfigurationList %w", err) + } + + var nodeSelector metav1.LabelSelector + + // validate if a nodeSelector is configured for the nodeName + // this is just for the case where metallb would change + // the nodeSelector it puts on the FRRConfigurations it creates + f := func(c networkv1.FRRNodeConfigurationSelectorType) bool { + return c.NodeName == nodeName + } + idx := slices.IndexFunc(instance.Spec.FRRNodeConfigurationSelector, f) + if idx >= 0 { + nodeSelector = instance.Spec.FRRNodeConfigurationSelector[idx].NodeSelector + } else { + // metallb puts per default just the hostname selector in the frrconfiguration. + // nodeSelector: + // matchLabels: + // kubernetes.io/hostname: worker-0 + nodeSelector.MatchLabels = map[string]string{ + corev1.LabelHostname: nodeName, + } + Log.Info(fmt.Sprintf("using default nodeSelector %+v", nodeSelector.MatchLabels)) + } + + var frrCfg *frrk8sv1.FRRConfiguration + for _, cfg := range frrConfigList.Items { + frrLabels := cfg.GetLabels() + // skip checking our own managed FRRConfigurations, + // but validate if the FRRConfiguration is still needed, + // or the pod was deleted/completed/failed/unknown + if _, ok := frrLabels[labels.GetOwnerNameLabelSelector(labels.GetGroupLabel("bgpconfiguration"))]; ok { + if podName, ok := frrLabels[groupLabel+"/pod-name"]; ok { + f := func(p bgp.PodDetail) bool { + return p.Name == podName && p.Namespace == instance.Namespace + } + idx := slices.IndexFunc(podNetworkDetailList, f) + if idx < 0 { + // There is no pod in the namespace corrsponding to the FRRConfiguration, delete it + if err := r.Client.Delete(ctx, &cfg); err != nil && k8s_errors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("Unable to delete FRRConfiguration %w", err) + } + Log.Info(fmt.Sprintf("pod with name: %s either in state deleted, completed, failed or unknown , deleted FRRConfiguration %s", podName, cfg.Name)) + } + } + // skip our own managed FRRConfigurations + continue + } + if equality.Semantic.DeepEqual(cfg.Spec.NodeSelector, nodeSelector) { + frrCfg = cfg.DeepCopy() + } + } + + if frrCfg != nil { + frrNodeConfigs[nodeName] = *frrCfg + } else { + // error, we have not found the frrConfig for the node + return ctrl.Result{}, fmt.Errorf("no FRRConfiguration found for node %s using nodeSelector %v", nodeName, nodeSelector) + } + } + + // create FRRConfigurations for the podNetworkDetailList + for _, podNetworkDetail := range podNetworkDetailList { + err = r.createOrPatchFRRConfiguration( + ctx, + instance, + &podNetworkDetail, + frrNodeConfigs, + labels.GetLabels(instance, + groupLabel, + map[string]string{ + groupLabel + "/pod-name": podNetworkDetail.Name, + }), + ) + if err != nil { + return ctrl.Result{}, err + } + } + + Log.Info("Reconciled Service successfully") + return ctrl.Result{}, nil +} + +// getPodNetworkDetails - returns the podDetails for a list of pods in status.phase: Running +// where the pod has the multus k8s_networkv1.NetworkAttachmentAnnot annotation +// and its value is not '[]' +func getPodNetworkDetails( + ctx context.Context, + h *helper.Helper, + pods *corev1.PodList, +) ([]bgp.PodDetail, error) { + Log := h.GetLogger() + Log.Info("Reconciling getPodNetworkDetails") + detailList := []bgp.PodDetail{} + if pods != nil { + for _, pod := range pods.Items { + // skip pods which are in deletion to make sure its FRRConfiguration gets deleted + if !pod.DeletionTimestamp.IsZero() { + continue + } + // skip pods which are not in Running phase (deleted/completed/failed/unknown) + if pod.Status.Phase != corev1.PodRunning { + continue + } + if netAttachString, ok := pod.Annotations[k8s_networkv1.NetworkAttachmentAnnot]; ok && netAttachString != "[]" { + // get the elements from val to validate the status annotation has the right length + netAttach := []k8s_networkv1.NetworkSelectionElement{} + err := json.Unmarshal([]byte(netAttachString), &netAttach) + if err != nil { + return nil, fmt.Errorf("failed to decode networks %s: %w", netAttachString, err) + } + + // verify the nodeName information is already present in the pod spec, otherwise report an error to reconcile + if pod.Spec.NodeName == "" { + return detailList, fmt.Errorf(fmt.Sprintf("empty spec.nodeName on pod %s", pod.Name)) + } + + detail := bgp.PodDetail{ + Name: pod.Name, + Namespace: pod.Namespace, + Node: pod.Spec.NodeName, + } + + netsStatus, err := nad.GetNetworkStatusFromAnnotation(pod.Annotations) + if err != nil { + return detailList, fmt.Errorf("failed to get netsStatus from pod annoation - %v: %w", pod.Annotations, err) + } + // on pod start it can happen that the network status annotation does not yet + // reflect all requested networks. return with an error to reconcile if the length + // is <= the status. Note: the status also has the pod network + if len(netsStatus) <= len(netAttach) { + return detailList, fmt.Errorf(fmt.Sprintf("metadata.Annotations['k8s.ovn.org/pod-networks'] %s on pod %s, does not match requested networks %s", + pod.GetAnnotations()[k8s_networkv1.NetworkStatusAnnot], pod.Name, netAttachString)) + } + + var netsStatusCopy = make([]k8s_networkv1.NetworkStatus, len(netsStatus)) + copy(netsStatusCopy, netsStatus) + // verify there are IP information for all networks in the status, otherwise report an error to reconcile + for idx, netStat := range netsStatusCopy { + // remove status for the pod interface + // it should always be ovn-kubernetes, but if not, remove status for eth0 which is the pod network + if netStat.Name == "ovn-kubernetes" || netStat.Interface == "eth0" { + removeIndex(netsStatus, idx) + continue + } + + // get ipam configuration from NAD + nadName := strings.TrimPrefix(netStat.Name, pod.Namespace+"/") + netAtt, err := nad.GetNADWithName( + ctx, h, nadName, pod.Namespace) + if err != nil { + return detailList, err + } + + ipam, err := nad.GetJSONPathFromConfig(*netAtt, ".ipam") + if err != nil { + return detailList, err + } + + // if the NAD has no ipam configured, skip it as there will be no IP + if ipam == "{}" { + Log.Info(fmt.Sprintf("removing netsStatus for NAD %s for %s, IPAM configuration is empty: %s", netAtt.Name, pod.Name, ipam)) + removeIndex(netsStatus, idx) + continue + } + + // verify there is IP information for the network, otherwise report an error to reconcile + if len(netStat.IPs) == 0 { + return detailList, fmt.Errorf(fmt.Sprintf("no IP information for network %s on pod %s", netStat.Name, pod.Name)) + } + } + + detail.NetworkStatus = netsStatus + + detailList = append(detailList, detail) + } + } + } + + Log.Info("Reconciled getPodNetworkDetails successfully") + return detailList, nil +} + +func removeIndex(s []k8s_networkv1.NetworkStatus, index int) []k8s_networkv1.NetworkStatus { + return append(s[:index], s[index+1:]...) +} + +// createOrPatchFRRConfiguration - +func (r *BGPConfigurationReconciler) createOrPatchFRRConfiguration( + ctx context.Context, + instance *networkv1.BGPConfiguration, + podDtl *bgp.PodDetail, + nodeFRRCfgs map[string]frrk8sv1.FRRConfiguration, + frrLabels map[string]string, +) error { + Log := r.GetLogger(ctx) + Log.Info("Reconciling createOrUpdateFRRConfiguration") + + podPrefixes := bgp.GetFRRPodPrefixes(podDtl.NetworkStatus) + + nodeFRRCfg := nodeFRRCfgs[podDtl.Node] + + frrConfig := &frrk8sv1.FRRConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: instance.Namespace + "-" + podDtl.Name, + Namespace: instance.Spec.FRRConfigurationNamespace, + }, + } + + frrConfigSpec := &frrk8sv1.FRRConfigurationSpec{} + var routers []frrk8sv1.Router + for _, r := range nodeFRRCfg.Spec.BGP.Routers { + routers = append(routers, frrk8sv1.Router{ + ASN: r.ASN, + Neighbors: bgp.GetFRRNeighbors(r.Neighbors, podPrefixes), + Prefixes: podPrefixes, + }) + } + frrConfigSpec.BGP.Routers = routers + frrConfigSpec.NodeSelector = nodeFRRCfg.Spec.NodeSelector + + // create or update the FRRConfiguration + op, err := controllerutil.CreateOrPatch(ctx, r.Client, frrConfig, func() error { + frrConfig.Labels = util.MergeMaps(frrConfig.Labels, frrLabels) + frrConfigSpec.DeepCopyInto(&frrConfig.Spec) + + return nil + }) + if err != nil { + return fmt.Errorf("error create/updating service FRRConfiguration: %w", err) + } + + if op != controllerutil.OperationResultNone { + Log.Info("operation:", "FRRConfiguration name", frrConfig.Name, "Operation", string(op)) + } + + Log.Info("Reconciled createOrUpdateFRRConfiguration successfully") + return nil +} diff --git a/go.mod b/go.mod index e4c31a21..ae2ffd4c 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,11 @@ require ( github.com/go-logr/logr v1.4.2 github.com/google/uuid v1.6.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.5 + github.com/metallb/frr-k8s v0.0.0-00010101000000-000000000000 github.com/onsi/ginkgo/v2 v2.20.1 github.com/onsi/gomega v1.34.1 github.com/openstack-k8s-operators/infra-operator/apis v0.3.0 - github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc + github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f github.com/openstack-k8s-operators/lib-common/modules/test v0.5.1-0.20241114091812-6dc9fd0961dc github.com/rabbitmq/cluster-operator/v2 v2.9.0 go.uber.org/zap v1.27.0 @@ -25,13 +26,15 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/swag v0.22.9 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -50,14 +53,14 @@ require ( github.com/openshift/api v3.9.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.51.1 // indirect + github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect @@ -72,7 +75,7 @@ require ( k8s.io/apiextensions-apiserver v0.29.10 // indirect k8s.io/component-base v0.29.10 // indirect k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect @@ -86,3 +89,6 @@ replace github.com/openshift/api => github.com/openshift/api v0.0.0-202408300231 // custom RabbitmqClusterSpecCore for OpenStackControlplane (v2.6.0_patches_tag) replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20241017142550-a3524acedd49 //allow-merging + +// pin frr-k8s to v0.0.11 because later versions have a req for golang 1.22 +replace github.com/metallb/frr-k8s => github.com/metallb/frr-k8s v0.0.11 //allow-merging diff --git a/go.sum b/go.sum index 70914b75..738272af 100644 --- a/go.sum +++ b/go.sum @@ -6,24 +6,28 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= -github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= +github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= -github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -63,6 +67,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/metallb/frr-k8s v0.0.11 h1:Y0SkDHg3BBHIcxOV/H5S8AzklYsyEraC6i1MMO8qoKc= +github.com/metallb/frr-k8s v0.0.11/go.mod h1:Yeqy4z+77hlwto8sAg/gozP8Flu0lWZIua96YAhnkYI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -76,8 +82,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/openshift/api v0.0.0-20240830023148-b7d0481c9094 h1:J1wuGhVxpsHykZBa6Beb1gQ96Ptej9AE/BvwCBiRj1E= github.com/openshift/api v0.0.0-20240830023148-b7d0481c9094/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc h1:Ufa/q/nC9wmKblvsc0kJppsXHOJoY4fbUamb3ItWCOk= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241114091812-6dc9fd0961dc/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f h1:uCdmEcic/axWXzAHXsrrP/mkaQVJJnXgQB+6corsLm0= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241212093958-9796f77b325f/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= github.com/openstack-k8s-operators/lib-common/modules/test v0.5.1-0.20241114091812-6dc9fd0961dc h1:knyjd0eg4DyY+dTDHSrE9QwrZ0mtr7MpASCrmhW+5pw= github.com/openstack-k8s-operators/lib-common/modules/test v0.5.1-0.20241114091812-6dc9fd0961dc/go.mod h1:LV0jo5etIsGyINpmB37i4oWR8zU6ApIuh7fsqGGA41o= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20241017142550-a3524acedd49 h1:/7SnnHfGCH/dwuZFNUx54zw4cnwv2w6hjONq16aoowM= @@ -88,20 +94,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= +github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -130,8 +136,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -198,8 +204,8 @@ k8s.io/component-base v0.29.10 h1:YQrQ/bpzGPGqIPEPaBzxjH0/1DJOI+yZPZNbbz7ZCBY= k8s.io/component-base v0.29.10/go.mod h1:IbwsBob2DnYiAONsSHIuYenchqcDycbHSLHrXshuLgM= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 h1:qVoMaQV5t62UUvHe16Q3eb2c5HPzLHYzsi0Tu/xLndo= +k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.17.6 h1:12IXsozEsIXWAMRpgRlYS1jjAHQXHtWEOMdULh3DbEw= diff --git a/main.go b/main.go index 79de7b84..f6a17453 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" instancehav1 "github.com/openstack-k8s-operators/infra-operator/apis/instanceha/v1beta1" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" @@ -69,6 +70,7 @@ func init() { utilruntime.Must(instancehav1.AddToScheme(scheme)) utilruntime.Must(redisv1.AddToScheme(scheme)) utilruntime.Must(networkv1.AddToScheme(scheme)) + utilruntime.Must(frrk8sv1.AddToScheme(scheme)) utilruntime.Must(k8s_networkv1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -207,6 +209,15 @@ func main() { os.Exit(1) } + if err = (&networkcontrollers.BGPConfigurationReconciler{ + Client: mgr.GetClient(), + Kclient: kclient, + Scheme: mgr.GetScheme(), + }).SetupWithManager(context.Background(), mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BGPConfiguration") + os.Exit(1) + } + // Acquire environmental defaults and initialize operator defaults with them memcachedv1.SetupDefaults() redisv1.SetupDefaults() diff --git a/pkg/bgp/funcs.go b/pkg/bgp/funcs.go new file mode 100644 index 00000000..f7e65c38 --- /dev/null +++ b/pkg/bgp/funcs.go @@ -0,0 +1,60 @@ +package bgp + +import ( + k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/common/util" +) + +// PodDetail - +type PodDetail struct { + Name string + Namespace string + Node string + NetworkStatus []k8s_networkv1.NetworkStatus +} + +// GetFRRPodPrefixes - returns the FRRConfiguration prefix entries for a pod +// secondary network interfaces in the format "10.10.10.10/32" +func GetFRRPodPrefixes(networkStatus []k8s_networkv1.NetworkStatus) []string { + podPrefixes := []string{} + for _, podNetStat := range networkStatus { + if podNetStat.Name == "ovn-kubernetes" { + continue + } + + for _, ip := range podNetStat.IPs { + ip = ip + "/32" + if !util.StringInSlice(ip, podPrefixes) { + podPrefixes = append(podPrefixes, ip) + } + } + } + + return podPrefixes +} + +// GetFRRNeighbors - returs a list of FRR Neighor for for podPrefixes, using a copy of the +// nodeNeigbors and replacing its Prefixes with the podPrefixes. +func GetFRRNeighbors(nodeNeighbors []frrk8sv1.Neighbor, podPrefixes []string) []frrk8sv1.Neighbor { + podNeighbors := []frrk8sv1.Neighbor{} + + for _, neighbor := range nodeNeighbors { + neighbor.ToAdvertise.Allowed.Prefixes = podPrefixes + podNeighbors = append(podNeighbors, neighbor) + } + + return podNeighbors +} + +// GetNodesRunningPods - get a uniq list of all nodes from all a PodDetail list +func GetNodesRunningPods(podNetworkDetailList []PodDetail) []string { + nodes := []string{} + for _, p := range podNetworkDetailList { + if !util.StringInSlice(p.Node, nodes) { + nodes = append(nodes, p.Node) + } + } + + return nodes +} diff --git a/pkg/bgp/funcs_test.go b/pkg/bgp/funcs_test.go new file mode 100644 index 00000000..ebbb310b --- /dev/null +++ b/pkg/bgp/funcs_test.go @@ -0,0 +1,319 @@ +package bgp + +import ( + "testing" + + . "github.com/onsi/gomega" //revive:disable:dot-imports + + k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" +) + +func TestGetNodesRunningPods(t *testing.T) { + + tests := []struct { + name string + podDatails []PodDetail + want []string + }{ + { + name: "No network", + podDatails: []PodDetail{}, + want: []string{}, + }, + { + name: "single pod", + podDatails: []PodDetail{ + { + Name: "foo", + Namespace: "bar", + Node: "node1", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + }, + want: []string{"node1"}, + }, + { + name: "multiple pods different nodes", + podDatails: []PodDetail{ + { + Name: "foo1", + Namespace: "bar", + Node: "node1", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + { + Name: "foo2", + Namespace: "bar", + Node: "node2", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + }, + want: []string{"node1", "node2"}, + }, + { + name: "multiple pods same node", + podDatails: []PodDetail{ + { + Name: "foo1", + Namespace: "bar", + Node: "node1", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + { + Name: "foo2", + Namespace: "bar", + Node: "node1", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + }, + want: []string{"node1"}, + }, + { + name: "multiple pods mix of node assignment", + podDatails: []PodDetail{ + { + Name: "foo1", + Namespace: "bar", + Node: "node1", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + { + Name: "foo2", + Namespace: "bar", + Node: "node2", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + { + Name: "foo3", + Namespace: "bar", + Node: "node2", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + { + Name: "foo4", + Namespace: "bar", + Node: "node3", + NetworkStatus: []k8s_networkv1.NetworkStatus{}, + }, + }, + want: []string{"node1", "node2", "node3"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + nodes := GetNodesRunningPods(tt.podDatails) + g.Expect(nodes).To(HaveLen(len(tt.want))) + g.Expect(nodes).To(BeEquivalentTo(tt.want)) + }) + } +} + +func TestGetFRRPodPrefixes(t *testing.T) { + + tests := []struct { + name string + netStatus []k8s_networkv1.NetworkStatus + want []string + }{ + { + name: "No status", + netStatus: []k8s_networkv1.NetworkStatus{}, + want: []string{}, + }, + { + name: "only pod network", + netStatus: []k8s_networkv1.NetworkStatus{ + { + Name: "ovn-kubernetes", + Interface: "eth0", + IPs: []string{"192.168.56.59"}, + Mac: "0a:58:c0:a8:38:3b", + Default: true, + }, + }, + want: []string{}, + }, + { + name: "pod and additional network", + netStatus: []k8s_networkv1.NetworkStatus{ + { + Name: "ovn-kubernetes", + Interface: "eth0", + IPs: []string{"192.168.56.59"}, + Mac: "0a:58:c0:a8:38:3b", + Default: true, + }, + { + Name: "foo/bar", + Interface: "bar", + IPs: []string{"172.17.0.40"}, + Mac: "de:39:07:a1:b5:6b", + Default: false, + }, + }, + want: []string{"172.17.0.40/32"}, + }, + { + name: "pod and multiple additional networks", + netStatus: []k8s_networkv1.NetworkStatus{ + { + Name: "ovn-kubernetes", + Interface: "eth0", + IPs: []string{"192.168.56.59"}, + Mac: "0a:58:c0:a8:38:3b", + Default: true, + }, + { + Name: "foo/bar", + Interface: "bar", + IPs: []string{"172.17.0.40"}, + Mac: "de:39:07:a1:b5:6b", + Default: false, + }, + { + Name: "foo/dog", + Interface: "dog", + IPs: []string{"172.18.0.40"}, + Mac: "de:39:07:a1:b5:6c", + Default: false, + }, + }, + want: []string{"172.17.0.40/32", "172.18.0.40/32"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + nodes := GetFRRPodPrefixes(tt.netStatus) + g.Expect(nodes).To(HaveLen(len(tt.want))) + g.Expect(nodes).To(BeEquivalentTo(tt.want)) + }) + } +} + +func TestGetFRRNeighbors(t *testing.T) { + + tests := []struct { + name string + nodeNeighbors []frrk8sv1.Neighbor + podPrefixes []string + want []frrk8sv1.Neighbor + }{ + { + name: "No nodeNeighbors, no podPrefixes", + nodeNeighbors: []frrk8sv1.Neighbor{}, + podPrefixes: []string{}, + want: []frrk8sv1.Neighbor{}, + }, + { + name: "single nodeNeighbor", + nodeNeighbors: []frrk8sv1.Neighbor{ + { + Address: "10.10.10.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "10.10.10.11/32", + "10.10.10.12/32", + }, + }, + }, + }, + }, + podPrefixes: []string{"172.17.0.40/32", "172.18.0.40/32"}, + want: []frrk8sv1.Neighbor{ + { + Address: "10.10.10.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "172.17.0.40/32", + "172.18.0.40/32", + }, + }, + }, + }, + }, + }, + { + name: "multiple nodeNeighbors", + nodeNeighbors: []frrk8sv1.Neighbor{ + { + Address: "10.10.10.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "10.10.10.11/32", + "10.10.10.12/32", + }, + }, + }, + }, + { + Address: "10.10.11.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "10.10.10.11/32", + "10.10.10.12/32", + }, + }, + }, + }, + }, + podPrefixes: []string{"172.17.0.40/32", "172.18.0.40/32"}, + want: []frrk8sv1.Neighbor{ + { + Address: "10.10.10.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "172.17.0.40/32", + "172.18.0.40/32", + }, + }, + }, + }, + { + Address: "10.10.11.10", + ASN: 64999, + ToAdvertise: frrk8sv1.Advertise{ + Allowed: frrk8sv1.AllowedOutPrefixes{ + Mode: frrk8sv1.AllowRestricted, + Prefixes: []string{ + "172.17.0.40/32", + "172.18.0.40/32", + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + nodes := GetFRRNeighbors(tt.nodeNeighbors, tt.podPrefixes) + g.Expect(nodes).To(HaveLen(len(tt.want))) + g.Expect(nodes).To(BeEquivalentTo(tt.want)) + }) + } +} diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 4f5aa713..acceb362 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -35,6 +35,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" @@ -697,3 +699,172 @@ func GetMemcached(name types.NamespacedName) *memcachedv1.Memcached { }, timeout, interval).Should(Succeed()) return instance } + +func CreateBGPConfiguration(namespace string, spec map[string]interface{}) client.Object { + name := uuid.New().String() + + raw := map[string]interface{}{ + "apiVersion": "network.openstack.org/v1beta1", + "kind": "BGPConfiguration", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": spec, + } + + return th.CreateUnstructured(raw) +} + +func GetBGPConfiguration(name types.NamespacedName) *networkv1.BGPConfiguration { + instance := &networkv1.BGPConfiguration{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + +func GetBGPConfigurationSpec(namespace string) map[string]interface{} { + if namespace != "" { + return map[string]interface{}{ + "frrConfigurationNamespace": namespace, + } + } + return map[string]interface{}{} +} + +func GetFRRConfiguration(name types.NamespacedName) *frrk8sv1.FRRConfiguration { + instance := &frrk8sv1.FRRConfiguration{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + +func CreateFRRConfiguration(name types.NamespacedName, spec map[string]interface{}) client.Object { + raw := map[string]interface{}{ + "apiVersion": "frrk8s.metallb.io/v1beta1", + "kind": "FRRConfiguration", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + + return th.CreateUnstructured(raw) +} + +func GetMetalLBFRRConfigurationSpec(node string) map[string]interface{} { + return map[string]interface{}{ + "bgp": map[string]interface{}{ + "routers": []map[string]interface{}{ + { + "asn": 64999, + "neighbors": []map[string]interface{}{ + { + "address": "10.10.10.10", + "asn": 64999, + "disableMP": false, + "holdTime": "1m30s", + "keepaliveTime": "30s", + "password": "foo", + "port": 179, + "toAdvertise": map[string]interface{}{ + "allowed": map[string]interface{}{ + "mode": "filtered", + "prefixes": []string{ + "11.11.11.11/32", + "11.11.11.12/32", + }, + }, + }, + "toReceive": map[string]interface{}{ + "allowed": map[string]interface{}{ + "mode": "filtered", + }, + }, + }, + }, + "prefixes": []string{ + "11.11.11.11/32", + "11.11.11.12/32", + }, + }, + }, + }, + "nodeSelector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "kubernetes.io/hostname": node, + }, + }, + } +} + +func GetNADSpec() map[string]interface{} { + return map[string]interface{}{ + "config": `{ + "cniVersion": "0.3.1", + "name": "internalapi", + "type": "bridge", + "isDefaultGateway": true, + "isGateway": true, + "forceAddress": false, + "ipMasq": true, + "hairpinMode": true, + "bridge": "internalapi", + "ipam": { + "type": "whereabouts", + "range": "172.17.0.0/24", + "range_start": "172.17.0.30", + "range_end": "172.17.0.70", + "gateway": "172.17.0.1" + } + }`, + } +} + +func GetPodSpec(node string) map[string]interface{} { + return map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "foo", + "image": "foo:latest", + "ports": []map[string]interface{}{ + { + "containerPort": 80, + }, + }, + }, + }, + "terminationGracePeriodSeconds": 0, + "nodeName": node, + } +} + +func GetPodAnnotation(namespace string) map[string]string { + return map[string]string{ + k8s_networkv1.NetworkStatusAnnot: fmt.Sprintf(`[{ + "name": "ovn-kubernetes", + "interface": "eth0", + "ips": [ + "192.168.56.59" + ], + "mac": "0a:58:c0:a8:38:3b", + "default": true, + "dns": {} +},{ + "name": "%s/internalapi", + "interface": "internalapi", + "ips": [ + "172.17.0.40" + ], + "mac": "de:39:07:a1:b5:6b", + "dns": {}, + "gateway": [ + "172.17.0.1" + ] +}]`, namespace), + k8s_networkv1.NetworkAttachmentAnnot: fmt.Sprintf(`[{"name":"internalapi","namespace":"%s","interface":"internalapi","default-route":["172.17.0.1"]}]`, namespace), + } +} diff --git a/tests/functional/bgpconfiguration_controller_test.go b/tests/functional/bgpconfiguration_controller_test.go new file mode 100644 index 00000000..7274c2cf --- /dev/null +++ b/tests/functional/bgpconfiguration_controller_test.go @@ -0,0 +1,228 @@ +/* +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 functional_test + +import ( + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + "k8s.io/apimachinery/pkg/types" + + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("BGPConfiguration controller", func() { + var bgpcfgName types.NamespacedName + var meallbFRRCfgName types.NamespacedName + frrCfgNamespace := "metallb-system" + + When("a default BGPConfiguration gets created", func() { + BeforeEach(func() { + bgpcfg := CreateBGPConfiguration(namespace, GetBGPConfigurationSpec("")) + bgpcfgName.Name = bgpcfg.GetName() + bgpcfgName.Namespace = bgpcfg.GetNamespace() + DeferCleanup(th.DeleteInstance, bgpcfg) + }) + + It("should have created a BGPConfiguration with default FRRConfigurationNamespace", func() { + Eventually(func(g Gomega) { + bgpcfg := GetBGPConfiguration(bgpcfgName) + g.Expect(bgpcfg).To(Not(BeNil())) + g.Expect(bgpcfg.Spec.FRRConfigurationNamespace).To(Equal(frrCfgNamespace)) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A pod with NAD gets created but node FRR reference configuration missing", func() { + var podFrrName types.NamespacedName + var podName types.NamespacedName + var metallbNS *corev1.Namespace + + BeforeEach(func() { + metallbNS = th.CreateNamespace(frrCfgNamespace + "-" + namespace) + + // create a nad config with gateway + nad := th.CreateNAD(types.NamespacedName{Namespace: namespace, Name: "internalapi"}, GetNADSpec()) + + bgpcfg := CreateBGPConfiguration(namespace, GetBGPConfigurationSpec(metallbNS.Name)) + bgpcfgName.Name = bgpcfg.GetName() + bgpcfgName.Namespace = bgpcfg.GetNamespace() + + podName = types.NamespacedName{Namespace: namespace, Name: uuid.New().String()} + // create pod with NAD annotation + th.CreatePod(podName, GetPodAnnotation(namespace), GetPodSpec("worker-0")) + th.SimulatePodPhaseRunning(podName) + + podFrrName.Name = podName.Namespace + "-" + podName.Name + podFrrName.Namespace = frrCfgNamespace + + DeferCleanup(th.DeleteInstance, bgpcfg) + DeferCleanup(th.DeleteInstance, nad) + }) + + It("should NOT have created a FRRConfiguration for the pod", func() { + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + + frr := &frrk8sv1.FRRConfiguration{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, podFrrName, frr)).Should(Not(Succeed())) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("a pod gets created", func() { + var podFrrName types.NamespacedName + var podName types.NamespacedName + var metallbNS *corev1.Namespace + + BeforeEach(func() { + metallbNS = th.CreateNamespace(frrCfgNamespace + "-" + namespace) + // create a FRR configuration for a node + meallbFRRCfgName = types.NamespacedName{Namespace: metallbNS.Name, Name: "worker-0"} + meallbFRRCfg := CreateFRRConfiguration(meallbFRRCfgName, GetMetalLBFRRConfigurationSpec("worker-0")) + Expect(meallbFRRCfg).To(Not(BeNil())) + + // TODO test without GW? + // create a nad config with gateway + nad := th.CreateNAD(types.NamespacedName{Namespace: namespace, Name: "internalapi"}, GetNADSpec()) + + bgpcfg := CreateBGPConfiguration(namespace, GetBGPConfigurationSpec(metallbNS.Name)) + bgpcfgName.Name = bgpcfg.GetName() + bgpcfgName.Namespace = bgpcfg.GetNamespace() + + podName = types.NamespacedName{Namespace: namespace, Name: uuid.New().String()} + // create pod without NAD annotation + th.CreatePod(podName, map[string]string{}, GetPodSpec("worker-0")) + th.SimulatePodPhaseRunning(podName) + + podFrrName.Name = podName.Namespace + "-" + podName.Name + podFrrName.Namespace = metallbNS.Name + + DeferCleanup(th.DeleteInstance, bgpcfg) + DeferCleanup(th.DeleteInstance, nad) + DeferCleanup(th.DeleteInstance, meallbFRRCfg) + }) + + It("should NOT have created a FRRConfiguration for the pod", func() { + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + + frr := &frrk8sv1.FRRConfiguration{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, podFrrName, frr)).Should(Not(Succeed())) + }, timeout, interval).Should(Succeed()) + }) + + When("NAD annotation gets added to the pod", func() { + BeforeEach(func() { + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + + pod.Annotations = GetPodAnnotation(namespace) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Update(ctx, pod)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + }) + + It("should have created a FRRConfiguration for the pod", func() { + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + + podFrrName := podName.Namespace + "-" + podName.Name + Eventually(func(g Gomega) { + frr := GetFRRConfiguration(types.NamespacedName{Namespace: metallbNS.Name, Name: podFrrName}) + g.Expect(frr).To(Not(BeNil())) + g.Expect(frr.Spec.BGP.Routers[0].Prefixes[0]).To(Equal("172.17.0.40/32")) + }, timeout, interval).Should(Succeed()) + + }) + }) + + When("another pod with NAD annotation gets created", func() { + var podName types.NamespacedName + + BeforeEach(func() { + podName = types.NamespacedName{Namespace: namespace, Name: uuid.New().String()} + // create pod with NAD annotation + th.CreatePod(podName, GetPodAnnotation(namespace), GetPodSpec("worker-0")) + th.SimulatePodPhaseRunning(podName) + }) + + It("should have created a FRRConfiguration for the pod2", func() { + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + + podFrrName := podName.Namespace + "-" + podName.Name + Eventually(func(g Gomega) { + frr := GetFRRConfiguration(types.NamespacedName{Namespace: metallbNS.Name, Name: podFrrName}) + g.Expect(frr).To(Not(BeNil())) + g.Expect(frr.Spec.BGP.Routers[0].Prefixes[0]).To(Equal("172.17.0.40/32")) + }, timeout, interval).Should(Succeed()) + + }) + }) + }) + + When("a pod with NAD gets deleted", func() { + var podFrrName types.NamespacedName + var podName types.NamespacedName + + BeforeEach(func() { + metallbNS := th.CreateNamespace(frrCfgNamespace + "-" + namespace) + // create a FRR configuration for a node + meallbFRRCfgName = types.NamespacedName{Namespace: metallbNS.Name, Name: "worker-0"} + meallbFRRCfg := CreateFRRConfiguration(meallbFRRCfgName, GetMetalLBFRRConfigurationSpec("worker-0")) + Expect(meallbFRRCfg).To(Not(BeNil())) + + // create a nad config with gateway + nad := th.CreateNAD(types.NamespacedName{Namespace: namespace, Name: "internalapi"}, GetNADSpec()) + + bgpcfg := CreateBGPConfiguration(namespace, GetBGPConfigurationSpec(metallbNS.Name)) + bgpcfgName.Name = bgpcfg.GetName() + bgpcfgName.Namespace = bgpcfg.GetNamespace() + + podName = types.NamespacedName{Namespace: namespace, Name: uuid.New().String()} + // create pod with NAD annotation + th.CreatePod(podName, GetPodAnnotation(namespace), GetPodSpec("worker-0")) + th.SimulatePodPhaseRunning(podName) + + podFrrName.Name = podName.Namespace + "-" + podName.Name + podFrrName.Namespace = metallbNS.Name + + DeferCleanup(th.DeleteInstance, bgpcfg) + DeferCleanup(th.DeleteInstance, nad) + DeferCleanup(th.DeleteInstance, meallbFRRCfg) + }) + + It("should delete the FRRConfiguration for the pod", func() { + // delete the pod + pod := th.GetPod(podName) + Expect(pod).To(Not(BeNil())) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Delete(ctx, pod)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + + // validate that the frr cfg is gone + frr := &frrk8sv1.FRRConfiguration{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, podFrrName, frr)).Should(Not(Succeed())) + }, timeout, interval).Should(Succeed()) + }) + }) +}) diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index be5b0b50..a86d4787 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -37,6 +37,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/webhook" + k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + frrk8sv1 "github.com/metallb/frr-k8s/api/v1beta1" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" @@ -85,11 +87,25 @@ var _ = BeforeSuite(func() { "github.com/rabbitmq/cluster-operator/v2", "../../go.mod", "config/crd/bases") Expect(err).ShouldNot(HaveOccurred()) + frrCRDs, err := test.GetCRDDirFromModule( + "github.com/metallb/frr-k8s", "../../go.mod", "config/crd/bases") + Expect(err).ShouldNot(HaveOccurred()) + + networkv1CRD, err := test.GetCRDDirFromModule( + "github.com/k8snetworkplumbingwg/network-attachment-definition-client", "../../go.mod", "artifacts/networks-crd.yaml") + Expect(err).ShouldNot(HaveOccurred()) + By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("..", "..", "config", "crd", "bases"), rabbitmqv2CRDs, + frrCRDs, + }, + CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + networkv1CRD, + }, }, ErrorIfCRDPathMissing: true, WebhookInstallOptions: envtest.WebhookInstallOptions{ @@ -118,6 +134,10 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = memcachedv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = k8s_networkv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = frrk8sv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme logger = ctrl.Log.WithName("---Test---") @@ -194,6 +214,13 @@ var _ = BeforeSuite(func() { }).SetupWithManager(context.Background(), k8sManager) Expect(err).ToNot(HaveOccurred()) + err = (&network_ctrl.BGPConfigurationReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(context.Background(), k8sManager) + Expect(err).ToNot(HaveOccurred()) + err = (&rabbitmq_ctrl.TransportURLReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(),