{"action":"created","comment":{"url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/comments/240919127","pull_request_review_id":184058127,"id":240919127,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MDkxOTEyNw==","diff_hunk":"@@ -0,0 +1,1059 @@\n+/*\n+Copyright 2018 The Kubernetes Authors.\n+\n+Licensed under the Apache License, Version 2.0 (the \"License\");\n+you may not use this file except in compliance with the License.\n+You may obtain a copy of the License at\n+\n+    http://www.apache.org/licenses/LICENSE-2.0\n+\n+Unless required by applicable law or agreed to in writing, software\n+distributed under the License is distributed on an \"AS IS\" BASIS,\n+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+See the License for the specific language governing permissions and\n+limitations under the License.\n+*/\n+\n+package connectionagent\n+\n+import (\n+\t\"bytes\"\n+\t\"fmt\"\n+\tgonet \"net\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\t\"github.com/golang/glog\"\n+\n+\tk8serrors \"k8s.io/apimachinery/pkg/api/errors\"\n+\tk8smetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n+\tk8slabels \"k8s.io/apimachinery/pkg/labels\"\n+\tk8sruntime \"k8s.io/apimachinery/pkg/runtime\"\n+\tk8stypes \"k8s.io/apimachinery/pkg/types\"\n+\tk8sutilruntime \"k8s.io/apimachinery/pkg/util/runtime\"\n+\tk8swait \"k8s.io/apimachinery/pkg/util/wait\"\n+\tk8scache \"k8s.io/client-go/tools/cache\"\n+\tk8sworkqueue \"k8s.io/client-go/util/workqueue\"\n+\n+\tnetv1a1 \"k8s.io/examples/staging/kos/pkg/apis/network/v1alpha1\"\n+\tkosclientset \"k8s.io/examples/staging/kos/pkg/client/clientset/versioned\"\n+\tnetvifc1a1 \"k8s.io/examples/staging/kos/pkg/client/clientset/versioned/typed/network/v1alpha1\"\n+\tkosinformers \"k8s.io/examples/staging/kos/pkg/client/informers/externalversions\"\n+\tkosinternalifcs \"k8s.io/examples/staging/kos/pkg/client/informers/externalversions/internalinterfaces\"\n+\tkosinformersv1a1 \"k8s.io/examples/staging/kos/pkg/client/informers/externalversions/network/v1alpha1\"\n+\tkoslisterv1a1 \"k8s.io/examples/staging/kos/pkg/client/listers/network/v1alpha1\"\n+\tnetfabric \"k8s.io/examples/staging/kos/pkg/networkfabric\"\n+)\n+\n+const (\n+\t// Name of the indexer which computes the MAC address\n+\t// for a network attachment. Used for handling of\n+\t// pre-existing interfaces at start-up.\n+\tattMACIndexName = \"attachmentMAC\"\n+\n+\t// NetworkAttachments in network.example.com/v1alpha1\n+\t// fields names. Used to build field selectors.\n+\tattNodeFieldName   = \"spec.node\"\n+\tattIPFieldName     = \"status.ipv4\"\n+\tattHostIPFieldName = \"status.hostIP\"\n+\tattIfcFieldName    = \"status.ifcName\"\n+\tattVNIFieldName    = \"spec.vni\"\n+\n+\t// fields selector comparison operators.\n+\t// Used to build fields selectors.\n+\tequal    = \"=\"\n+\tnotEqual = \"!=\"\n+\n+\t// Name of status subresource used for patching NetworkAttachments status\n+\t// when processing deleted local Attachments.\n+\tstatusSubRes = \"status\"\n+\n+\t// resync period for Informers caches. Set\n+\t// to 0 because we don't want resyncs.\n+\tresyncPeriod = 0\n+)\n+\n+// vnState stores all the state needed for a Virtual Network for\n+// which there is at least one NetworkAttachment local to this node.\n+type vnState struct {\n+\t// remoteAttsInformer is an informer on the NetworkAttachments that are\n+\t// both: (1) in the Virtual Network the vnState represents, (2) not on\n+\t// this node. It is stopped when the last local NetworkAttachment in the\n+\t// Virtual Network associated with the vnState instance is deleted. To\n+\t// stop it, remoteAttsInformerStopCh must be closed.\n+\tremoteAttsInformer       k8scache.SharedIndexInformer\n+\tremoteAttsInformerStopCh chan struct{}\n+\n+\t// remoteAttsLister is a lister on the NetworkAttachments that are\n+\t// both: (1) in the Virtual Network the vnState represents, (2) not\n+\t// on this node. Since a Virtual Network cannot span multiple k8s API\n+\t// namespaces, it's a NamespaceLister.\n+\tremoteAttsLister koslisterv1a1.NetworkAttachmentNamespaceLister\n+\n+\t// namespace is the namespace of the Virtual Network\n+\t// associated with this vnState.\n+\tnamespace string\n+\n+\t// localAtts stores the names the local NetworkAttachments in\n+\t// the Virtual Network the vnState represents.\n+\tlocalAtts  map[string]struct{}\n+\tremoteAtts map[string]struct{}\n+}\n+\n+// attQueueRef is a queue reference to a Network Attachment. Upon\n+// dequeuing a reference, a worker goroutine can lookup for the\n+// attachment in different caches: the local Attachments cache, or\n+// one of the caches for remote attachments (one for each Virtual\n+// Network relevant to the Connection Agent). That's why attQueueRef\n+// stores a locality flag (true if lookup for the attachment must be\n+// done in the local attachments cache) and a vni, which identifies\n+// the remote attachments cache where to lookup if the locality flag\n+// is false.\n+type attQueueRef struct {\n+\tvni   uint32\n+\tlocal bool\n+\tnsn   k8stypes.NamespacedName\n+}\n+\n+// ConnectionAgent represents a K8S controller which runs on every node of the cluster and\n+// eagerly maintains up-to-date the mapping between virtual IPs and physical IPs for\n+// every relevant NetworkAttachment. A NetworkAttachment is relevant to a connection agent\n+// if: (1) it runs on the same node as the connection agent, or (2) it's part of a\n+// Virtual Network where at least one NetworkAttachment for which (1) is true exists.\n+// To achieve its goal, a connection agent receives notifications about relevant\n+// NetworkAttachments from the K8s API server through Informers, and when necessary\n+// creates/updates/deletes Network Interfaces through a low-level network interface fabric.\n+// When a new Virtual Network becomes relevant for the connection agent because of the creation\n+// of the first attachment of that Virtual Network on the same node as the connection agent,\n+// a new informer on remote NetworkAttachments in that Virtual Network is created.\n+type ConnectionAgent struct {\n+\tlocalNodeName string\n+\thostIP        gonet.IP\n+\tkcs           *kosclientset.Clientset\n+\tnetv1a1Ifc    netvifc1a1.NetworkV1alpha1Interface\n+\tqueue         k8sworkqueue.RateLimitingInterface\n+\tworkers       int\n+\tnetFabric     netfabric.Interface\n+\tstopCh        <-chan struct{}\n+\n+\tlocalAttsInformer k8scache.SharedIndexInformer\n+\tlocalAttsLister   koslisterv1a1.NetworkAttachmentLister\n+\n+\tvniToVnStateMutex sync.RWMutex\n+\tvniToVnState      map[uint32]*vnState\n+\n+\tlocalAttIfcsMutex sync.RWMutex\n+\tlocalAttIfcs      map[k8stypes.NamespacedName]netfabric.NetworkInterface\n+\n+\tremoteAttIfcsMutex sync.RWMutex\n+\tremoteAttIfcs      map[k8stypes.NamespacedName]netfabric.NetworkInterface\n+}\n+\n+func NewConnectionAgent(localNodeName string,\n+\thostIP gonet.IP,\n+\tkcs *kosclientset.Clientset,\n+\tqueue k8sworkqueue.RateLimitingInterface,\n+\tworkers int,\n+\tnetFabric netfabric.Interface) *ConnectionAgent {\n+\n+\treturn &ConnectionAgent{\n+\t\tlocalNodeName: localNodeName,\n+\t\thostIP:        hostIP,\n+\t\tkcs:           kcs,\n+\t\tnetv1a1Ifc:    kcs.NetworkV1alpha1(),\n+\t\tqueue:         queue,\n+\t\tworkers:       workers,\n+\t\tnetFabric:     netFabric,\n+\t\tvniToVnState:  make(map[uint32]*vnState),\n+\t\tlocalAttIfcs:  make(map[k8stypes.NamespacedName]netfabric.NetworkInterface),\n+\t\tremoteAttIfcs: make(map[k8stypes.NamespacedName]netfabric.NetworkInterface),\n+\t}\n+}\n+\n+func (ca *ConnectionAgent) Run(stopCh <-chan struct{}) error {\n+\tdefer k8sutilruntime.HandleCrash()\n+\tdefer ca.queue.ShutDown()\n+\n+\tca.stopCh = stopCh\n+\tca.initLocalAttsInformerAndLister()\n+\tgo ca.localAttsInformer.Run(stopCh)\n+\tglog.V(2).Infoln(\"Local NetworkAttachments Informer started\")\n+\n+\terr := ca.waitForLocalAttsCacheSync(stopCh)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tglog.V(2).Infoln(\"Local NetworkAttachments cache synced\")\n+\n+\terr = ca.handlePreExistingLocalIfcs()\n+\tif err != nil {\n+\t\t// ? An error is returned if deleting a single interface fails. This might be a bad idea: we might just want to keep running\n+\t\t// (after logging the failure) if out of 1k interfaces one could not be deleted. In fact, if the network fabric (OvS) deletion\n+\t\t// can occasionally experience transient failures and we have a lot of interfaces on the node, the fact that we are just returning\n+\t\t// an error might make it impossible to make the connection agent successfully start up. If that's the case (OvS can experience\n+\t\t// flaky failures), we should retry to delete the interface after some time, not just simply log the failure, because that would\n+\t\t// mean leaving on the node an interface the connection agent is not aware of, and this could cause troubles if later an attachment\n+\t\t// which needs an interface with the same MAC as the one which could not be deleted is created. If insterad OvS is reliable and fails\n+\t\t// only when there's something wrong with the system state, then the current solution is fine. Choose what to do after the OvS network\n+\t\t// fabric is ready.\n+\t\treturn err\n+\t}\n+\tglog.V(2).Infoln(\"Pre-existing local interfaces synced\")\n+\n+\terr = ca.handlePreExistingRemoteIfcs()\n+\tif err != nil {\n+\t\t// same considerations as for local ifcs\n+\t\treturn err\n+\t}\n+\tglog.V(2).Infoln(\"Pre-existing remote interfaces synced\")\n+\n+\tfor i := 0; i < ca.workers; i++ {\n+\t\tgo k8swait.Until(ca.processQueue, time.Second, stopCh)\n+\t}\n+\tglog.V(2).Infof(\"Launched %d workers\", ca.workers)\n+\n+\t<-stopCh\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) initLocalAttsInformerAndLister() {\n+\tlocalAttWithAnIPSelector := ca.localAttWithAnIPSelector()\n+\tglog.V(6).Info(\"Created Local NetworkAttachments fields selector: \" + localAttWithAnIPSelector)\n+\n+\tca.localAttsInformer, ca.localAttsLister = v1a1AttsCustomInformerAndLister(ca.kcs,\n+\t\tresyncPeriod,\n+\t\tfromFieldsSelectorToTweakListOptionsFunc(localAttWithAnIPSelector))\n+\n+\tca.localAttsInformer.AddIndexers(map[string]k8scache.IndexFunc{attMACIndexName: attachmentMACAddr})\n+\n+\tca.localAttsInformer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{\n+\t\tAddFunc:    ca.onLocalAttAdded,\n+\t\tUpdateFunc: ca.onLocalAttUpdated,\n+\t\tDeleteFunc: ca.onLocalAttRemoved})\n+}\n+\n+func (ca *ConnectionAgent) onLocalAttAdded(obj interface{}) {\n+\tlocalAtt := obj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment %#+v addition to local NetworkAttachments cache\", localAtt)\n+\tlocalAttRef := ca.fromAttToEnquableRef(localAtt)\n+\tca.queue.Add(localAttRef)\n+}\n+\n+func (ca *ConnectionAgent) onLocalAttUpdated(oldObj, newObj interface{}) {\n+\toldLocalAtt := oldObj.(*netv1a1.NetworkAttachment)\n+\tnewLocalAtt := newObj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment update from %#+v to %#+v in local NetworkAttachments cache\", oldLocalAtt, newLocalAtt)\n+\tnewLocalAttRef := ca.fromAttToEnquableRef(newLocalAtt)\n+\tca.queue.Add(newLocalAttRef)\n+\tif newLocalAtt.Spec.VNI != oldLocalAtt.Spec.VNI {\n+\t\toldLocalAttRef := ca.fromAttToEnquableRef(oldLocalAtt)\n+\t\tca.queue.Add(oldLocalAttRef)\n+\t}\n+}\n+\n+func (ca *ConnectionAgent) onLocalAttRemoved(obj interface{}) {\n+\tpeeledObj := peel(obj)\n+\tlocalAtt := peeledObj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment %#+v removal from local NetworkAttachments cache\", localAtt)\n+\tlocalAttRef := ca.fromAttToEnquableRef(localAtt)\n+\tca.queue.Add(localAttRef)\n+}\n+\n+func (ca *ConnectionAgent) waitForLocalAttsCacheSync(stopCh <-chan struct{}) (err error) {\n+\tif !k8scache.WaitForCacheSync(stopCh, ca.localAttsInformer.HasSynced) {\n+\t\terr = fmt.Errorf(\"Caches failed to sync\")\n+\t}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) handlePreExistingLocalIfcs() error {\n+\tallPreExistingLocalIfcs, err := ca.netFabric.ListLocalIfcs()\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"Failed initial local network interfaces list: %s\", err.Error())\n+\t}\n+\tfor _, aPreExistingLocalIfc := range allPreExistingLocalIfcs {\n+\t\tifcMac := aPreExistingLocalIfc.GuestMAC.String()\n+\t\tifcOwnerAtts, err := ca.localAttsInformer.GetIndexer().ByIndex(attMACIndexName, ifcMac)\n+\t\tif err != nil {\n+\t\t\treturn fmt.Errorf(\"Indexing local network interface with MAC %s failed: %s\", ifcMac, err.Error())\n+\t\t}\n+\t\tif len(ifcOwnerAtts) == 1 {\n+\t\t\t// If we're here there's a local attachment which should own the interface because their MAC\n+\t\t\t// addresses match. Hence we add the interface under the attachment namespaced name to the\n+\t\t\t// map associating interfaces and attachments.\n+\t\t\tifcOwnerAtt := ifcOwnerAtts[0].(*netv1a1.NetworkAttachment)\n+\t\t\tfirstLocalAttInVN := ca.updateVNStateForExistingLocalAtt(ifcOwnerAtt)\n+\t\t\tif firstLocalAttInVN {\n+\t\t\t\tglog.V(2).Infof(\"VN with ID %d became relevant: an Informer has been started\", ifcOwnerAtt.Spec.VNI)\n+\t\t\t}\n+\t\t\tca.localAttIfcs[attNSN(ifcOwnerAtt)] = aPreExistingLocalIfc\n+\t\t\tglog.V(3).Infof(\"Init: matched interface %v with local attachment %v\", aPreExistingLocalIfc, ifcOwnerAtt)\n+\t\t} else {\n+\t\t\terr = ca.netFabric.DeleteLocalIfc(aPreExistingLocalIfc)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t\tglog.V(3).Infof(\"Init: deleted orphan local interface %v\", aPreExistingLocalIfc)\n+\t\t}\n+\t}\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) handlePreExistingRemoteIfcs() error {\n+\t// Start all remote attachments caches because we need to look up remote\n+\t// attachments to decide which interfaces to keep and which to delete\n+\tallLocalAtts, err := ca.localAttsLister.List(k8slabels.Everything())\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"Failed initial local attachments list: %s\", err.Error())\n+\t}\n+\tfor _, aLocalAtt := range allLocalAtts {\n+\t\tfirstLocalAttInVN := ca.updateVNStateForExistingLocalAtt(aLocalAtt)\n+\t\tif firstLocalAttInVN {\n+\t\t\tglog.V(2).Infof(\"VN with ID %d became relevant: an Informer has been started\", aLocalAtt.Spec.VNI)\n+\t\t}\n+\t}\n+\n+\t// Read all remote ifcs, for each interface find the attachment with the same MAC in the\n+\t// cache for the remote attachments with the same VNI as the interface. If either the\n+\t// attachment or the cache are not found, delete the interface, bind it to the attachment\n+\t// otherwise.\n+\tallPreExistingRemoteIfcs, err := ca.netFabric.ListRemoteIfcs()\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"Failed initial remote network interfaces list: %s\", err.Error())\n+\t}\n+\tfor _, aPreExistingRemoteIfc := range allPreExistingRemoteIfcs {\n+\t\tvar ifcOwnerAtts []interface{}\n+\t\tifcMac := aPreExistingRemoteIfc.GuestMAC.String()\n+\t\tremoteAttsInformer, remoteAttsInformerStopCh := ca.getRemoteAttsInformerForVNI(aPreExistingRemoteIfc.VNI)\n+\t\tif remoteAttsInformer != nil {\n+\t\t\tif !remoteAttsInformer.HasSynced() &&\n+\t\t\t\t!k8scache.WaitForCacheSync(remoteAttsInformerStopCh, remoteAttsInformer.HasSynced) {\n+\t\t\t\treturn fmt.Errorf(\"Failed to sync cache of remote attachments for VNI %d\", aPreExistingRemoteIfc.VNI)\n+\t\t\t}\n+\t\t\tifcOwnerAtts, err = remoteAttsInformer.GetIndexer().ByIndex(attMACIndexName, ifcMac)\n+\t\t}\n+\t\tif len(ifcOwnerAtts) == 1 {\n+\t\t\t// If we're here a remote attachment owning the interface has been found\n+\t\t\tifcOwnerAtt := ifcOwnerAtts[0].(*netv1a1.NetworkAttachment)\n+\t\t\tremAttNSN := attNSN(ifcOwnerAtt)\n+\t\t\tca.addRemoteAttToVNState(remAttNSN.Name, ifcOwnerAtt.Spec.VNI)\n+\t\t\tca.remoteAttIfcs[remAttNSN] = aPreExistingRemoteIfc\n+\t\t\tglog.V(3).Infof(\"Init: matched interface %v with remote attachment %v\", aPreExistingRemoteIfc, ifcOwnerAtt)\n+\t\t} else {\n+\t\t\t// If we're here either there's no remote attachments cache associated with the interface vni (because there are\n+\t\t\t// no local attachments with that vni), or no remote attachment owning the interface was found. Either ways, we need\n+\t\t\t// to delete the interface.\n+\t\t\terr = ca.netFabric.DeleteRemoteIfc(aPreExistingRemoteIfc)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t\tglog.V(3).Infof(\"Init: deleted orphan remote interface %v\", aPreExistingRemoteIfc)\n+\t\t}\n+\t}\n+\n+\treturn nil\n+}\n+\n+// getRemoteAttsIndexerForVNI accesses the map with all the vnStates but it's not thread-safe\n+// because it is meant to be used only at start-up, when there's only one thread running.\n+func (ca *ConnectionAgent) getRemoteAttsInformerForVNI(vni uint32) (k8scache.SharedIndexInformer, chan struct{}) {\n+\tvnState := ca.vniToVnState[vni]\n+\tif vnState == nil {\n+\t\treturn nil, nil\n+\t}\n+\treturn vnState.remoteAttsInformer, vnState.remoteAttsInformerStopCh\n+}\n+\n+func (ca *ConnectionAgent) processQueue() {\n+\tfor {\n+\t\titem, stop := ca.queue.Get()\n+\t\tif stop {\n+\t\t\treturn\n+\t\t}\n+\t\tattRef := item.(attQueueRef)\n+\t\tca.processQueueItem(attRef)\n+\t}\n+}\n+\n+func (ca *ConnectionAgent) processQueueItem(attRef attQueueRef) {\n+\tdefer ca.queue.Done(attRef)\n+\tvar (\n+\t\terr         error\n+\t\tlocalityStr string\n+\t)\n+\tif attRef.local {\n+\t\terr = ca.processLocalAtt(attRef.nsn, attRef.vni)\n+\t\tlocalityStr = \"local\"\n+\t} else {\n+\t\terr = ca.processRemoteAtt(attRef.nsn, attRef.vni)\n+\t\tlocalityStr = \"remote\"\n+\t}\n+\trequeues := ca.queue.NumRequeues(attRef)\n+\tif err != nil {\n+\t\tglog.Warningf(\"Failed processing %s NetworkAttachment %s in VNI %d, requeuing (%d earlier requeues): %s\",\n+\t\t\tlocalityStr,\n+\t\t\tattRef.nsn,\n+\t\t\tattRef.vni,\n+\t\t\trequeues,\n+\t\t\terr.Error())\n+\t\tca.queue.AddRateLimited(attRef)\n+\t\treturn\n+\t}\n+\tglog.V(4).Infof(\"Finished %s NetworkAttachment %s in VNI %d with %d requeues\", localityStr, attRef.nsn, attRef.vni, requeues)\n+\tca.queue.Forget(attRef)\n+}\n+\n+func (ca *ConnectionAgent) processLocalAtt(nsn k8stypes.NamespacedName, vni uint32) error {\n+\tatt, err := ca.localAttsLister.NetworkAttachments(nsn.Namespace).Get(nsn.Name)\n+\n+\tif att != nil && att.Spec.VNI == vni {\n+\t\t// If we're here the attachment was found and the VNIs match, so we proceed\n+\t\t// to attempting to create a local interface.\n+\t\tfirstLocalAttInVN := ca.updateVNStateForExistingLocalAtt(att)\n+\t\tif firstLocalAttInVN {\n+\t\t\tglog.V(2).Infof(\"VN with ID %d became relevant: an Informer has been started\", att.Spec.VNI)\n+\t\t}\n+\t\tifcNameForStatus, statusIfcNameMaybeNeedsUpdate, err := ca.attemptToCreateLocalAttIfc(att)\n+\t\tupdatedAtt, statusUpdateErr := ca.setIfcNameAndHostIPInLocalAttStatusIfNeeded(att, ifcNameForStatus, statusIfcNameMaybeNeedsUpdate)\n+\t\tif updatedAtt != nil {\n+\t\t\tglog.V(2).Infof(\"Updated local att %s status with ifc name %s and host IP %s\", nsn,\n+\t\t\t\tupdatedAtt.Status.IfcName,\n+\t\t\t\tupdatedAtt.Status.HostIP,\n+\t\t\t)\n+\t\t}\n+\t\treturn aggregateErrors(\"\\n\\t\", err, statusUpdateErr)\n+\t}\n+\n+\tif (err != nil && k8serrors.IsNotFound(err)) || (att != nil && att.Spec.VNI != vni) {\n+\t\t// If we're here either the attachment has been deleted or its VNI is different wrt to the one\n+\t\t// in the reference. In both cases: (1) The vnState for the reference vni should be updated to\n+\t\t// reflect the fact that the local attachment is no longer there and (2) If an old interface (with\n+\t\t// the same VNI as the one in the reference) exists it should be deleted.\n+\t\tlastLocalAttInVN := ca.updateVNStateAfterLocalAttDeletion(nsn.Name, vni)\n+\t\tif lastLocalAttInVN {\n+\t\t\tglog.V(2).Infof(\"VN with ID %d became irrelevant: %s\",\n+\t\t\t\tvni,\n+\t\t\t\t\"its Informer has been stopped and references to remote atts in that VN have been enqueued\")\n+\t\t}\n+\t\tdeletedIfcName, ifcDeletionErr := ca.attemptToDeleteLocalAttIfc(nsn, vni)\n+\t\tif err != nil && k8serrors.IsNotFound(err) && ifcDeletionErr == nil {\n+\t\t\t// If we're here the attachment has been deleted and if its interface (associated with the reference vni)\n+\t\t\t// has been deleted as well. We do a best effort attempt to clear host IP and ifc name from deleted attachment status,\n+\t\t\t// no retries because the update might fail for a lot of legitimate reasons (e.g. the attachment has been deleted from\n+\t\t\t// etcd, the status has already been updated to a new value by the CA on the new node of the attachment, etc...). Ideally we\n+\t\t\t// would retry depending on the returned error, but it's a overhead and clearing the status is not that important.\n+\t\t\terr = ca.clearHostIPAndIfcNameFromLocalAttStatus(nsn, deletedIfcName)\n+\t\t\tif err == nil {\n+\t\t\t\tglog.V(3).Infof(\"Cleared local att %s status after deletion from local atts cache\", nsn)\n+\t\t\t} else {\n+\t\t\t\tglog.V(3).Infof(\"Could not clear local att %s status after deletion from local atts cache: %s\", nsn, err.Error())\n+\t\t\t}\n+\t\t}\n+\t\treturn ifcDeletionErr\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) processRemoteAtt(nsn k8stypes.NamespacedName, vni uint32) error {\n+\tvar (\n+\t\tatt *netv1a1.NetworkAttachment\n+\t\terr error\n+\t)\n+\tremoteAttsLister := ca.getRemoteAttListerForVNI(vni)\n+\tif remoteAttsLister != nil {\n+\t\tatt, err = remoteAttsLister.Get(nsn.Name)\n+\t}\n+\n+\tif remoteAttsLister == nil || (err != nil && k8serrors.IsNotFound(err)) {\n+\t\t// If we're here either the lister where to lookup the attachment was not found\n+\t\t// because the last local attachment in the same VN as this remote att was deleted,\n+\t\t// or this remote attachment was deleted. If there's an interface for this remote\n+\t\t// attachment with its vni it's should be deleted, and if the vnState for this attachment\n+\t\t// exists it should be removed from it.\n+\t\tca.removeRemoteAttFromVNState(nsn.Name, vni)\n+\t\treturn ca.attemptToDeleteRemoteAttIfc(nsn, vni)\n+\t}\n+\n+\t// If we're here the attachment has been found. Add it to its vnState and create its\n+\t// Interface if possible. It is \"possible\" if: (1) There's no local interface associated\n+\t// with the attachment and (2) either there's no remote interface associated with the\n+\t// attachment, or there's one with the same vni as the one of the attachment.\n+\tadded := ca.addRemoteAttToVNState(nsn.Name, vni)\n+\tif added {\n+\t\treturn ca.attemptToCreateRemoteAttIfc(att)\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) attemptToCreateLocalAttIfc(att *netv1a1.NetworkAttachment) (ifcForAttStatus string,\n+\tifcForAttStatusValid bool,\n+\terr error) {\n+\n+\tvni := att.Spec.VNI\n+\thostIP := ca.hostIP\n+\tguestIP := gonet.ParseIP(att.Status.IPv4)\n+\tguestMAC := generateMACAddr(vni, guestIP)\n+\tnewIfc := netfabric.NetworkInterface{\n+\t\tName:     generateIfcName(guestMAC),\n+\t\tVNI:      vni,\n+\t\tGuestMAC: guestMAC,\n+\t\tGuestIP:  guestIP,\n+\t\tHostIP:   hostIP,\n+\t}\n+\n+\tattNSN := attNSN(att)\n+\toldIfc, oldIfcExists, newIfcSet := ca.testAndSwapLocalAttIfcIfVNIsMatch(attNSN, newIfc)\n+\tif !newIfcSet {\n+\t\terr = fmt.Errorf(\"Attachment %s: found interface with vni %d, attempting to create local one with vni %d. Dropping attempt\",\n+\t\t\tattNSN,\n+\t\t\toldIfc.VNI,\n+\t\t\tvni,\n+\t\t)\n+\t\treturn\n+\t}\n+\n+\t_, attHasRemoteIfc := ca.getRemoteAttIfc(attNSN)\n+\tif attHasRemoteIfc {\n+\t\tif oldIfcExists {\n+\t\t\tca.testAndSwapLocalAttIfcIfVNIsMatch(attNSN, oldIfc)\n+\t\t} else {\n+\t\t\tca.removeLocalAttIfc(attNSN)\n+\t\t}\n+\t\terr = fmt.Errorf(\"Attachment %s: Attempting to create local interface, found a remote one. Dropping attempt\", attNSN)\n+\t\treturn\n+\t}\n+\n+\tnewIfcNeedsToBeCreated := !oldIfcExists || ifcsDiffer(oldIfc, newIfc)\n+\tif !newIfcNeedsToBeCreated {\n+\t\t// If we're here the old ifc exists and it's equal to the new one\n+\t\tifcForAttStatus = oldIfc.Name\n+\t\tifcForAttStatusValid = true\n+\t\treturn\n+\t}\n+\n+\tif oldIfcExists {\n+\t\t// If we're here there's an old interface which differs from the new one.\n+\t\t// We delete the old interface before creating a new one.\n+\t\terr = ca.netFabric.DeleteLocalIfc(oldIfc)\n+\t\tif err != nil {\n+\t\t\tca.testAndSwapLocalAttIfcIfVNIsMatch(attNSN, oldIfc)\n+\t\t\treturn\n+\t\t}\n+\t}\n+\terr = ca.netFabric.CreateLocalIfc(newIfc)\n+\tif err != nil {\n+\t\tca.removeLocalAttIfc(attNSN)\n+\t\treturn\n+\t}\n+\tifcForAttStatus = newIfc.Name\n+\tifcForAttStatusValid = true\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) attemptToCreateRemoteAttIfc(att *netv1a1.NetworkAttachment) error {\n+\tvni := att.Spec.VNI\n+\thostIP := gonet.ParseIP(att.Status.HostIP)\n+\tguestIP := gonet.ParseIP(att.Status.IPv4)\n+\tguestMAC := generateMACAddr(vni, guestIP)\n+\tnewIfc := netfabric.NetworkInterface{\n+\t\tName:     generateIfcName(guestMAC),\n+\t\tVNI:      vni,\n+\t\tGuestMAC: guestMAC,\n+\t\tGuestIP:  guestIP,\n+\t\tHostIP:   hostIP,\n+\t}\n+\n+\tattNSN := attNSN(att)\n+\toldIfc, oldIfcExists, newIfcSet := ca.testAndSwapRemoteAttIfcIfVNIsMatch(attNSN, newIfc)\n+\tif !newIfcSet {\n+\t\treturn fmt.Errorf(\"Attachment %s: found interface with vni %d, attempting to create remote one with vni %d. Dropping attempt\",\n+\t\t\tattNSN,\n+\t\t\toldIfc.VNI,\n+\t\t\tvni,\n+\t\t)\n+\t}\n+\n+\t_, attHasLocalIfc := ca.getLocalAttIfc(attNSN)\n+\tif attHasLocalIfc {\n+\t\tif oldIfcExists {\n+\t\t\tca.testAndSwapRemoteAttIfcIfVNIsMatch(attNSN, oldIfc)\n+\t\t} else {\n+\t\t\tca.removeRemoteAttIfc(attNSN)\n+\t\t}\n+\t\treturn fmt.Errorf(\"Attachment %s: Attempting to create remote interface, found a local one. Dropping attempt\",\n+\t\t\tattNSN,\n+\t\t)\n+\t}\n+\n+\tnewIfcNeedsToBeCreated := !oldIfcExists || ifcsDiffer(oldIfc, newIfc)\n+\tif !newIfcNeedsToBeCreated {\n+\t\t// If we're here the new and the old interface are equals, so we return\n+\t\treturn nil\n+\t}\n+\n+\tvar err error\n+\tif oldIfcExists {\n+\t\t// If we're here there's an old interface which differs from the new one.\n+\t\t// We delete the old interface before creating a new one.\n+\t\terr = ca.netFabric.DeleteRemoteIfc(oldIfc)\n+\t\tif err != nil {\n+\t\t\tca.testAndSwapRemoteAttIfcIfVNIsMatch(attNSN, oldIfc)\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\t// create the new interface\n+\terr = ca.netFabric.CreateRemoteIfc(newIfc)\n+\tif err != nil {\n+\t\tca.removeRemoteAttIfc(attNSN)\n+\t}\n+\treturn err\n+}\n+\n+func (ca *ConnectionAgent) attemptToDeleteLocalAttIfc(nsn k8stypes.NamespacedName, vni uint32) (string, error) {\n+\tifc, ifcFound := ca.getLocalAttIfc(nsn)\n+\tif ifcFound && ifc.VNI == vni {\n+\t\terr := ca.netFabric.DeleteLocalIfc(ifc)\n+\t\tif err == nil {\n+\t\t\tca.removeLocalAttIfc(nsn)\n+\t\t\treturn ifc.Name, nil\n+\t\t}\n+\t\treturn \"\", err\n+\t}\n+\treturn \"\", nil\n+}\n+\n+func (ca *ConnectionAgent) attemptToDeleteRemoteAttIfc(nsn k8stypes.NamespacedName, vni uint32) error {\n+\tifc, ifcFound := ca.getRemoteAttIfc(nsn)\n+\tif ifcFound && ifc.VNI == vni {\n+\t\terr := ca.netFabric.DeleteRemoteIfc(ifc)\n+\t\tif err == nil {\n+\t\t\tca.removeRemoteAttIfc(nsn)\n+\t\t}\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) updateVNStateForExistingLocalAtt(localAtt *netv1a1.NetworkAttachment) (firstInVN bool) {\n+\tvni := localAtt.Spec.VNI\n+\n+\tca.vniToVnStateMutex.Lock()\n+\tdefer ca.vniToVnStateMutex.Unlock()\n+\n+\tlocalAttVnState := ca.vniToVnState[vni]\n+\n+\tif localAttVnState == nil {\n+\t\tremoteAttsInformer, remoteAttsLister := v1a1AttsCustomNamespaceInformerAndLister(ca.kcs,\n+\t\t\tresyncPeriod,\n+\t\t\tlocalAtt.Namespace,\n+\t\t\tfromFieldsSelectorToTweakListOptionsFunc(ca.remoteAttInVNWithVirtualIPHostIPAndIfcSelector(vni)))\n+\n+\t\tremoteAttsInformer.AddIndexers(map[string]k8scache.IndexFunc{attMACIndexName: attachmentMACAddr})\n+\n+\t\tremoteAttsInformer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{\n+\t\t\tAddFunc:    ca.onRemoteAttAdded,\n+\t\t\tUpdateFunc: ca.onRemoteAttUpdated,\n+\t\t\tDeleteFunc: ca.onRemoteAttRemoved,\n+\t\t})\n+\n+\t\tremoteAttsInformerStopCh := make(chan struct{})\n+\t\tgo remoteAttsInformer.Run(aggregateTwoStopChannels(ca.stopCh, remoteAttsInformerStopCh))\n+\n+\t\tlocalAttVnState = &vnState{\n+\t\t\tremoteAttsInformer:       remoteAttsInformer,\n+\t\t\tremoteAttsInformerStopCh: remoteAttsInformerStopCh,\n+\t\t\tremoteAttsLister:         remoteAttsLister,\n+\t\t\tnamespace:                localAtt.Namespace,\n+\t\t\tlocalAtts:                make(map[string]struct{}, 1),\n+\t\t}\n+\t\tca.vniToVnState[vni] = localAttVnState\n+\t\tfirstInVN = true\n+\t}\n+\n+\tlocalAttVnState.localAtts[localAtt.Name] = struct{}{}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) addRemoteAttToVNState(remAttName string, vni uint32) (added bool) {\n+\tca.vniToVnStateMutex.Lock()\n+\tdefer ca.vniToVnStateMutex.Unlock()\n+\tvnState := ca.vniToVnState[vni]\n+\tif vnState != nil {\n+\t\tif vnState.remoteAtts == nil {\n+\t\t\tvnState.remoteAtts = make(map[string]struct{}, 1)\n+\t\t}\n+\t\tvnState.remoteAtts[remAttName] = struct{}{}\n+\t\tadded = true\n+\t}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) updateVNStateAfterLocalAttDeletion(localAttName string, vni uint32) (lastInVN bool) {\n+\tvnState := ca.removeLocalAttGetVnStateIfLast(localAttName, vni)\n+\tif vnState != nil {\n+\t\t// If we're here the attachment with name localAttName is the last local attachment in its VNI.\n+\t\tca.stopRemAttInformerAndEnqueueRemAttRefs(vnState, vni)\n+\t\tlastInVN = true\n+\t}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) removeLocalAttGetVnStateIfLast(localAttName string, vni uint32) *vnState {\n+\tca.vniToVnStateMutex.Lock()\n+\tdefer ca.vniToVnStateMutex.Unlock()\n+\tvnState := ca.vniToVnState[vni]\n+\tif vnState != nil {\n+\t\tdelete(vnState.localAtts, localAttName)\n+\t\tif len(vnState.localAtts) == 0 {\n+\t\t\tdelete(ca.vniToVnState, vni)\n+\t\t\treturn vnState\n+\t\t}\n+\t}\n+\treturn nil\n+}\n+\n+func (ca *ConnectionAgent) stopRemAttInformerAndEnqueueRemAttRefs(state *vnState, vni uint32) {\n+\tclose(state.remoteAttsInformerStopCh)\n+\tfor aRemoteAttName := range state.remoteAtts {\n+\t\tca.queue.Add(attQueueRef{\n+\t\t\tvni:   vni,\n+\t\t\tlocal: false,\n+\t\t\tnsn: k8stypes.NamespacedName{\n+\t\t\t\tNamespace: state.namespace,\n+\t\t\t\tName:      aRemoteAttName,\n+\t\t\t},\n+\t\t})\n+\t}\n+}\n+\n+func (ca *ConnectionAgent) removeRemoteAttFromVNState(remAttName string, vni uint32) {\n+\tca.vniToVnStateMutex.Lock()\n+\tdefer ca.vniToVnStateMutex.Unlock()\n+\tvnState := ca.vniToVnState[vni]\n+\tif vnState != nil {\n+\t\tdelete(vnState.remoteAtts, remAttName)\n+\t}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) getRemoteAttListerForVNI(vni uint32) koslisterv1a1.NetworkAttachmentNamespaceLister {\n+\tca.vniToVnStateMutex.RLock()\n+\tdefer ca.vniToVnStateMutex.RUnlock()\n+\tvnState := ca.vniToVnState[vni]\n+\tif vnState == nil {\n+\t\treturn nil\n+\t}\n+\treturn vnState.remoteAttsLister\n+}\n+\n+func (ca *ConnectionAgent) setIfcNameAndHostIPInLocalAttStatusIfNeeded(localAtt *netv1a1.NetworkAttachment,\n+\tifcName string,\n+\tifcNameMightNeedUpdate bool) (updatedAtt *netv1a1.NetworkAttachment, err error) {\n+\n+\tif ca.localAttNeedsStatusUpdate(localAtt, ifcName, ifcNameMightNeedUpdate) {\n+\t\tupdatedAtt, err = ca.setIfcNameAndHostIPInLocalAttStatus(localAtt, ifcName, ifcNameMightNeedUpdate)\n+\t}\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) localAttNeedsStatusUpdate(localAtt *netv1a1.NetworkAttachment,\n+\tifcName string,\n+\tifcNameMightNeedUpdate bool) bool {\n+\n+\treturn !ca.hostIP.Equal(gonet.ParseIP(localAtt.Status.HostIP)) ||\n+\t\t(ifcNameMightNeedUpdate && localAtt.Status.IfcName != ifcName)\n+}\n+\n+func (ca *ConnectionAgent) setIfcNameAndHostIPInLocalAttStatus(localAtt *netv1a1.NetworkAttachment,\n+\tifcName string,\n+\tifcNameMightNeedUpdate bool) (updatedAtt *netv1a1.NetworkAttachment, err error) {\n+\n+\tatt2 := localAtt.DeepCopy()\n+\tatt2.Status.HostIP = ca.hostIP.String()\n+\tif ifcNameMightNeedUpdate {\n+\t\tatt2.Status.IfcName = ifcName\n+\t}\n+\tupdatedAtt, err = ca.netv1a1Ifc.NetworkAttachments(att2.Namespace).Update(att2)\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) clearHostIPAndIfcNameFromLocalAttStatus(localAttNSN k8stypes.NamespacedName, ifcName string) (err error) {\n+\tpatchData := prepareStatusPatchData(ca.hostIP, ifcName)\n+\t_, err = ca.netv1a1Ifc.NetworkAttachments(localAttNSN.Namespace).Patch(localAttNSN.Name, k8stypes.JSONPatchType, patchData, statusSubRes)\n+\treturn\n+}\n+\n+func prepareStatusPatchData(hostIP gonet.IP, ifcName string) []byte {\n+\treturn []byte(fmt.Sprintf(`[{\"op\": \"test\", \"path\": \"/status/hostIP\", \"value\": \"%s\"},\n+\t{\"op\": \"remove\", \"path\": \"/status/hostIP\"},\n+\t{\"op\": \"test\", \"path\": \"/status/ifcName\", \"value\": \"%s\"},\n+\t{\"op\": \"remove\", \"path\": \"/status/ifcName\"}]`, hostIP, ifcName))\n+}\n+\n+func ifcsDiffer(oldIfc, newIfc netfabric.NetworkInterface) bool {\n+\treturn oldIfc.VNI != newIfc.VNI ||\n+\t\toldIfc.Name != newIfc.Name ||\n+\t\t!bytes.Equal(oldIfc.GuestMAC, newIfc.GuestMAC) ||\n+\t\t!oldIfc.GuestIP.Equal(newIfc.GuestIP) ||\n+\t\t!oldIfc.HostIP.Equal(newIfc.HostIP)\n+}\n+\n+func (ca *ConnectionAgent) onRemoteAttAdded(obj interface{}) {\n+\tremoteAtt := obj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment %#+v addition to remote NetworkAttachments cache for VNI %d\", remoteAtt, remoteAtt.Spec.VNI)\n+\tremoteAttRef := ca.fromAttToEnquableRef(remoteAtt)\n+\tca.queue.Add(remoteAttRef)\n+}\n+\n+func (ca *ConnectionAgent) onRemoteAttUpdated(oldObj, newObj interface{}) {\n+\toldRemoteAtt := oldObj.(*netv1a1.NetworkAttachment)\n+\tnewRemoteAtt := newObj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment update from %#+v to %#+v in remote NetworkAttachments cache for VNI %d\", oldRemoteAtt, newRemoteAtt, newRemoteAtt.Spec.VNI)\n+\tnewRemoteAttRef := ca.fromAttToEnquableRef(newRemoteAtt)\n+\tca.queue.Add(newRemoteAttRef)\n+}\n+\n+func (ca *ConnectionAgent) onRemoteAttRemoved(obj interface{}) {\n+\tpeeledObj := peel(obj)\n+\tremoteAtt := peeledObj.(*netv1a1.NetworkAttachment)\n+\tglog.V(5).Infof(\"Notified of NetworkAttachment %#+v removal from remote NetworkAttachments cache for VNI %d\", remoteAtt, remoteAtt.Spec.VNI)\n+\tremoteAttRef := ca.fromAttToEnquableRef(remoteAtt)\n+\tca.queue.Add(remoteAttRef)\n+}\n+\n+func (ca *ConnectionAgent) getLocalAttIfc(ifcOwnerAttNSN k8stypes.NamespacedName) (ifc netfabric.NetworkInterface, ifcFound bool) {\n+\tca.localAttIfcsMutex.RLock()\n+\tdefer ca.localAttIfcsMutex.RUnlock()\n+\tifc, ifcFound = ca.localAttIfcs[ifcOwnerAttNSN]\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) getRemoteAttIfc(ifcOwnerAttNSN k8stypes.NamespacedName) (ifc netfabric.NetworkInterface, ifcFound bool) {\n+\tca.remoteAttIfcsMutex.RLock()\n+\tdefer ca.remoteAttIfcsMutex.RUnlock()\n+\tifc, ifcFound = ca.remoteAttIfcs[ifcOwnerAttNSN]\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) testAndSwapLocalAttIfcIfVNIsMatch(ifcOwnerAttNSN k8stypes.NamespacedName,\n+\tnewIfc netfabric.NetworkInterface) (oldIfc netfabric.NetworkInterface, oldIfcExists, newIfcSet bool) {\n+\n+\tca.localAttIfcsMutex.Lock()\n+\tdefer ca.localAttIfcsMutex.Unlock()\n+\toldIfc, oldIfcExists = ca.localAttIfcs[ifcOwnerAttNSN]\n+\tif oldIfcExists && oldIfc.VNI != newIfc.VNI {\n+\t\tnewIfcSet = false\n+\t\treturn\n+\t}\n+\tca.localAttIfcs[ifcOwnerAttNSN] = newIfc\n+\tnewIfcSet = true\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) testAndSwapRemoteAttIfcIfVNIsMatch(ifcOwnerAttNSN k8stypes.NamespacedName,\n+\tnewIfc netfabric.NetworkInterface) (oldIfc netfabric.NetworkInterface, oldIfcExists, newIfcSet bool) {\n+\n+\tca.remoteAttIfcsMutex.Lock()\n+\tdefer ca.remoteAttIfcsMutex.Unlock()\n+\toldIfc, oldIfcExists = ca.remoteAttIfcs[ifcOwnerAttNSN]\n+\tif oldIfcExists && oldIfc.VNI != newIfc.VNI {\n+\t\tnewIfcSet = false\n+\t\treturn\n+\t}\n+\tca.remoteAttIfcs[ifcOwnerAttNSN] = newIfc\n+\tnewIfcSet = true\n+\treturn\n+}\n+\n+func (ca *ConnectionAgent) removeLocalAttIfc(ifcOwnerAttNSN k8stypes.NamespacedName) {\n+\tca.localAttIfcsMutex.Lock()\n+\tdefer ca.localAttIfcsMutex.Unlock()\n+\tdelete(ca.localAttIfcs, ifcOwnerAttNSN)\n+}\n+\n+func (ca *ConnectionAgent) removeRemoteAttIfc(ifcOwnerAttNSN k8stypes.NamespacedName) {\n+\tca.remoteAttIfcsMutex.Lock()\n+\tdefer ca.remoteAttIfcsMutex.Unlock()\n+\tdelete(ca.remoteAttIfcs, ifcOwnerAttNSN)\n+}\n+\n+func (ca *ConnectionAgent) fromAttToEnquableRef(att *netv1a1.NetworkAttachment) (enquableRef attQueueRef) {\n+\tenquableRef.nsn = attNSN(att)\n+\tenquableRef.vni = att.Spec.VNI\n+\tif att.Spec.Node == ca.localNodeName {\n+\t\tenquableRef.local = true\n+\t} else {\n+\t\tenquableRef.local = false\n+\t}\n+\treturn\n+}\n+\n+// Return a string representing a field selector that matches NetworkAttachments\n+// that run on the local node and have a virtual IP.\n+func (ca *ConnectionAgent) localAttWithAnIPSelector() string {\n+\t// localAttSelector expresses the constraint that the NetworkAttachment runs on this node.\n+\tlocalAttSelector := attNodeFieldName + equal + ca.localNodeName\n+\n+\t// Express the constraint that the NetworkAttachment has a virtual IP by saying\n+\t// that the field containig the virtual IP must not be equal to the empty string.\n+\tattWithAnIPSelector := attIPFieldName + notEqual\n+\n+\t// Build a selector which is a logical AND between\n+\t// attWithAnIPSelectorString and localAttSelectorString.\n+\tallSelectors := []string{localAttSelector, attWithAnIPSelector}\n+\treturn strings.Join(allSelectors, \",\")\n+}\n+\n+// Return a string representing a field selector that matches NetworkAttachments\n+// that run on a remote node on the Virtual Network identified by the given VNI and\n+// have a virtual IP and the host IP field set.\n+func (ca *ConnectionAgent) remoteAttInVNWithVirtualIPHostIPAndIfcSelector(vni uint32) string {\n+\t// remoteAttSelector expresses the constraint that the NetworkAttachment runs on a remote node.\n+\tremoteAttSelector := attNodeFieldName + notEqual + ca.localNodeName\n+\n+\t// attWithAnIPSelector, attWithHostIPSelector and attWithIfcSelector express the constraints that the\n+\t// NetworkAttachment has the fields storing virtual IP, host IP and ifc name sets, by saying that such\n+\t// fields must not be equal to the empty string.\n+\tattWithAnIPSelector := attIPFieldName + notEqual\n+\tattWithHostIPSelector := attHostIPFieldName + notEqual\n+\tattWithIfcSelector := attIfcFieldName + notEqual\n+\n+\t// attInSpecificVNSelctor expresses the constraint that the NetworkAttachment is in the Virtual\n+\t// Network identified by vni.\n+\tattInSpecificVNSelctor := attVNIFieldName + equal + fmt.Sprint(vni)\n+\n+\t// Build and return a selector which is a logical AND between all the selectors defined above.\n+\tallSelectors := []string{remoteAttSelector, attWithAnIPSelector, attWithHostIPSelector, attWithIfcSelector, attInSpecificVNSelctor}\n+\treturn strings.Join(allSelectors, \",\")\n+}\n+\n+func fromFieldsSelectorToTweakListOptionsFunc(customFieldSelector string) kosinternalifcs.TweakListOptionsFunc {\n+\treturn func(options *k8smetav1.ListOptions) {\n+\t\t// TODO if a selector is both in options.FieldSelector and customFieldsSelector it appears twice in the\n+\t\t// resulting selector. This is not incorrect but it's redundant (and less efficient I suspect).\n+\t\t// Fix this: if a selector appears both in options.FieldSelector and customFieldSelector it must\n+\t\t// appear in the resulting selector only once.\n+\t\toptionsFieldSelector := options.FieldSelector\n+\t\tallSelectors := make([]string, 0, 2)\n+\t\tif strings.Trim(optionsFieldSelector, \" \") != \"\" {\n+\t\t\tallSelectors = append(allSelectors, optionsFieldSelector)\n+\t\t}\n+\t\tallSelectors = append(allSelectors, customFieldSelector)\n+\t\toptions.FieldSelector = strings.Join(allSelectors, \",\")\n+\t}\n+}\n+\n+func v1a1AttsCustomInformerAndLister(kcs *kosclientset.Clientset,\n+\tresyncPeriod time.Duration,\n+\ttweakListOptionsFunc kosinternalifcs.TweakListOptionsFunc) (k8scache.SharedIndexInformer, koslisterv1a1.NetworkAttachmentLister) {\n+\n+\tattv1a1Informer := createAttsv1a1Informer(kcs,\n+\t\tresyncPeriod,\n+\t\tk8smetav1.NamespaceAll,\n+\t\ttweakListOptionsFunc)\n+\treturn attv1a1Informer.Informer(), attv1a1Informer.Lister()\n+}\n+\n+func v1a1AttsCustomNamespaceInformerAndLister(kcs *kosclientset.Clientset,\n+\tresyncPeriod time.Duration,\n+\tnamespace string,\n+\ttweakListOptionsFunc kosinternalifcs.TweakListOptionsFunc) (k8scache.SharedIndexInformer, koslisterv1a1.NetworkAttachmentNamespaceLister) {\n+\n+\tattv1a1Informer := createAttsv1a1Informer(kcs,\n+\t\tresyncPeriod,\n+\t\tnamespace,\n+\t\ttweakListOptionsFunc)\n+\treturn attv1a1Informer.Informer(), attv1a1Informer.Lister().NetworkAttachments(namespace)\n+}\n+\n+func createAttsv1a1Informer(kcs *kosclientset.Clientset,\n+\tresyncPeriod time.Duration,\n+\tnamespace string,\n+\ttweakListOptionsFunc kosinternalifcs.TweakListOptionsFunc) kosinformersv1a1.NetworkAttachmentInformer {\n+\n+\tlocalAttsInformerFactory := kosinformers.NewFilteredSharedInformerFactory(kcs,\n+\t\tresyncPeriod,\n+\t\tnamespace,\n+\t\ttweakListOptionsFunc)\n+\tnetv1a1Ifc := localAttsInformerFactory.Network().V1alpha1()\n+\treturn netv1a1Ifc.NetworkAttachments()\n+}\n+\n+// attachmentMACAddr is an Index function that computes the MAC address of a NetworkAttachment\n+func attachmentMACAddr(obj interface{}) (subnets []string, err error) {\n+\tatt := obj.(*netv1a1.NetworkAttachment)\n+\treturn []string{generateMACAddr(att.Spec.VNI, gonet.ParseIP(att.Status.IPv4)).String()}, nil\n+}\n+\n+// TODO factor out attNSN in pkg controller/utils, as it is used both by\n+// the connection agent and the IPAM controller.\n+func attNSN(netAtt *netv1a1.NetworkAttachment) k8stypes.NamespacedName {\n+\treturn k8stypes.NamespacedName{Namespace: netAtt.Namespace,\n+\t\tName: netAtt.Name}\n+}\n+\n+func generateIfcName(macAddr gonet.HardwareAddr) string {\n+\treturn \"kos\" + macAddr.String()\n+}\n+\n+func generateMACAddr(vni uint32, guestIPv4 gonet.IP) gonet.HardwareAddr {\n+\tguestIPBytes := guestIPv4.To4()\n+\tmac := make([]byte, 6, 6)\n+\tmac[5] = byte(vni >> 0)\n+\tmac[4] = byte(vni >> 8)\n+\tmac[3] = byte(vni>>16) | (guestIPBytes[3] << 5)\n+\tmac[2] = (guestIPBytes[3] >> 3) | (guestIPBytes[2] << 5)\n+\tmac[1] = (guestIPBytes[2] >> 3) | (guestIPBytes[1] << 5)\n+\tmac[0] = (guestIPBytes[1] & 0xF8) | (guestIPBytes[0] & 0x04) | 2","path":"staging/kos/pkg/controllers/connectionagent/connectionagent.go","position":1010,"original_position":1010,"commit_id":"0ece9dc0193cbe355fde9afafb22c7a7f3a7ebd7","original_commit_id":"0ece9dc0193cbe355fde9afafb22c7a7f3a7ebd7","user":{"login":"MikeSpreitzer","id":14296719,"node_id":"MDQ6VXNlcjE0Mjk2NzE5","avatar_url":"https://avatars2.githubusercontent.com/u/14296719?v=4","gravatar_id":"","url":"https://api.github.com/users/MikeSpreitzer","html_url":"https://github.com/MikeSpreitzer","followers_url":"https://api.github.com/users/MikeSpreitzer/followers","following_url":"https://api.github.com/users/MikeSpreitzer/following{/other_user}","gists_url":"https://api.github.com/users/MikeSpreitzer/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeSpreitzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeSpreitzer/subscriptions","organizations_url":"https://api.github.com/users/MikeSpreitzer/orgs","repos_url":"https://api.github.com/users/MikeSpreitzer/repos","events_url":"https://api.github.com/users/MikeSpreitzer/events{/privacy}","received_events_url":"https://api.github.com/users/MikeSpreitzer/received_events","type":"User","site_admin":false},"body":"This encoding produces collisions.  For example, between 10.168.1.1 and 192.168.1.1.\r\nFor the 25th bit of address you want something like `( ( (guestIPBytes[0]&8) >> 3) + (guestIPBytes[0]&4) >> 2) ) & 1`.\r\nAlso, how about doing less shifting and putting whole bytes directly in `mac[2]` and `mac[1]`?","created_at":"2018-12-12T08:31:26Z","updated_at":"2018-12-12T08:31:26Z","html_url":"https://github.com/MikeSpreitzer/kube-examples/pull/2#discussion_r240919127","pull_request_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/comments/240919127"},"html":{"href":"https://github.com/MikeSpreitzer/kube-examples/pull/2#discussion_r240919127"},"pull_request":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2"}}},"pull_request":{"url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2","id":234312444,"node_id":"MDExOlB1bGxSZXF1ZXN0MjM0MzEyNDQ0","html_url":"https://github.com/MikeSpreitzer/kube-examples/pull/2","diff_url":"https://github.com/MikeSpreitzer/kube-examples/pull/2.diff","patch_url":"https://github.com/MikeSpreitzer/kube-examples/pull/2.patch","issue_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/2","number":2,"state":"open","locked":false,"title":"[WIP] Add connection agent","user":{"login":"matte21","id":15641246,"node_id":"MDQ6VXNlcjE1NjQxMjQ2","avatar_url":"https://avatars3.githubusercontent.com/u/15641246?v=4","gravatar_id":"","url":"https://api.github.com/users/matte21","html_url":"https://github.com/matte21","followers_url":"https://api.github.com/users/matte21/followers","following_url":"https://api.github.com/users/matte21/following{/other_user}","gists_url":"https://api.github.com/users/matte21/gists{/gist_id}","starred_url":"https://api.github.com/users/matte21/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matte21/subscriptions","organizations_url":"https://api.github.com/users/matte21/orgs","repos_url":"https://api.github.com/users/matte21/repos","events_url":"https://api.github.com/users/matte21/events{/privacy}","received_events_url":"https://api.github.com/users/matte21/received_events","type":"User","site_admin":false},"body":"WIP because I'm still reviewing the code and there are some changes requests left to implement.","created_at":"2018-11-28T15:11:39Z","updated_at":"2018-12-12T08:31:26Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3baf7930f3664e398b64a05e2c5a7ccf9ae8c23a","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2/commits","review_comments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2/comments","review_comment_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/comments{/number}","comments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/2/comments","statuses_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/statuses/0ece9dc0193cbe355fde9afafb22c7a7f3a7ebd7","head":{"label":"MikeSpreitzer:add-connection-agent","ref":"add-connection-agent","sha":"0ece9dc0193cbe355fde9afafb22c7a7f3a7ebd7","user":{"login":"MikeSpreitzer","id":14296719,"node_id":"MDQ6VXNlcjE0Mjk2NzE5","avatar_url":"https://avatars2.githubusercontent.com/u/14296719?v=4","gravatar_id":"","url":"https://api.github.com/users/MikeSpreitzer","html_url":"https://github.com/MikeSpreitzer","followers_url":"https://api.github.com/users/MikeSpreitzer/followers","following_url":"https://api.github.com/users/MikeSpreitzer/following{/other_user}","gists_url":"https://api.github.com/users/MikeSpreitzer/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeSpreitzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeSpreitzer/subscriptions","organizations_url":"https://api.github.com/users/MikeSpreitzer/orgs","repos_url":"https://api.github.com/users/MikeSpreitzer/repos","events_url":"https://api.github.com/users/MikeSpreitzer/events{/privacy}","received_events_url":"https://api.github.com/users/MikeSpreitzer/received_events","type":"User","site_admin":false},"repo":{"id":145603833,"node_id":"MDEwOlJlcG9zaXRvcnkxNDU2MDM4MzM=","name":"kube-examples","full_name":"MikeSpreitzer/kube-examples","private":false,"owner":{"login":"MikeSpreitzer","id":14296719,"node_id":"MDQ6VXNlcjE0Mjk2NzE5","avatar_url":"https://avatars2.githubusercontent.com/u/14296719?v=4","gravatar_id":"","url":"https://api.github.com/users/MikeSpreitzer","html_url":"https://github.com/MikeSpreitzer","followers_url":"https://api.github.com/users/MikeSpreitzer/followers","following_url":"https://api.github.com/users/MikeSpreitzer/following{/other_user}","gists_url":"https://api.github.com/users/MikeSpreitzer/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeSpreitzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeSpreitzer/subscriptions","organizations_url":"https://api.github.com/users/MikeSpreitzer/orgs","repos_url":"https://api.github.com/users/MikeSpreitzer/repos","events_url":"https://api.github.com/users/MikeSpreitzer/events{/privacy}","received_events_url":"https://api.github.com/users/MikeSpreitzer/received_events","type":"User","site_admin":false},"html_url":"https://github.com/MikeSpreitzer/kube-examples","description":"Kubernetes application example tutorials","fork":true,"url":"https://api.github.com/repos/MikeSpreitzer/kube-examples","forks_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/forks","keys_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/keys{/key_id}","collaborators_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/teams","hooks_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/hooks","issue_events_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/events{/number}","events_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/events","assignees_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/assignees{/user}","branches_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/branches{/branch}","tags_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/tags","blobs_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/refs{/sha}","trees_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/trees{/sha}","statuses_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/statuses/{sha}","languages_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/languages","stargazers_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/stargazers","contributors_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/contributors","subscribers_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/subscribers","subscription_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/subscription","commits_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/commits{/sha}","git_commits_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/commits{/sha}","comments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/comments{/number}","issue_comment_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/comments{/number}","contents_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/contents/{+path}","compare_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/compare/{base}...{head}","merges_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/merges","archive_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/downloads","issues_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues{/number}","pulls_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls{/number}","milestones_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/milestones{/number}","notifications_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/labels{/name}","releases_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/releases{/id}","deployments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/deployments","created_at":"2018-08-21T18:30:21Z","updated_at":"2018-10-09T13:33:24Z","pushed_at":"2018-12-11T19:04:02Z","git_url":"git://github.com/MikeSpreitzer/kube-examples.git","ssh_url":"git@github.com:MikeSpreitzer/kube-examples.git","clone_url":"https://github.com/MikeSpreitzer/kube-examples.git","svn_url":"https://github.com/MikeSpreitzer/kube-examples","homepage":null,"size":41234,"stargazers_count":2,"watchers_count":2,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":2,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":2,"watchers":2,"default_branch":"master"}},"base":{"label":"MikeSpreitzer:add-kos","ref":"add-kos","sha":"78b4d71b67f54cae4fb7a35fcfcf7e261f861c7c","user":{"login":"MikeSpreitzer","id":14296719,"node_id":"MDQ6VXNlcjE0Mjk2NzE5","avatar_url":"https://avatars2.githubusercontent.com/u/14296719?v=4","gravatar_id":"","url":"https://api.github.com/users/MikeSpreitzer","html_url":"https://github.com/MikeSpreitzer","followers_url":"https://api.github.com/users/MikeSpreitzer/followers","following_url":"https://api.github.com/users/MikeSpreitzer/following{/other_user}","gists_url":"https://api.github.com/users/MikeSpreitzer/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeSpreitzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeSpreitzer/subscriptions","organizations_url":"https://api.github.com/users/MikeSpreitzer/orgs","repos_url":"https://api.github.com/users/MikeSpreitzer/repos","events_url":"https://api.github.com/users/MikeSpreitzer/events{/privacy}","received_events_url":"https://api.github.com/users/MikeSpreitzer/received_events","type":"User","site_admin":false},"repo":{"id":145603833,"node_id":"MDEwOlJlcG9zaXRvcnkxNDU2MDM4MzM=","name":"kube-examples","full_name":"MikeSpreitzer/kube-examples","private":false,"owner":{"login":"MikeSpreitzer","id":14296719,"node_id":"MDQ6VXNlcjE0Mjk2NzE5","avatar_url":"https://avatars2.githubusercontent.com/u/14296719?v=4","gravatar_id":"","url":"https://api.github.com/users/MikeSpreitzer","html_url":"https://github.com/MikeSpreitzer","followers_url":"https://api.github.com/users/MikeSpreitzer/followers","following_url":"https://api.github.com/users/MikeSpreitzer/following{/other_user}","gists_url":"https://api.github.com/users/MikeSpreitzer/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeSpreitzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeSpreitzer/subscriptions","organizations_url":"https://api.github.com/users/MikeSpreitzer/orgs","repos_url":"https://api.github.com/users/MikeSpreitzer/repos","events_url":"https://api.github.com/users/MikeSpreitzer/events{/privacy}","received_events_url":"https://api.github.com/users/MikeSpreitzer/received_events","type":"User","site_admin":false},"html_url":"https://github.com/MikeSpreitzer/kube-examples","description":"Kubernetes application example tutorials","fork":true,"url":"https://api.github.com/repos/MikeSpreitzer/kube-examples","forks_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/forks","keys_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/keys{/key_id}","collaborators_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/teams","hooks_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/hooks","issue_events_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/events{/number}","events_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/events","assignees_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/assignees{/user}","branches_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/branches{/branch}","tags_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/tags","blobs_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/refs{/sha}","trees_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/trees{/sha}","statuses_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/statuses/{sha}","languages_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/languages","stargazers_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/stargazers","contributors_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/contributors","subscribers_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/subscribers","subscription_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/subscription","commits_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/commits{/sha}","git_commits_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/git/commits{/sha}","comments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/comments{/number}","issue_comment_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/comments{/number}","contents_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/contents/{+path}","compare_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/compare/{base}...{head}","merges_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/merges","archive_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/downloads","issues_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues{/number}","pulls_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls{/number}","milestones_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/milestones{/number}","notifications_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/labels{/name}","releases_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/releases{/id}","deployments_url":"https://api.github.com/repos/MikeSpreitzer/kube-examples/deployments","created_at":"2018-08-21T18:30:21Z","updated_at":"2018-10-09T13:33:24Z","pushed_at":"2018-12-11T19:04:02Z","git_url":"git://github.com/MikeSpreitzer/kube-examples.git","ssh_url":"git@github.com:MikeSpreitzer/kube-examples.git","clone_url":"https://github.com/MikeSpreitzer/kube-examples.git","svn_url":"https://github.com/MikeSpreitzer/kube-examples","homepage":null,"size":41234,"stargazers_count":2,"watchers_count":2,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":2,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":2,"watchers":2,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2"},"html":{"href":"https://github.com/MikeSpreitzer/kube-examples/pull/2"},"issue":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/2"},"comments":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/issues/2/comments"},"review_comments":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2/comments"},"review_comment":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/pulls/2/commits"},"statuses":{"href":"https://api.github.com/repos/MikeSpreitzer/kube-examples/statuses/0ece9dc0193cbe355fde9afafb22c7a7f3a7ebd7"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"MikeSpreitzer"}}