diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf index 8111f6b8a61..995a3f36bd2 100644 --- a/build/charts/antrea/conf/antrea-agent.conf +++ b/build/charts/antrea/conf/antrea-agent.conf @@ -50,6 +50,9 @@ featureGates: # Enable mirroring or redirecting the traffic Pods send or receive. {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "TrafficControl" "default" false) }} +# Enable running agent on an unmanaged VM/BM. +{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "ExternalNode" "default" false) }} + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: {{ .Values.ovs.bridgeName | quote }} diff --git a/build/charts/antrea/conf/antrea-controller.conf b/build/charts/antrea/conf/antrea-controller.conf index f54bf5ce864..499a44637c3 100644 --- a/build/charts/antrea/conf/antrea-controller.conf +++ b/build/charts/antrea/conf/antrea-controller.conf @@ -25,6 +25,9 @@ featureGates: # Enable managing external IPs of Services of LoadBalancer type. {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "ServiceExternalIP" "default" false) }} +# Enable managing ExternalNode for unmanaged VM/BM. +{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "ExternalNode" "default" false) }} + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. diff --git a/build/charts/antrea/templates/controller/clusterrole.yaml b/build/charts/antrea/templates/controller/clusterrole.yaml index 2a5f043af35..e9a86b199c2 100644 --- a/build/charts/antrea/templates/controller/clusterrole.yaml +++ b/build/charts/antrea/templates/controller/clusterrole.yaml @@ -234,6 +234,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: diff --git a/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml b/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml index 6cd661c3eeb..973f6da6d5f 100644 --- a/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml +++ b/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml @@ -109,4 +109,4 @@ webhooks: scope: "Cluster" admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None - timeoutSeconds: 5 + timeoutSeconds: 5 \ No newline at end of file diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml index e4499e5c1ea..d511f7fc4f1 100644 --- a/build/yamls/antrea-aks.yml +++ b/build/yamls/antrea-aks.yml @@ -108,6 +108,9 @@ data: # Enable mirroring or redirecting the traffic Pods send or receive. # TrafficControl: false + # Enable running agent on an unmanaged VM/BM. + # ExternalNode: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -358,6 +361,9 @@ data: # Enable managing external IPs of Services of LoadBalancer type. # ServiceExternalIP: false + # Enable managing ExternalNode for unmanaged VM/BM. + # ExternalNode: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -3372,6 +3378,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: @@ -3557,7 +3571,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 0cc20edc3fc882f0ea9bd3450fbab504858feeff47e1d3f09d8f6ebacd741dbe + checksum/config: fd3db69eb5db9d8492480b297952e3577e1063d608dd3e9d904a84c9c7466af7 labels: app: antrea component: antrea-agent @@ -3797,7 +3811,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 0cc20edc3fc882f0ea9bd3450fbab504858feeff47e1d3f09d8f6ebacd741dbe + checksum/config: fd3db69eb5db9d8492480b297952e3577e1063d608dd3e9d904a84c9c7466af7 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml index 8feb3cca93f..d40c47c3f7c 100644 --- a/build/yamls/antrea-eks.yml +++ b/build/yamls/antrea-eks.yml @@ -108,6 +108,9 @@ data: # Enable mirroring or redirecting the traffic Pods send or receive. # TrafficControl: false + # Enable running agent on an unmanaged VM/BM. + # ExternalNode: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -358,6 +361,9 @@ data: # Enable managing external IPs of Services of LoadBalancer type. # ServiceExternalIP: false + # Enable managing ExternalNode for unmanaged VM/BM. + # ExternalNode: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -3372,6 +3378,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: @@ -3557,7 +3571,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 0cc20edc3fc882f0ea9bd3450fbab504858feeff47e1d3f09d8f6ebacd741dbe + checksum/config: fd3db69eb5db9d8492480b297952e3577e1063d608dd3e9d904a84c9c7466af7 labels: app: antrea component: antrea-agent @@ -3799,7 +3813,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 0cc20edc3fc882f0ea9bd3450fbab504858feeff47e1d3f09d8f6ebacd741dbe + checksum/config: fd3db69eb5db9d8492480b297952e3577e1063d608dd3e9d904a84c9c7466af7 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml index 327a9713348..02f4b02b937 100644 --- a/build/yamls/antrea-gke.yml +++ b/build/yamls/antrea-gke.yml @@ -108,6 +108,9 @@ data: # Enable mirroring or redirecting the traffic Pods send or receive. # TrafficControl: false + # Enable running agent on an unmanaged VM/BM. + # ExternalNode: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -358,6 +361,9 @@ data: # Enable managing external IPs of Services of LoadBalancer type. # ServiceExternalIP: false + # Enable managing ExternalNode for unmanaged VM/BM. + # ExternalNode: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -3372,6 +3378,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: @@ -3557,7 +3571,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 6b6be76fd37d8fdac7783fcd026b6f34e993630c12c339b1dafa99ba5b36cf00 + checksum/config: 44e834426bd7efecb883d82527b964b6ac50a347db2ef007bfb577cbee6da121 labels: app: antrea component: antrea-agent @@ -3797,7 +3811,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 6b6be76fd37d8fdac7783fcd026b6f34e993630c12c339b1dafa99ba5b36cf00 + checksum/config: 44e834426bd7efecb883d82527b964b6ac50a347db2ef007bfb577cbee6da121 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml index ec70844dfdc..4f1431eef76 100644 --- a/build/yamls/antrea-ipsec.yml +++ b/build/yamls/antrea-ipsec.yml @@ -121,6 +121,9 @@ data: # Enable mirroring or redirecting the traffic Pods send or receive. # TrafficControl: false + # Enable running agent on an unmanaged VM/BM. + # ExternalNode: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -371,6 +374,9 @@ data: # Enable managing external IPs of Services of LoadBalancer type. # ServiceExternalIP: false + # Enable managing ExternalNode for unmanaged VM/BM. + # ExternalNode: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -3385,6 +3391,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: @@ -3570,7 +3584,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: d289c621cfdc7aee9e8320c0398e76f302591b0adc12156d470320ee9839c073 + checksum/config: effea4fa7935691aaea3fa102062366856076965a51577c751c11b8653230a77 checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4 labels: app: antrea @@ -3846,7 +3860,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: d289c621cfdc7aee9e8320c0398e76f302591b0adc12156d470320ee9839c073 + checksum/config: effea4fa7935691aaea3fa102062366856076965a51577c751c11b8653230a77 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml index 9fc57aceb82..fb60f5b759f 100644 --- a/build/yamls/antrea.yml +++ b/build/yamls/antrea.yml @@ -108,6 +108,9 @@ data: # Enable mirroring or redirecting the traffic Pods send or receive. # TrafficControl: false + # Enable running agent on an unmanaged VM/BM. + # ExternalNode: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -358,6 +361,9 @@ data: # Enable managing external IPs of Services of LoadBalancer type. # ServiceExternalIP: false + # Enable managing ExternalNode for unmanaged VM/BM. + # ExternalNode: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -3372,6 +3378,14 @@ rules: - ippools/status verbs: - update + - apiGroups: + - crd.antrea.io + resources: + - externalnodes + verbs: + - get + - watch + - list - apiGroups: - apps resources: @@ -3557,7 +3571,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 976e8c918d8c411df17238dd333a51f9adfdfafe2d6d480d7652f16be02fff3c + checksum/config: 854c66e484311ec7df5096b24c238923750a7713d83b3aa4332cd24297e22936 labels: app: antrea component: antrea-agent @@ -3797,7 +3811,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 976e8c918d8c411df17238dd333a51f9adfdfafe2d6d480d7652f16be02fff3c + checksum/config: 854c66e484311ec7df5096b24c238923750a7713d83b3aa4332cd24297e22936 labels: app: antrea component: antrea-controller diff --git a/cmd/antrea-controller/controller.go b/cmd/antrea-controller/controller.go index 25b75bca3bb..1d5afab27da 100644 --- a/cmd/antrea-controller/controller.go +++ b/cmd/antrea-controller/controller.go @@ -42,6 +42,7 @@ import ( "antrea.io/antrea/pkg/controller/egress" egressstore "antrea.io/antrea/pkg/controller/egress/store" "antrea.io/antrea/pkg/controller/externalippool" + "antrea.io/antrea/pkg/controller/externalnode" "antrea.io/antrea/pkg/controller/grouping" antreaipam "antrea.io/antrea/pkg/controller/ipam" "antrea.io/antrea/pkg/controller/metrics" @@ -124,6 +125,7 @@ func run(o *Options) error { cgInformer := crdInformerFactory.Crd().V1alpha3().ClusterGroups() egressInformer := crdInformerFactory.Crd().V1alpha2().Egresses() externalIPPoolInformer := crdInformerFactory.Crd().V1alpha2().ExternalIPPools() + externalNodeInformer := crdInformerFactory.Crd().V1alpha1().ExternalNodes() clusterIdentityAllocator := clusteridentity.NewClusterIdentityAllocator( env.GetAntreaNamespace(), @@ -156,6 +158,11 @@ func run(o *Options) error { networkPolicyStore, groupStore) + var externalNodeController *externalnode.ExternalNodeController + if features.DefaultFeatureGate.Enabled(features.ExternalNode) { + externalNodeController = externalnode.NewExternalNodeController(crdClient, externalNodeInformer, eeInformer) + } + var networkPolicyStatusController *networkpolicy.StatusController if features.DefaultFeatureGate.Enabled(features.AntreaPolicy) { networkPolicyStatusController = networkpolicy.NewStatusController(crdClient, networkPolicyStore, cnpInformer, anpInformer) @@ -315,6 +322,10 @@ func run(o *Options) error { go externalIPController.Run(stopCh) } + if features.DefaultFeatureGate.Enabled(features.ExternalNode) { + go externalNodeController.Run(stopCh) + } + if antreaIPAMController != nil { go antreaIPAMController.Run(stopCh) } diff --git a/pkg/controller/externalnode/controller.go b/pkg/controller/externalnode/controller.go new file mode 100644 index 00000000000..0d55df51639 --- /dev/null +++ b/pkg/controller/externalnode/controller.go @@ -0,0 +1,375 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package externalnode + +import ( + "context" + "reflect" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "antrea.io/antrea/pkg/apis/crd/v1alpha1" + "antrea.io/antrea/pkg/apis/crd/v1alpha2" + clientset "antrea.io/antrea/pkg/client/clientset/versioned" + externalnodeinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha1" + externalentityinformers "antrea.io/antrea/pkg/client/informers/externalversions/crd/v1alpha2" + externalnodelisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha1" + externalentitylisters "antrea.io/antrea/pkg/client/listers/crd/v1alpha2" + "antrea.io/antrea/pkg/util/externalnode" + "antrea.io/antrea/pkg/util/k8s" +) + +const ( + controllerName = "ExternalNodeController" + // How long to wait before retrying the processing of an ExternalNode change. + minRetryDelay = 5 * time.Second + maxRetryDelay = 300 * time.Second + // Default number of workers processing ExternalNode changes. + defaultWorkers = 4 + // Set resyncPeriod to 0 to disable resyncing. + resyncPeriod time.Duration = 0 +) + +var ( + keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc + splitKeyFunc = cache.SplitMetaNamespaceKey +) + +type ExternalNodeController struct { + crdClient clientset.Interface + + externalNodeInformer externalnodeinformers.ExternalNodeInformer + externalNodeLister externalnodelisters.ExternalNodeLister + externalNodeListerSynced cache.InformerSynced + + externalEntityInformer externalentityinformers.ExternalEntityInformer + externalEntityLister externalentitylisters.ExternalEntityLister + externalEntityListerSynced cache.InformerSynced + + syncedExternalNode cache.Store + // queue maintains the ExternalNode objects that need to be synced. + queue workqueue.RateLimitingInterface +} + +func NewExternalNodeController(crdClient clientset.Interface, externalNodeInformer externalnodeinformers.ExternalNodeInformer, + externalEntityInformer externalentityinformers.ExternalEntityInformer) *ExternalNodeController { + c := &ExternalNodeController{ + crdClient: crdClient, + + externalNodeInformer: externalNodeInformer, + externalNodeLister: externalNodeInformer.Lister(), + externalNodeListerSynced: externalNodeInformer.Informer().HasSynced, + + externalEntityInformer: externalEntityInformer, + externalEntityLister: externalEntityInformer.Lister(), + externalEntityListerSynced: externalEntityInformer.Informer().HasSynced, + + syncedExternalNode: cache.NewStore(keyFunc), + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "externalnode"), + } + c.externalNodeInformer.Informer().AddEventHandlerWithResyncPeriod( + cache.ResourceEventHandlerFuncs{ + AddFunc: c.enqueueExternalNodeAdd, + UpdateFunc: c.enqueueExternalNodeUpdate, + DeleteFunc: c.enqueueExternalNodeDelete, + }, + resyncPeriod) + return c +} + +func (c *ExternalNodeController) enqueueExternalNodeAdd(obj interface{}) { + en := obj.(*v1alpha1.ExternalNode) + key, _ := keyFunc(en) + c.queue.Add(key) + klog.InfoS("Enqueued ExternalNode ADD event", "ExternalNode", klog.KObj(en)) +} + +func (c *ExternalNodeController) enqueueExternalNodeUpdate(oldObj interface{}, newObj interface{}) { + en := newObj.(*v1alpha1.ExternalNode) + key, _ := keyFunc(en) + c.queue.Add(key) + klog.InfoS("Enqueued ExternalNode UPDATE event", "ExternalNode", klog.KObj(en)) +} + +func (c *ExternalNodeController) enqueueExternalNodeDelete(obj interface{}) { + en := obj.(*v1alpha1.ExternalNode) + key, _ := keyFunc(en) + c.queue.Add(key) + klog.InfoS("Enqueued ExternalNode DELETE event", "ExternalNode", klog.KObj(en)) +} + +// Run will create defaultWorkers workers (goroutines) which will process the ExternalEntity events from the work queue. +func (c *ExternalNodeController) Run(stopCh <-chan struct{}) { + defer c.queue.ShutDown() + + klog.InfoS("Starting", "controllerName", controllerName) + defer klog.InfoS("Shutting down", "controllerName", controllerName) + + if !cache.WaitForNamedCacheSync(controllerName, stopCh, c.externalNodeListerSynced, c.externalEntityListerSynced) { + return + } + if err := c.reconcileExternalNodes(); err != nil { + klog.ErrorS(err, "Failed to reconcile ExternalNodes") + return + } + + for i := 0; i < defaultWorkers; i++ { + go wait.Until(c.worker, time.Second, stopCh) + } + <-stopCh +} + +// reconcileExternalNodes reconciles all the existing ExternalNodes and cleans up the stale ExternalEntities. +func (c *ExternalNodeController) reconcileExternalNodes() error { + externalNodes, err := c.externalNodeLister.List(labels.Everything()) + if err != nil { + return err + } + enUIDEENameMap := make(map[types.UID]string) + for _, en := range externalNodes { + if err = c.addExternalNode(en); err != nil { + return err + } + eeName := externalnode.GenExternalEntityName(en) + enUIDEENameMap[en.UID] = eeName + } + externalEntities, err := c.externalEntityLister.List(labels.Everything()) + if err != nil { + return err + } + for _, ee := range externalEntities { + if (len(ee.OwnerReferences) > 0) && (ee.OwnerReferences[0].Kind == "ExternalNode") { + // Clean up stale ExternalEntities when ExternalNode no longer exists or + // when interface[0] name is changed. + if eeName, ok := enUIDEENameMap[ee.OwnerReferences[0].UID]; !ok || (ok && (eeName != ee.Name)) { + err = c.crdClient.CrdV1alpha2().ExternalEntities(ee.Namespace).Delete(context.TODO(), ee.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + } + } + } + return nil +} + +// worker is a long-running function that will continually call the processNextWorkItem function in +// order to read and process a message on the work queue. +func (c *ExternalNodeController) worker() { + for c.processNextWorkItem() { + } +} + +func (c *ExternalNodeController) processNextWorkItem() bool { + obj, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(obj) + + if key, ok := obj.(string); !ok { + c.queue.Forget(obj) + klog.Errorf("Expected string in ExternalNode work queue but got %#v", obj) + return true + } else if err := c.syncExternalNode(key); err == nil { + // If no error occurs we Forget this item so it does not get queued again until + // another change happens. + c.queue.Forget(key) + } else { + // Put the item back on the workqueue to handle any transient errors. + c.queue.AddRateLimited(key) + klog.ErrorS(err, "Error syncing ExternalNode", "ExternalNode", key) + } + return true +} + +func (c *ExternalNodeController) syncExternalNode(key string) error { + namespace, name, err := splitKeyFunc(key) + if err != nil { + // This err should not occur. + return err + } + en, err := c.externalNodeLister.ExternalNodes(namespace).Get(name) + if errors.IsNotFound(err) { + return c.deleteExternalNode(namespace, name) + } + + preEn, exists, _ := c.syncedExternalNode.GetByKey(key) + if !exists { + return c.addExternalNode(en) + } else { + return c.updateExternalNode(preEn.(*v1alpha1.ExternalNode), en) + } +} + +// addExternalNode creates ExternalEntity for each NetworkInterface in the ExternalNode. +// Only one interface is supported for now and there should be one ExternalEntity generated for one ExternalNode. +func (c *ExternalNodeController) addExternalNode(en *v1alpha1.ExternalNode) error { + eeName := externalnode.GenExternalEntityName(en) + if eeName == "" { + klog.InfoS("Interfaces are empty for ExternalNode", "ExternalNode", klog.KObj(en)) + return nil + } + ee := genExternalEntity(eeName, en) + err := c.createExternalEntity(ee) + if err != nil { + return err + } + c.syncedExternalNode.Add(en) + return nil +} + +func (c *ExternalNodeController) createExternalEntity(ee *v1alpha2.ExternalEntity) error { + _, err := c.crdClient.CrdV1alpha2().ExternalEntities(ee.Namespace).Create(context.TODO(), ee, metav1.CreateOptions{}) + if errors.IsAlreadyExists(err) { + klog.InfoS("Update ExternalEntity instead of creating it as it already exists", "ExternalEntity", klog.KObj(ee)) + return c.updateExternalEntity(ee) + } + return err +} + +func (c *ExternalNodeController) updateExternalNode(preEn *v1alpha1.ExternalNode, curEn *v1alpha1.ExternalNode) error { + if reflect.DeepEqual(preEn.Spec.Interfaces, curEn.Spec.Interfaces) && reflect.DeepEqual(preEn.Labels, curEn.Labels) { + return nil + } + // Delete the previous ExternalEntity and create a new one if the name of the generated ExternalEntity is changed. + // Otherwise, update the ExternalEntity. + preEEName := externalnode.GenExternalEntityName(preEn) + curEEName := externalnode.GenExternalEntityName(curEn) + if preEEName == "" && curEEName == "" { + return nil + } else if preEEName != curEEName { + if preEEName != "" { + err := c.deleteExternalEntity(preEn.Namespace, preEEName) + if err != nil { + return err + } + } + if curEEName != "" { + curEE := genExternalEntity(curEEName, curEn) + err := c.createExternalEntity(curEE) + if err != nil { + return err + } + } + } else { + preIPs := sets.NewString(preEn.Spec.Interfaces[0].IPs...) + curIPs := sets.NewString(curEn.Spec.Interfaces[0].IPs...) + if (!reflect.DeepEqual(preEn.Labels, curEn.Labels)) || (!preIPs.Equal(curIPs)) { + eeName := externalnode.GenExternalEntityName(curEn) + updatedEE := genExternalEntity(eeName, curEn) + err := c.updateExternalEntity(updatedEE) + if err != nil { + return err + } + } + } + c.syncedExternalNode.Update(curEn) + return nil +} +func (c *ExternalNodeController) updateExternalEntity(ee *v1alpha2.ExternalEntity) error { + // resourceVersion must be specified for update operation, + // so it gets the existing ExternalEntity and modifies the changed fields. + existingEE, _ := c.crdClient.CrdV1alpha2().ExternalEntities(ee.Namespace).Get(context.TODO(), ee.Name, metav1.GetOptions{}) + isChanged := false + if !reflect.DeepEqual(existingEE.Spec, ee.Spec) { + existingEE.Spec = ee.Spec + isChanged = true + } + if !reflect.DeepEqual(existingEE.Labels, ee.Labels) { + existingEE.Labels = ee.Labels + isChanged = true + } + if isChanged { + _, err := c.crdClient.CrdV1alpha2().ExternalEntities(ee.Namespace).Update(context.TODO(), existingEE, metav1.UpdateOptions{}) + if err != nil { + return err + } + } + return nil +} + +func (c *ExternalNodeController) deleteExternalNode(namespace string, name string) error { + obj, exists, _ := c.syncedExternalNode.GetByKey(k8s.NamespacedName(namespace, name)) + if !exists { + klog.InfoS("Skipping deleting ExternalNode as it does not exist", "enName", namespace, "enNamespace", namespace) + return nil + } + en := obj.(*v1alpha1.ExternalNode) + eeName := externalnode.GenExternalEntityName(en) + if eeName == "" { + klog.InfoS("Interfaces are empty for ExternalNode", "ExternalNode", klog.KObj(en)) + return nil + } + err := c.deleteExternalEntity(namespace, eeName) + if err != nil { + return err + } + c.syncedExternalNode.Delete(en) + return nil +} + +func (c *ExternalNodeController) deleteExternalEntity(namespace string, name string) error { + err := c.crdClient.CrdV1alpha2().ExternalEntities(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + if errors.IsNotFound(err) { + klog.InfoS("Skipping deleting ExternalEntity as it is not found", "eeName", name, "eeNamespace", namespace) + return nil + } + return err +} + +func genExternalEntity(eeName string, en *v1alpha1.ExternalNode) *v1alpha2.ExternalEntity { + ownerRef := &metav1.OwnerReference{ + APIVersion: "v1alpha1", + Kind: "ExternalNode", + Name: en.GetName(), + UID: en.GetUID(), + } + endpoints := make([]v1alpha2.Endpoint, 0) + // Generate one/multiple endpoint(s) if one/multiple IP(s) are specified for interface[0]. + // Generate one endpoint with name only if IP is not specified for interface[0]. + if len(en.Spec.Interfaces[0].IPs) > 0 { + for _, ip := range en.Spec.Interfaces[0].IPs { + endpoints = append(endpoints, v1alpha2.Endpoint{ + IP: ip, + Name: en.Spec.Interfaces[0].Name, + }) + } + } else { + klog.InfoS("Cannot generate endpoints as Interfaces[0].IPs is empty", "ExternalNode", klog.KObj(en)) + } + + ee := &v1alpha2.ExternalEntity{ + ObjectMeta: metav1.ObjectMeta{ + Name: eeName, + Namespace: en.Namespace, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + Labels: en.Labels, + }, + Spec: v1alpha2.ExternalEntitySpec{ + Endpoints: endpoints, + ExternalNode: en.Name, + }, + } + return ee +} diff --git a/pkg/util/externalnode/externalnode.go b/pkg/util/externalnode/externalnode.go new file mode 100644 index 00000000000..b1b8077a57f --- /dev/null +++ b/pkg/util/externalnode/externalnode.go @@ -0,0 +1,31 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package externalnode + +import "antrea.io/antrea/pkg/apis/crd/v1alpha1" + +func GenExternalEntityName(externalNode *v1alpha1.ExternalNode) string { + if len(externalNode.Spec.Interfaces) == 0 { + return "" + } + // Only one network interface is supported now. + // Other interfaces except interfaces[0] will be ignored if there are more than one interfaces. + ifName := externalNode.Spec.Interfaces[0].Name + if ifName == "" { + return externalNode.Name + } else { + return externalNode.Name + "-" + ifName + } +}