<commit_before>// Copyright 2016-2019 Authors of Cilium
//
// 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 ipcache

import (
	"context"
	"fmt"
	"net"
	"os"
	"sync"
	"time"

	"github.com/cilium/cilium/pkg/bpf"
	"github.com/cilium/cilium/pkg/controller"
	"github.com/cilium/cilium/pkg/identity"
	"github.com/cilium/cilium/pkg/ipcache"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	ipcacheMap "github.com/cilium/cilium/pkg/maps/ipcache"
	"github.com/cilium/cilium/pkg/node"

	"github.com/sirupsen/logrus"
)

var log = logging.DefaultLogger.WithField(logfields.LogSubsys, "datapath-ipcache")

// datapath is an interface to the datapath implementation, used to apply
// changes that are made within this module.
type datapath interface {
	TriggerReloadWithoutCompile(reason string) (*sync.WaitGroup, error)
}

// BPFListener implements the ipcache.IPIdentityMappingBPFListener
// interface with an IPCache store that is backed by BPF maps.
//
// One listener is shared between callers of OnIPIdentityCacheChange() and the
// controller launched from OnIPIdentityCacheGC(). However, The listener is not
// updated after initialization so no locking is provided for access.
type BPFListener struct {
	// bpfMap is the BPF map that this listener will update when events are
	// received from the IPCache.
	bpfMap *ipcacheMap.Map

	// datapath allows this listener to trigger BPF program regeneration.
	datapath datapath
}

func newListener(m *ipcacheMap.Map, d datapath) *BPFListener {
	return &BPFListener{
		bpfMap:   m,
		datapath: d,
	}
}

// NewListener returns a new listener to push IPCache entries into BPF maps.
func NewListener(d datapath) *BPFListener {
	return newListener(ipcacheMap.IPCache, d)
}

// OnIPIdentityCacheChange is called whenever there is a change of state in the
// IPCache (pkg/ipcache).
// TODO (FIXME): GH-3161.
//
// 'oldIPIDPair' is ignored here, because in the BPF maps an update for the
// IP->ID mapping will replace any existing contents; knowledge of the old pair
// is not required to upsert the new pair.
func (l *BPFListener) OnIPIdentityCacheChange(modType ipcache.CacheModification, cidr net.IPNet,
	oldHostIP, newHostIP net.IP, oldID *identity.NumericIdentity, newID identity.NumericIdentity, encryptKey uint8) {
	scopedLog := log.WithFields(logrus.Fields{
		logfields.IPAddr:       cidr,
		logfields.Identity:     newID,
		logfields.Modification: modType,
	})

	scopedLog.Debug("Daemon notified of IP-Identity cache state change")

	// TODO - see if we can factor this into an interface under something like
	// pkg/datapath instead of in the daemon directly so that the code is more
	// logically located.

	// Update BPF Maps.

	key := ipcacheMap.NewKey(cidr.IP, cidr.Mask)

	switch modType {
	case ipcache.Upsert:
		value := ipcacheMap.RemoteEndpointInfo{
			SecurityIdentity: uint32(newID),
			Key:              encryptKey,
		}

		if newHostIP != nil {
			// If the hostIP is specified and it doesn't point to
			// the local host, then the ipcache should be populated
			// with the hostIP so that this traffic can be guided
			// to a tunnel endpoint destination.
			externalIP := node.GetExternalIPv4()
			if ip4 := newHostIP.To4(); ip4 != nil && !ip4.Equal(externalIP) {
				copy(value.TunnelEndpoint[:], ip4)
			}
		}
		err := l.bpfMap.Update(&key, &value)
		if err != nil {
			scopedLog.WithError(err).WithFields(logrus.Fields{"key": key.String(),
				"value": value.String()}).
				Warning("unable to update bpf map")
		}
	case ipcache.Delete:
		err := l.bpfMap.Delete(&key)
		if err != nil {
			scopedLog.WithError(err).WithFields(logrus.Fields{"key": key.String()}).
				Warning("unable to delete from bpf map")
		}
	default:
		scopedLog.Warning("cache modification type not supported")
	}
}

// updateStaleEntriesFunction returns a DumpCallback that will update the
// specified "keysToRemove" map with entries that exist in the BPF map which
// do not exist in the in-memory ipcache.
//
// Must be called while holding ipcache.IPIdentityCache.Lock for reading.
func updateStaleEntriesFunction(keysToRemove map[string]*ipcacheMap.Key) bpf.DumpCallback {
	return func(key bpf.MapKey, _ bpf.MapValue) {
		k := key.(*ipcacheMap.Key)
		keyToIP := k.String()

		// Don't RLock as part of the same goroutine.
		if i, exists := ipcache.IPIdentityCache.LookupByPrefixRLocked(keyToIP); !exists {
			switch i.Source {
			case ipcache.FromKVStore, ipcache.FromAgentLocal:
				// Cannot delete from map during callback because DumpWithCallback
				// RLocks the map.
				keysToRemove[keyToIP] = k.DeepCopy()
			}
		}
	}
}

// handleMapShuffleFailure attempts to move the map with name 'backup' back to
// 'realized', and logs a warning message if this can't be achieved.
func handleMapShuffleFailure(src, dst string) {
	backupPath := bpf.MapPath(src)
	realizedPath := bpf.MapPath(dst)

	if err := os.Rename(backupPath, realizedPath); err != nil {
		log.WithError(err).WithFields(logrus.Fields{
			logfields.BPFMapPath: realizedPath,
		}).Warningf("Unable to recover during error renaming map paths")
	}
}

// shuffleMaps attempts to move the map with name 'realized' to 'backup' and
// 'pending' to 'realized'. If an error occurs, attempts to return the maps
// back to their original paths.
func shuffleMaps(realized, backup, pending string) error {
	realizedPath := bpf.MapPath(realized)
	backupPath := bpf.MapPath(backup)
	pendingPath := bpf.MapPath(pending)

	if err := os.Rename(realizedPath, backupPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("Unable to back up existing ipcache: %s", err)
	}

	if err := os.Rename(pendingPath, realizedPath); err != nil {
		handleMapShuffleFailure(backup, realized)
		return fmt.Errorf("Unable to shift ipcache into new location: %s", err)
	}

	return nil
}

// garbageCollect implements GC of the ipcache map in one of two ways:
//
// On Linux 4.9, 4.10 or 4.15 and later:
//   Periodically sweep through every element in the BPF map and check it
//   against the in-memory copy of the map. If it doesn't exist in memory,
//   delete the entry.
// On Linux 4.11 to 4.14:
//   Create a brand new map, populate it with all of the IPCache entries from
//   the in-memory cache, delete the old map, and trigger regeneration of all
//   BPF programs so that they pick up the new map.
//
// Returns an error if garbage collection failed to occur.
func (l *BPFListener) garbageCollect(ctx context.Context) error {
	log.Debug("Running garbage collection for BPF IPCache")

	// Since controllers run asynchronously, need to make sure
	// IPIdentityCache is not being updated concurrently while we do
	// GC;
	ipcache.IPIdentityCache.RLock()
	defer ipcache.IPIdentityCache.RUnlock()

	if ipcacheMap.SupportsDelete() {
		keysToRemove := map[string]*ipcacheMap.Key{}
		if err := l.bpfMap.DumpWithCallback(updateStaleEntriesFunction(keysToRemove)); err != nil {
			return fmt.Errorf("error dumping ipcache BPF map: %s", err)
		}

		// Remove all keys which are not in in-memory cache from BPF map
		// for consistency.
		for _, k := range keysToRemove {
			log.WithFields(logrus.Fields{logfields.BPFMapKey: k}).
				Debug("deleting from ipcache BPF map")
			if err := l.bpfMap.Delete(k); err != nil {
				return fmt.Errorf("error deleting key %s from ipcache BPF map: %s", k, err)
			}
		}
	} else {
		// Populate the map at the new path
		pendingMapName := fmt.Sprintf("%s_pending", ipcacheMap.Name)
		pendingMap := ipcacheMap.NewMap(pendingMapName)
		if _, err := pendingMap.OpenOrCreate(); err != nil {
			return fmt.Errorf("Unable to create %s map: %s", pendingMapName, err)
		}
		pendingListener := newListener(pendingMap, l.datapath)
		ipcache.IPIdentityCache.DumpToListenerLocked(pendingListener)
		err := pendingMap.Close()
		if err != nil {
			log.WithError(err).WithField("map-name", pendingMapName).Warning("unable to close map")
		}

		// Move the maps around on the filesystem so that BPF reload
		// will pick up the new paths without requiring recompilation.
		backupMapName := fmt.Sprintf("%s_old", ipcacheMap.Name)
		if err := shuffleMaps(ipcacheMap.Name, backupMapName, pendingMapName); err != nil {
			return err
		}

		wg, err := l.datapath.TriggerReloadWithoutCompile("datapath ipcache")
		if err != nil {
			handleMapShuffleFailure(backupMapName, ipcacheMap.Name)
			return err
		}

		// If the base programs successfully compiled, then the maps
		// should be OK so let's update all references to the IPCache
		// so that they point to the new version.
		_ = os.RemoveAll(bpf.MapPath(backupMapName))
		if err := ipcacheMap.Reopen(); err != nil {
			// Very unlikely; base program compilation succeeded.
			log.WithError(err).Warning("Failed to reopen BPF ipcache map")
			return err
		}
		wg.Wait()
	}
	return nil
}

// OnIPIdentityCacheGC spawns a controller which synchronizes the BPF IPCache Map
// with the in-memory IP-Identity cache.
func (l *BPFListener) OnIPIdentityCacheGC() {
	// This controller ensures that the in-memory IP-identity cache is in-sync
	// with the BPF map on disk. These can get out of sync if the cilium-agent
	// is offline for some time, as the maps persist on the BPF filesystem.
	// In the case that there is some loss of event history in the key-value
	// store (e.g., compaction in etcd), we cannot rely upon the key-value store
	// fully to give us the history of all events. As such, periodically check
	// for inconsistencies in the data-path with that in the agent to ensure
	// consistent state.
	controller.NewManager().UpdateController("ipcache-bpf-garbage-collection",
		controller.ControllerParams{
			DoFunc:      l.garbageCollect,
			RunInterval: 5 * time.Minute,
		},
	)
}
<commit_msg>pkg/datapath/ipcache: only log if not running in debug<commit_after>// Copyright 2016-2019 Authors of Cilium
//
// 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 ipcache

import (
	"context"
	"fmt"
	"net"
	"os"
	"sync"
	"time"

	"github.com/cilium/cilium/pkg/bpf"
	"github.com/cilium/cilium/pkg/controller"
	"github.com/cilium/cilium/pkg/identity"
	"github.com/cilium/cilium/pkg/ipcache"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	ipcacheMap "github.com/cilium/cilium/pkg/maps/ipcache"
	"github.com/cilium/cilium/pkg/node"
	"github.com/cilium/cilium/pkg/option"

	"github.com/sirupsen/logrus"
)

var log = logging.DefaultLogger.WithField(logfields.LogSubsys, "datapath-ipcache")

// datapath is an interface to the datapath implementation, used to apply
// changes that are made within this module.
type datapath interface {
	TriggerReloadWithoutCompile(reason string) (*sync.WaitGroup, error)
}

// BPFListener implements the ipcache.IPIdentityMappingBPFListener
// interface with an IPCache store that is backed by BPF maps.
//
// One listener is shared between callers of OnIPIdentityCacheChange() and the
// controller launched from OnIPIdentityCacheGC(). However, The listener is not
// updated after initialization so no locking is provided for access.
type BPFListener struct {
	// bpfMap is the BPF map that this listener will update when events are
	// received from the IPCache.
	bpfMap *ipcacheMap.Map

	// datapath allows this listener to trigger BPF program regeneration.
	datapath datapath
}

func newListener(m *ipcacheMap.Map, d datapath) *BPFListener {
	return &BPFListener{
		bpfMap:   m,
		datapath: d,
	}
}

// NewListener returns a new listener to push IPCache entries into BPF maps.
func NewListener(d datapath) *BPFListener {
	return newListener(ipcacheMap.IPCache, d)
}

// OnIPIdentityCacheChange is called whenever there is a change of state in the
// IPCache (pkg/ipcache).
// TODO (FIXME): GH-3161.
//
// 'oldIPIDPair' is ignored here, because in the BPF maps an update for the
// IP->ID mapping will replace any existing contents; knowledge of the old pair
// is not required to upsert the new pair.
func (l *BPFListener) OnIPIdentityCacheChange(modType ipcache.CacheModification, cidr net.IPNet,
	oldHostIP, newHostIP net.IP, oldID *identity.NumericIdentity, newID identity.NumericIdentity, encryptKey uint8) {

	scopedLog := log
	if option.Config.Debug {
		scopedLog = log.WithFields(logrus.Fields{
			logfields.IPAddr:       cidr,
			logfields.Identity:     newID,
			logfields.Modification: modType,
		})
	}

	scopedLog.Debug("Daemon notified of IP-Identity cache state change")

	// TODO - see if we can factor this into an interface under something like
	// pkg/datapath instead of in the daemon directly so that the code is more
	// logically located.

	// Update BPF Maps.

	key := ipcacheMap.NewKey(cidr.IP, cidr.Mask)

	switch modType {
	case ipcache.Upsert:
		value := ipcacheMap.RemoteEndpointInfo{
			SecurityIdentity: uint32(newID),
			Key:              encryptKey,
		}

		if newHostIP != nil {
			// If the hostIP is specified and it doesn't point to
			// the local host, then the ipcache should be populated
			// with the hostIP so that this traffic can be guided
			// to a tunnel endpoint destination.
			externalIP := node.GetExternalIPv4()
			if ip4 := newHostIP.To4(); ip4 != nil && !ip4.Equal(externalIP) {
				copy(value.TunnelEndpoint[:], ip4)
			}
		}
		err := l.bpfMap.Update(&key, &value)
		if err != nil {
			scopedLog.WithError(err).WithFields(logrus.Fields{
				"key":                  key.String(),
				"value":                value.String(),
				logfields.IPAddr:       cidr,
				logfields.Identity:     newID,
				logfields.Modification: modType,
			}).Warning("unable to update bpf map")
		}
	case ipcache.Delete:
		err := l.bpfMap.Delete(&key)
		if err != nil {
			scopedLog.WithError(err).WithFields(logrus.Fields{
				"key":                  key.String(),
				logfields.IPAddr:       cidr,
				logfields.Identity:     newID,
				logfields.Modification: modType,
			}).Warning("unable to delete from bpf map")
		}
	default:
		scopedLog.Warning("cache modification type not supported")
	}
}

// updateStaleEntriesFunction returns a DumpCallback that will update the
// specified "keysToRemove" map with entries that exist in the BPF map which
// do not exist in the in-memory ipcache.
//
// Must be called while holding ipcache.IPIdentityCache.Lock for reading.
func updateStaleEntriesFunction(keysToRemove map[string]*ipcacheMap.Key) bpf.DumpCallback {
	return func(key bpf.MapKey, _ bpf.MapValue) {
		k := key.(*ipcacheMap.Key)
		keyToIP := k.String()

		// Don't RLock as part of the same goroutine.
		if i, exists := ipcache.IPIdentityCache.LookupByPrefixRLocked(keyToIP); !exists {
			switch i.Source {
			case ipcache.FromKVStore, ipcache.FromAgentLocal:
				// Cannot delete from map during callback because DumpWithCallback
				// RLocks the map.
				keysToRemove[keyToIP] = k.DeepCopy()
			}
		}
	}
}

// handleMapShuffleFailure attempts to move the map with name 'backup' back to
// 'realized', and logs a warning message if this can't be achieved.
func handleMapShuffleFailure(src, dst string) {
	backupPath := bpf.MapPath(src)
	realizedPath := bpf.MapPath(dst)

	if err := os.Rename(backupPath, realizedPath); err != nil {
		log.WithError(err).WithFields(logrus.Fields{
			logfields.BPFMapPath: realizedPath,
		}).Warningf("Unable to recover during error renaming map paths")
	}
}

// shuffleMaps attempts to move the map with name 'realized' to 'backup' and
// 'pending' to 'realized'. If an error occurs, attempts to return the maps
// back to their original paths.
func shuffleMaps(realized, backup, pending string) error {
	realizedPath := bpf.MapPath(realized)
	backupPath := bpf.MapPath(backup)
	pendingPath := bpf.MapPath(pending)

	if err := os.Rename(realizedPath, backupPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("Unable to back up existing ipcache: %s", err)
	}

	if err := os.Rename(pendingPath, realizedPath); err != nil {
		handleMapShuffleFailure(backup, realized)
		return fmt.Errorf("Unable to shift ipcache into new location: %s", err)
	}

	return nil
}

// garbageCollect implements GC of the ipcache map in one of two ways:
//
// On Linux 4.9, 4.10 or 4.15 and later:
//   Periodically sweep through every element in the BPF map and check it
//   against the in-memory copy of the map. If it doesn't exist in memory,
//   delete the entry.
// On Linux 4.11 to 4.14:
//   Create a brand new map, populate it with all of the IPCache entries from
//   the in-memory cache, delete the old map, and trigger regeneration of all
//   BPF programs so that they pick up the new map.
//
// Returns an error if garbage collection failed to occur.
func (l *BPFListener) garbageCollect(ctx context.Context) error {
	log.Debug("Running garbage collection for BPF IPCache")

	// Since controllers run asynchronously, need to make sure
	// IPIdentityCache is not being updated concurrently while we do
	// GC;
	ipcache.IPIdentityCache.RLock()
	defer ipcache.IPIdentityCache.RUnlock()

	if ipcacheMap.SupportsDelete() {
		keysToRemove := map[string]*ipcacheMap.Key{}
		if err := l.bpfMap.DumpWithCallback(updateStaleEntriesFunction(keysToRemove)); err != nil {
			return fmt.Errorf("error dumping ipcache BPF map: %s", err)
		}

		// Remove all keys which are not in in-memory cache from BPF map
		// for consistency.
		for _, k := range keysToRemove {
			log.WithFields(logrus.Fields{logfields.BPFMapKey: k}).
				Debug("deleting from ipcache BPF map")
			if err := l.bpfMap.Delete(k); err != nil {
				return fmt.Errorf("error deleting key %s from ipcache BPF map: %s", k, err)
			}
		}
	} else {
		// Populate the map at the new path
		pendingMapName := fmt.Sprintf("%s_pending", ipcacheMap.Name)
		pendingMap := ipcacheMap.NewMap(pendingMapName)
		if _, err := pendingMap.OpenOrCreate(); err != nil {
			return fmt.Errorf("Unable to create %s map: %s", pendingMapName, err)
		}
		pendingListener := newListener(pendingMap, l.datapath)
		ipcache.IPIdentityCache.DumpToListenerLocked(pendingListener)
		err := pendingMap.Close()
		if err != nil {
			log.WithError(err).WithField("map-name", pendingMapName).Warning("unable to close map")
		}

		// Move the maps around on the filesystem so that BPF reload
		// will pick up the new paths without requiring recompilation.
		backupMapName := fmt.Sprintf("%s_old", ipcacheMap.Name)
		if err := shuffleMaps(ipcacheMap.Name, backupMapName, pendingMapName); err != nil {
			return err
		}

		wg, err := l.datapath.TriggerReloadWithoutCompile("datapath ipcache")
		if err != nil {
			handleMapShuffleFailure(backupMapName, ipcacheMap.Name)
			return err
		}

		// If the base programs successfully compiled, then the maps
		// should be OK so let's update all references to the IPCache
		// so that they point to the new version.
		_ = os.RemoveAll(bpf.MapPath(backupMapName))
		if err := ipcacheMap.Reopen(); err != nil {
			// Very unlikely; base program compilation succeeded.
			log.WithError(err).Warning("Failed to reopen BPF ipcache map")
			return err
		}
		wg.Wait()
	}
	return nil
}

// OnIPIdentityCacheGC spawns a controller which synchronizes the BPF IPCache Map
// with the in-memory IP-Identity cache.
func (l *BPFListener) OnIPIdentityCacheGC() {
	// This controller ensures that the in-memory IP-identity cache is in-sync
	// with the BPF map on disk. These can get out of sync if the cilium-agent
	// is offline for some time, as the maps persist on the BPF filesystem.
	// In the case that there is some loss of event history in the key-value
	// store (e.g., compaction in etcd), we cannot rely upon the key-value store
	// fully to give us the history of all events. As such, periodically check
	// for inconsistencies in the data-path with that in the agent to ensure
	// consistent state.
	controller.NewManager().UpdateController("ipcache-bpf-garbage-collection",
		controller.ControllerParams{
			DoFunc:      l.garbageCollect,
			RunInterval: 5 * time.Minute,
		},
	)
}
<|endoftext|>


<commit_before>package network

// Set up test environment needed for network diagnostics
import (
	"fmt"
	"io/ioutil"
	"strings"
	"time"

	kerrs "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apiserver/pkg/storage/names"
	kclientcmd "k8s.io/client-go/tools/clientcmd"
	kapi "k8s.io/kubernetes/pkg/api"

	"github.com/openshift/origin/pkg/diagnostics/networkpod/util"
	diagutil "github.com/openshift/origin/pkg/diagnostics/util"
	"github.com/openshift/origin/pkg/network"
	networkapi "github.com/openshift/origin/pkg/network/apis/network"
	"github.com/openshift/origin/pkg/oc/cli/config"
)

func (d *NetworkDiagnostic) TestSetup() error {
	d.nsName1 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagNamespacePrefix))
	d.nsName2 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagNamespacePrefix))

	nsList := []string{d.nsName1, d.nsName2}
	if network.IsOpenShiftMultitenantNetworkPlugin(d.pluginName) {
		d.globalnsName1 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagGlobalNamespacePrefix))
		nsList = append(nsList, d.globalnsName1)
		d.globalnsName2 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagGlobalNamespacePrefix))
		nsList = append(nsList, d.globalnsName2)
	}

	for _, name := range nsList {
		// Create a new namespace for network diagnostics
		ns := &kapi.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: map[string]string{"openshift.io/node-selector": ""}}}
		if _, err := d.KubeClient.Core().Namespaces().Create(ns); err != nil {
			return fmt.Errorf("Creating namespace %q failed: %v", name, err)
		}
		if strings.HasPrefix(name, util.NetworkDiagGlobalNamespacePrefix) {
			if err := d.makeNamespaceGlobal(name); err != nil {
				return fmt.Errorf("Making namespace %q global failed: %v", name, err)
			}
		}
	}

	// Store kubeconfig as secret, used by network diagnostic pod
	kconfigData, err := d.getKubeConfig()
	if err != nil {
		return fmt.Errorf("Fetching kube config for network pod failed: %v", err)
	}
	secret := &kapi.Secret{}
	secret.Name = util.NetworkDiagSecretName
	secret.Data = map[string][]byte{strings.ToLower(kclientcmd.RecommendedConfigPathEnvVar): kconfigData}
	if _, err = d.KubeClient.Core().Secrets(d.nsName1).Create(secret); err != nil {
		return fmt.Errorf("Creating secret %q failed: %v", secret.Name, err)
	}

	// Create test pods and services on all valid nodes
	if err := d.createTestPodAndService(nsList); err != nil {
		// Failed to create test pods/services on some nodes
		d.res.Error("DNet3001", err, fmt.Sprintf("Failed to create network diags test pod and service: %v", err))
	}
	// Wait for test pods and services to be up and running on all valid nodes
	if err = d.waitForTestPodAndService(nsList); err != nil {
		return fmt.Errorf("Failed to run network diags test pod and service: %v", err)
	}
	return nil
}

func (d *NetworkDiagnostic) Cleanup() {
	// Deleting namespaces will delete corresponding service accounts/pods in the namespace automatically.
	d.KubeClient.Core().Namespaces().Delete(d.nsName1, nil)
	d.KubeClient.Core().Namespaces().Delete(d.nsName2, nil)
	d.KubeClient.Core().Namespaces().Delete(d.globalnsName1, nil)
	d.KubeClient.Core().Namespaces().Delete(d.globalnsName2, nil)
}

func (d *NetworkDiagnostic) getPodList(nsName, prefix string) (*kapi.PodList, error) {
	podList, err := d.KubeClient.Core().Pods(nsName).List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	filteredPodList := &kapi.PodList{}
	for _, pod := range podList.Items {
		if strings.HasPrefix(pod.Name, prefix) {
			filteredPodList.Items = append(filteredPodList.Items, pod)
		}
	}
	return filteredPodList, nil
}

func (d *NetworkDiagnostic) waitForNetworkPod(nsName, prefix string, backoff wait.Backoff, validPhases []kapi.PodPhase) error {
	return wait.ExponentialBackoff(backoff, func() (bool, error) {
		podList, err := d.getPodList(nsName, prefix)
		if err != nil {
			return false, err
		}

		for _, pod := range podList.Items {
			foundValidPhase := false
			for _, phase := range validPhases {
				if pod.Status.Phase == phase {
					foundValidPhase = true
					break
				}
			}
			if !foundValidPhase {
				return false, nil
			}
		}
		return true, nil
	})
}

func (d *NetworkDiagnostic) createTestPodAndService(nsList []string) error {
	errList := []error{}
	for _, node := range d.nodes {
		for _, nsName := range nsList {
			// Create 2 pods and a service in global and non-global network diagnostic namespaces
			var testPodName string
			for i := 0; i < 2; i++ {
				testPodName = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagTestPodNamePrefix))
				// Create network diags test pod on the given node for the given namespace
				pod := GetTestPod(d.TestPodImage, d.TestPodProtocol, testPodName, node.Name, d.TestPodPort)
				if _, err := d.KubeClient.Core().Pods(nsName).Create(pod); err != nil {
					errList = append(errList, fmt.Errorf("Creating network diagnostic test pod '%s/%s' on node %q failed: %v", nsName, testPodName, node.Name, err))
					continue
				}
			}

			// Create network diags test service on the given node for the given namespace
			testServiceName := names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagTestServiceNamePrefix))
			service := GetTestService(testServiceName, testPodName, d.TestPodProtocol, node.Name, d.TestPodPort)
			if _, err := d.KubeClient.Core().Services(nsName).Create(service); err != nil {
				errList = append(errList, fmt.Errorf("Creating network diagnostic test service '%s/%s' on node %q failed: %v", nsName, testServiceName, node.Name, err))
				continue
			}
		}
	}
	return kerrors.NewAggregate(errList)
}

func (d *NetworkDiagnostic) waitForTestPodAndService(nsList []string) error {
	errList := []error{}
	validPhases := []kapi.PodPhase{kapi.PodRunning, kapi.PodSucceeded, kapi.PodFailed}
	for _, name := range nsList {
		backoff := wait.Backoff{Steps: 36, Duration: time.Second, Factor: 1.1} // timeout: ~5 mins
		if err := d.waitForNetworkPod(name, util.NetworkDiagTestPodNamePrefix, backoff, validPhases); err != nil {
			errList = append(errList, err)
		}
	}

	if totalPods, runningPods, err := d.getCountOfTestPods(nsList); err == nil {
		// Perform network diagnostic checks if we are able to launch decent number of test pods (at least 50%)
		if runningPods != totalPods {
			if runningPods >= (totalPods / 2) {
				d.res.Warn("DNet3002", nil, fmt.Sprintf("Failed to run some network diags test pods: %d, So some network diagnostic checks may be skipped.", (totalPods-runningPods)))
				return nil
			} else {
				errList = append(errList, fmt.Errorf("Failed to run network diags test pods, failed: %d, total: %d", (totalPods-runningPods), totalPods))
			}
		}
	}
	return kerrors.NewAggregate(errList)
}

func (d *NetworkDiagnostic) getCountOfTestPods(nsList []string) (int, int, error) {
	totalPodCount := 0
	runningPodCount := 0
	for _, name := range nsList {
		podList, err := d.getPodList(name, util.NetworkDiagTestPodNamePrefix)
		if err != nil {
			return -1, -1, err
		}
		totalPodCount += len(podList.Items)

		for _, pod := range podList.Items {
			if pod.Status.Phase == kapi.PodRunning {
				runningPodCount += 1
			}
		}
	}
	return totalPodCount, runningPodCount, nil
}

func (d *NetworkDiagnostic) makeNamespaceGlobal(nsName string) error {
	backoff := wait.Backoff{
		Steps:    30,
		Duration: 500 * time.Millisecond,
		Factor:   1.1,
	}
	var netns *networkapi.NetNamespace
	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
		var err error
		netns, err = d.OSClient.NetNamespaces().Get(nsName, metav1.GetOptions{})
		if kerrs.IsNotFound(err) {
			// NetNamespace not created yet
			return false, nil
		} else if err != nil {
			return false, err
		}
		return true, nil
	})
	if err != nil {
		return err
	}

	network.SetChangePodNetworkAnnotation(netns, network.GlobalPodNetwork, "")

	if _, err = d.OSClient.NetNamespaces().Update(netns); err != nil {
		return err
	}

	return wait.ExponentialBackoff(backoff, func() (bool, error) {
		updatedNetNs, err := d.OSClient.NetNamespaces().Get(netns.NetName, metav1.GetOptions{})
		if err != nil {
			return false, err
		}

		if _, _, err = network.GetChangePodNetworkAnnotation(updatedNetNs); err == network.ErrorPodNetworkAnnotationNotFound {
			return true, nil
		}
		// Pod network change not applied yet
		return false, nil
	})
}

func (d *NetworkDiagnostic) getKubeConfig() ([]byte, error) {
	// KubeConfig path search order:
	// 1. User given config path
	// 2. Default admin config paths
	// 3. Default openshift client config search paths
	paths := []string{}
	paths = append(paths, d.ClientFlags.Lookup(config.OpenShiftConfigFlagName).Value.String())
	paths = append(paths, diagutil.AdminKubeConfigPaths...)
	paths = append(paths, config.NewOpenShiftClientConfigLoadingRules().Precedence...)

	for _, path := range paths {
		if configData, err := ioutil.ReadFile(path); err == nil {
			return configData, nil
		}
	}
	return nil, fmt.Errorf("Unable to find kube config")
}
<commit_msg>Print more details when network diagnostics test setup fails<commit_after>package network

// Set up test environment needed for network diagnostics
import (
	"fmt"
	"io/ioutil"
	"strings"
	"time"

	kerrs "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apiserver/pkg/storage/names"
	kclientcmd "k8s.io/client-go/tools/clientcmd"
	kapi "k8s.io/kubernetes/pkg/api"

	"github.com/openshift/origin/pkg/diagnostics/networkpod/util"
	diagutil "github.com/openshift/origin/pkg/diagnostics/util"
	"github.com/openshift/origin/pkg/network"
	networkapi "github.com/openshift/origin/pkg/network/apis/network"
	"github.com/openshift/origin/pkg/oc/cli/config"
)

func (d *NetworkDiagnostic) TestSetup() error {
	d.nsName1 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagNamespacePrefix))
	d.nsName2 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagNamespacePrefix))

	nsList := []string{d.nsName1, d.nsName2}
	if network.IsOpenShiftMultitenantNetworkPlugin(d.pluginName) {
		d.globalnsName1 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagGlobalNamespacePrefix))
		nsList = append(nsList, d.globalnsName1)
		d.globalnsName2 = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagGlobalNamespacePrefix))
		nsList = append(nsList, d.globalnsName2)
	}

	for _, name := range nsList {
		// Create a new namespace for network diagnostics
		ns := &kapi.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: map[string]string{"openshift.io/node-selector": ""}}}
		if _, err := d.KubeClient.Core().Namespaces().Create(ns); err != nil {
			return fmt.Errorf("Creating namespace %q failed: %v", name, err)
		}
		if strings.HasPrefix(name, util.NetworkDiagGlobalNamespacePrefix) {
			if err := d.makeNamespaceGlobal(name); err != nil {
				return fmt.Errorf("Making namespace %q global failed: %v", name, err)
			}
		}
	}

	// Store kubeconfig as secret, used by network diagnostic pod
	kconfigData, err := d.getKubeConfig()
	if err != nil {
		return fmt.Errorf("Fetching kube config for network pod failed: %v", err)
	}
	secret := &kapi.Secret{}
	secret.Name = util.NetworkDiagSecretName
	secret.Data = map[string][]byte{strings.ToLower(kclientcmd.RecommendedConfigPathEnvVar): kconfigData}
	if _, err = d.KubeClient.Core().Secrets(d.nsName1).Create(secret); err != nil {
		return fmt.Errorf("Creating secret %q failed: %v", secret.Name, err)
	}

	// Create test pods and services on all valid nodes
	if err := d.createTestPodAndService(nsList); err != nil {
		// Failed to create test pods/services on some nodes
		d.res.Error("DNet3001", err, fmt.Sprintf("Failed to create network diags test pod and service: %v", err))
	}
	// Wait for test pods and services to be up and running on all valid nodes
	if err = d.waitForTestPodAndService(nsList); err != nil {
		logData, er := d.getPodLogs(nsList)
		if er != nil {
			return fmt.Errorf("Failed to run network diags test pod and service: %v, fetching logs failed: %v", err, er)
		} else {
			return fmt.Errorf("Failed to run network diags test pod and service: %v, details: %s", err, logData)
		}
	}
	return nil
}

func (d *NetworkDiagnostic) Cleanup() {
	// Deleting namespaces will delete corresponding service accounts/pods in the namespace automatically.
	d.KubeClient.Core().Namespaces().Delete(d.nsName1, nil)
	d.KubeClient.Core().Namespaces().Delete(d.nsName2, nil)
	d.KubeClient.Core().Namespaces().Delete(d.globalnsName1, nil)
	d.KubeClient.Core().Namespaces().Delete(d.globalnsName2, nil)
}

func (d *NetworkDiagnostic) getPodList(nsName, prefix string) (*kapi.PodList, error) {
	podList, err := d.KubeClient.Core().Pods(nsName).List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	filteredPodList := &kapi.PodList{}
	for _, pod := range podList.Items {
		if strings.HasPrefix(pod.Name, prefix) {
			filteredPodList.Items = append(filteredPodList.Items, pod)
		}
	}
	return filteredPodList, nil
}

func (d *NetworkDiagnostic) waitForNetworkPod(nsName, prefix string, backoff wait.Backoff, validPhases []kapi.PodPhase) error {
	return wait.ExponentialBackoff(backoff, func() (bool, error) {
		podList, err := d.getPodList(nsName, prefix)
		if err != nil {
			return false, err
		}

		for _, pod := range podList.Items {
			foundValidPhase := false
			for _, phase := range validPhases {
				if pod.Status.Phase == phase {
					foundValidPhase = true
					break
				}
			}
			if !foundValidPhase {
				return false, nil
			}
		}
		return true, nil
	})
}

func (d *NetworkDiagnostic) createTestPodAndService(nsList []string) error {
	errList := []error{}
	for _, node := range d.nodes {
		for _, nsName := range nsList {
			// Create 2 pods and a service in global and non-global network diagnostic namespaces
			var testPodName string
			for i := 0; i < 2; i++ {
				testPodName = names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagTestPodNamePrefix))
				// Create network diags test pod on the given node for the given namespace
				pod := GetTestPod(d.TestPodImage, d.TestPodProtocol, testPodName, node.Name, d.TestPodPort)
				if _, err := d.KubeClient.Core().Pods(nsName).Create(pod); err != nil {
					errList = append(errList, fmt.Errorf("Creating network diagnostic test pod '%s/%s' on node %q failed: %v", nsName, testPodName, node.Name, err))
					continue
				}
			}

			// Create network diags test service on the given node for the given namespace
			testServiceName := names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", util.NetworkDiagTestServiceNamePrefix))
			service := GetTestService(testServiceName, testPodName, d.TestPodProtocol, node.Name, d.TestPodPort)
			if _, err := d.KubeClient.Core().Services(nsName).Create(service); err != nil {
				errList = append(errList, fmt.Errorf("Creating network diagnostic test service '%s/%s' on node %q failed: %v", nsName, testServiceName, node.Name, err))
				continue
			}
		}
	}
	return kerrors.NewAggregate(errList)
}

func (d *NetworkDiagnostic) waitForTestPodAndService(nsList []string) error {
	errList := []error{}
	validPhases := []kapi.PodPhase{kapi.PodRunning, kapi.PodSucceeded, kapi.PodFailed}
	for _, name := range nsList {
		backoff := wait.Backoff{Steps: 36, Duration: time.Second, Factor: 1.1} // timeout: ~5 mins
		if err := d.waitForNetworkPod(name, util.NetworkDiagTestPodNamePrefix, backoff, validPhases); err != nil {
			errList = append(errList, err)
		}
	}

	if totalPods, runningPods, err := d.getCountOfTestPods(nsList); err == nil {
		// Perform network diagnostic checks if we are able to launch decent number of test pods (at least 50%)
		if runningPods != totalPods {
			if runningPods >= (totalPods / 2) {
				d.res.Warn("DNet3002", nil, fmt.Sprintf("Failed to run some network diags test pods: %d, So some network diagnostic checks may be skipped.", (totalPods-runningPods)))
				return nil
			} else {
				errList = append(errList, fmt.Errorf("Failed to run network diags test pods, failed: %d, total: %d", (totalPods-runningPods), totalPods))
			}
		}
	}
	return kerrors.NewAggregate(errList)
}

func (d *NetworkDiagnostic) getPodLogs(nsList []string) (string, error) {
	logData := sets.String{}
	errList := []error{}
	limit := int64(1024)

	for _, name := range nsList {
		podList, err := d.getPodList(name, util.NetworkDiagTestPodNamePrefix)
		if err != nil {
			return "", err
		}

		for _, pod := range podList.Items {
			opts := &kapi.PodLogOptions{
				TypeMeta:   pod.TypeMeta,
				Container:  pod.Name,
				Follow:     true,
				LimitBytes: &limit,
			}

			req, err := d.Factory.LogsForObject(&pod, opts, 10*time.Second)
			if err != nil {
				errList = append(errList, err)
				continue
			}
			data, err := req.DoRaw()
			if err != nil {
				errList = append(errList, err)
				continue
			}
			logData.Insert(string(data[:]))
		}
	}
	return strings.Join(logData.List(), ", "), kerrors.NewAggregate(errList)
}

func (d *NetworkDiagnostic) getCountOfTestPods(nsList []string) (int, int, error) {
	totalPodCount := 0
	runningPodCount := 0
	for _, name := range nsList {
		podList, err := d.getPodList(name, util.NetworkDiagTestPodNamePrefix)
		if err != nil {
			return -1, -1, err
		}
		totalPodCount += len(podList.Items)

		for _, pod := range podList.Items {
			if pod.Status.Phase == kapi.PodRunning {
				runningPodCount += 1
			}
		}
	}
	return totalPodCount, runningPodCount, nil
}

func (d *NetworkDiagnostic) makeNamespaceGlobal(nsName string) error {
	backoff := wait.Backoff{
		Steps:    30,
		Duration: 500 * time.Millisecond,
		Factor:   1.1,
	}
	var netns *networkapi.NetNamespace
	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
		var err error
		netns, err = d.OSClient.NetNamespaces().Get(nsName, metav1.GetOptions{})
		if kerrs.IsNotFound(err) {
			// NetNamespace not created yet
			return false, nil
		} else if err != nil {
			return false, err
		}
		return true, nil
	})
	if err != nil {
		return err
	}

	network.SetChangePodNetworkAnnotation(netns, network.GlobalPodNetwork, "")

	if _, err = d.OSClient.NetNamespaces().Update(netns); err != nil {
		return err
	}

	return wait.ExponentialBackoff(backoff, func() (bool, error) {
		updatedNetNs, err := d.OSClient.NetNamespaces().Get(netns.NetName, metav1.GetOptions{})
		if err != nil {
			return false, err
		}

		if _, _, err = network.GetChangePodNetworkAnnotation(updatedNetNs); err == network.ErrorPodNetworkAnnotationNotFound {
			return true, nil
		}
		// Pod network change not applied yet
		return false, nil
	})
}

func (d *NetworkDiagnostic) getKubeConfig() ([]byte, error) {
	// KubeConfig path search order:
	// 1. User given config path
	// 2. Default admin config paths
	// 3. Default openshift client config search paths
	paths := []string{}
	paths = append(paths, d.ClientFlags.Lookup(config.OpenShiftConfigFlagName).Value.String())
	paths = append(paths, diagutil.AdminKubeConfigPaths...)
	paths = append(paths, config.NewOpenShiftClientConfigLoadingRules().Precedence...)

	for _, path := range paths {
		if configData, err := ioutil.ReadFile(path); err == nil {
			return configData, nil
		}
	}
	return nil, fmt.Errorf("Unable to find kube config")
}
<|endoftext|>


<commit_before>// Copyright 2020 Authors of Cilium
//
// 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 pool

import (
	"context"
	"fmt"
	"time"

	peerpb "github.com/cilium/cilium/api/v1/peer"
	hubblePeer "github.com/cilium/cilium/pkg/hubble/peer"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
)

// PeerManager defines the functions a peer manager must implement when
// handling peers and respective connections.
type PeerManager interface {
	// Start instructs the manager to start peer change notification handling
	// and connection management.
	Start()
	// Stop stops any peer manager activity.
	Stop()
	// List returns a list of peers with active connections. If a peer cannot
	// be connected to; its Conn attribute must be nil.
	List() []Peer
	// ReportOffline allows the caller to report a peer as being offline. The
	// peer is identified by its name.
	ReportOffline(name string)
}

// Peer is like hubblePeer.Peer but includes a Conn attribute to reach the
// peer's gRPC API endpoint.
type Peer struct {
	hubblePeer.Peer
	Conn ClientConn
}

type peer struct {
	hubblePeer.Peer
	conn            ClientConn
	connAttempts    int
	nextConnAttempt time.Time
	mu              lock.Mutex
}

// Manager implements the PeerManager interface.
type Manager struct {
	log     logrus.FieldLogger
	peers   map[string]*peer
	offline chan string
	mu      lock.Mutex
	opts    Options
	stop    chan struct{}
}

// ensure that Manager implements the PeerManager interface.
var _ PeerManager = (*Manager)(nil)

// NewManager creates a new manager that connects to a peer gRPC service using
// target to manage peers and a connection to every peer's gRPC API.
func NewManager(options ...Option) (*Manager, error) {
	opts := DefaultOptions
	for _, opt := range options {
		if err := opt(&opts); err != nil {
			return nil, fmt.Errorf("failed to apply option: %v", err)
		}
	}
	logger := logging.DefaultLogger.WithField(logfields.LogSubsys, "hubble-relay")
	logging.ConfigureLogLevel(opts.Debug)
	return &Manager{
		peers:   make(map[string]*peer),
		offline: make(chan string, 100),
		log:     logger,
		stop:    make(chan struct{}),
		opts:    opts,
	}, nil
}

// Start implements PeerManager.Start.
func (m *Manager) Start() {
	go m.watchNotifications()
	go m.manageConnections()
}

func (m *Manager) watchNotifications() {
	ctx, cancel := context.WithCancel(context.Background())
connect:
	for {
		cl, err := m.opts.PeerClientBuilder.Client()
		if err != nil {
			m.log.WithFields(logrus.Fields{
				"error":  err,
				"target": m.opts.PeerClientBuilder.Target(),
			}).Warning("Failed to create peer client for peers synchronization; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-time.After(m.opts.RetryTimeout):
				continue
			}
		}
		client, err := cl.Notify(ctx, &peerpb.NotifyRequest{})
		if err != nil {
			cl.Close()
			m.log.WithFields(logrus.Fields{
				"error":              err,
				"connection timeout": m.opts.RetryTimeout,
			}).Warning("Failed to create peer notify client for peers change notification; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-time.After(m.opts.RetryTimeout):
				continue
			}
		}
		for {
			select {
			case <-m.stop:
				cl.Close()
				cancel()
				return
			default:
			}
			cn, err := client.Recv()
			if err != nil {
				cl.Close()
				m.log.WithFields(logrus.Fields{
					"error":              err,
					"connection timeout": m.opts.RetryTimeout,
				}).Warning("Error while receiving peer change notification; will try again after the timeout has expired")
				select {
				case <-m.stop:
					cancel()
					return
				case <-time.After(m.opts.RetryTimeout):
					continue connect
				}
			}
			m.log.WithField("change notification", cn).Debug("Received peer change notification")
			p := hubblePeer.FromChangeNotification(cn)
			switch cn.GetType() {
			case peerpb.ChangeNotificationType_PEER_ADDED:
				m.add(p)
			case peerpb.ChangeNotificationType_PEER_DELETED:
				m.remove(p)
			case peerpb.ChangeNotificationType_PEER_UPDATED:
				m.update(p)
			}
		}
	}
}

func (m *Manager) manageConnections() {
	for {
		select {
		case <-m.stop:
			return
		case name := <-m.offline:
			m.mu.Lock()
			p := m.peers[name]
			m.mu.Unlock()
			m.connect(p)
		case <-time.After(m.opts.ConnCheckInterval):
			var retry []*peer
			m.mu.Lock()
			now := time.Now()
			for _, p := range m.peers {
				p.mu.Lock()
				if p.conn != nil {
					switch p.conn.GetState() {
					case connectivity.Connecting, connectivity.Idle, connectivity.Ready, connectivity.Shutdown:
						p.mu.Unlock()
						continue
					}
				}
				if p.nextConnAttempt.IsZero() || p.nextConnAttempt.Before(now) {
					retry = append(retry, p)
				}
				p.mu.Unlock()
			}
			m.mu.Unlock()
			for _, p := range retry {
				m.disconnect(p)
				m.connect(p)
			}
		}
	}
}

// Stop implements PeerManager.Stop.
func (m *Manager) Stop() {
	close(m.stop)
}

// List implements PeerManager.List.
func (m *Manager) List() []Peer {
	m.mu.Lock()
	defer m.mu.Unlock()
	if len(m.peers) == 0 {
		return nil
	}
	peers := make([]Peer, 0, len(m.peers))
	for _, v := range m.peers {
		// note: there shouldn't be null entries in the map
		peers = append(peers, Peer{
			Peer: hubblePeer.Peer{
				Name:    v.Name,
				Address: v.Address,
			},
			Conn: v.conn,
		})
	}
	return peers
}

// ReportOffline implements PeerManager.ReportOffline.
func (m *Manager) ReportOffline(name string) {
	go func() {
		m.mu.Lock()
		p, ok := m.peers[name]
		m.mu.Unlock()
		if !ok {
			return
		}
		p.mu.Lock()
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				// it looks like it's actually online or being brought online
				p.mu.Unlock()
				return
			}
		}
		p.mu.Unlock()
		select {
		case <-m.stop:
		case m.offline <- name:
		}
	}()
}

func (m *Manager) add(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *Manager) remove(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	m.mu.Lock()
	if p, ok := m.peers[hp.Name]; ok {
		m.disconnect(p)
		delete(m.peers, hp.Name)
	}
	m.mu.Unlock()
}

func (m *Manager) update(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	if old, ok := m.peers[p.Name]; ok {
		m.disconnect(old)
	}
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *Manager) connect(p *peer) {
	go func() {
		if p == nil {
			return
		}
		p.mu.Lock()
		defer p.mu.Unlock()
		now := time.Now()
		if p.nextConnAttempt.After(now) {
			return
		}
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				return // no need to attempt to connect
			default:
				if err := p.conn.Close(); err != nil {
					m.log.WithFields(logrus.Fields{
						"error": err,
					}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
				}
				p.conn = nil
			}
		}

		m.log.WithFields(logrus.Fields{
			"address": p.Address,
		}).Debugf("Connecting peer %s...", p.Name)
		conn, err := m.opts.ClientConnBuilder.ClientConn(p.Address.String())
		if err != nil {
			duration := m.opts.Backoff.Duration(p.connAttempts)
			p.nextConnAttempt = now.Add(duration)
			p.connAttempts++
			m.log.WithFields(logrus.Fields{
				"address": p.Address,
				"error":   err,
			}).Warningf("Failed to create gRPC client connection to peer %s; next attempt after %s", p.Name, duration)
		} else {
			p.nextConnAttempt = time.Time{}
			p.connAttempts = 0
			p.conn = conn
			m.log.Debugf("Peer %s connected", p.Name)
		}
	}()
}

func (m *Manager) disconnect(p *peer) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.conn == nil {
		return
	}
	m.log.Debugf("Disconnecting peer %s...", p.Name)
	if err := p.conn.Close(); err != nil {
		m.log.WithFields(logrus.Fields{
			"error": err,
		}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
	}
	p.conn = nil
	m.log.Debugf("Peer %s disconnected", p.Name)
}
<commit_msg>hubble/relay: ignore backoff on direct connection requests<commit_after>// Copyright 2020 Authors of Cilium
//
// 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 pool

import (
	"context"
	"fmt"
	"time"

	peerpb "github.com/cilium/cilium/api/v1/peer"
	hubblePeer "github.com/cilium/cilium/pkg/hubble/peer"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
)

// PeerManager defines the functions a peer manager must implement when
// handling peers and respective connections.
type PeerManager interface {
	// Start instructs the manager to start peer change notification handling
	// and connection management.
	Start()
	// Stop stops any peer manager activity.
	Stop()
	// List returns a list of peers with active connections. If a peer cannot
	// be connected to; its Conn attribute must be nil.
	List() []Peer
	// ReportOffline allows the caller to report a peer as being offline. The
	// peer is identified by its name.
	ReportOffline(name string)
}

// Peer is like hubblePeer.Peer but includes a Conn attribute to reach the
// peer's gRPC API endpoint.
type Peer struct {
	hubblePeer.Peer
	Conn ClientConn
}

type peer struct {
	hubblePeer.Peer
	conn            ClientConn
	connAttempts    int
	nextConnAttempt time.Time
	mu              lock.Mutex
}

// Manager implements the PeerManager interface.
type Manager struct {
	log     logrus.FieldLogger
	peers   map[string]*peer
	offline chan string
	mu      lock.Mutex
	opts    Options
	stop    chan struct{}
}

// ensure that Manager implements the PeerManager interface.
var _ PeerManager = (*Manager)(nil)

// NewManager creates a new manager that connects to a peer gRPC service using
// target to manage peers and a connection to every peer's gRPC API.
func NewManager(options ...Option) (*Manager, error) {
	opts := DefaultOptions
	for _, opt := range options {
		if err := opt(&opts); err != nil {
			return nil, fmt.Errorf("failed to apply option: %v", err)
		}
	}
	logger := logging.DefaultLogger.WithField(logfields.LogSubsys, "hubble-relay")
	logging.ConfigureLogLevel(opts.Debug)
	return &Manager{
		peers:   make(map[string]*peer),
		offline: make(chan string, 100),
		log:     logger,
		stop:    make(chan struct{}),
		opts:    opts,
	}, nil
}

// Start implements PeerManager.Start.
func (m *Manager) Start() {
	go m.watchNotifications()
	go m.manageConnections()
}

func (m *Manager) watchNotifications() {
	ctx, cancel := context.WithCancel(context.Background())
connect:
	for {
		cl, err := m.opts.PeerClientBuilder.Client()
		if err != nil {
			m.log.WithFields(logrus.Fields{
				"error":  err,
				"target": m.opts.PeerClientBuilder.Target(),
			}).Warning("Failed to create peer client for peers synchronization; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-time.After(m.opts.RetryTimeout):
				continue
			}
		}
		client, err := cl.Notify(ctx, &peerpb.NotifyRequest{})
		if err != nil {
			cl.Close()
			m.log.WithFields(logrus.Fields{
				"error":              err,
				"connection timeout": m.opts.RetryTimeout,
			}).Warning("Failed to create peer notify client for peers change notification; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-time.After(m.opts.RetryTimeout):
				continue
			}
		}
		for {
			select {
			case <-m.stop:
				cl.Close()
				cancel()
				return
			default:
			}
			cn, err := client.Recv()
			if err != nil {
				cl.Close()
				m.log.WithFields(logrus.Fields{
					"error":              err,
					"connection timeout": m.opts.RetryTimeout,
				}).Warning("Error while receiving peer change notification; will try again after the timeout has expired")
				select {
				case <-m.stop:
					cancel()
					return
				case <-time.After(m.opts.RetryTimeout):
					continue connect
				}
			}
			m.log.WithField("change notification", cn).Debug("Received peer change notification")
			p := hubblePeer.FromChangeNotification(cn)
			switch cn.GetType() {
			case peerpb.ChangeNotificationType_PEER_ADDED:
				m.add(p)
			case peerpb.ChangeNotificationType_PEER_DELETED:
				m.remove(p)
			case peerpb.ChangeNotificationType_PEER_UPDATED:
				m.update(p)
			}
		}
	}
}

func (m *Manager) manageConnections() {
	for {
		select {
		case <-m.stop:
			return
		case name := <-m.offline:
			m.mu.Lock()
			p := m.peers[name]
			m.mu.Unlock()
			// a connection request has been made, make sure to attempt a connection
			m.connect(p, true)
		case <-time.After(m.opts.ConnCheckInterval):
			var retry []*peer
			m.mu.Lock()
			now := time.Now()
			for _, p := range m.peers {
				p.mu.Lock()
				if p.conn != nil {
					switch p.conn.GetState() {
					case connectivity.Connecting, connectivity.Idle, connectivity.Ready, connectivity.Shutdown:
						p.mu.Unlock()
						continue
					}
				}
				if p.nextConnAttempt.IsZero() || p.nextConnAttempt.Before(now) {
					retry = append(retry, p)
				}
				p.mu.Unlock()
			}
			m.mu.Unlock()
			for _, p := range retry {
				m.disconnect(p)
				m.connect(p, false)
			}
		}
	}
}

// Stop implements PeerManager.Stop.
func (m *Manager) Stop() {
	close(m.stop)
}

// List implements PeerManager.List.
func (m *Manager) List() []Peer {
	m.mu.Lock()
	defer m.mu.Unlock()
	if len(m.peers) == 0 {
		return nil
	}
	peers := make([]Peer, 0, len(m.peers))
	for _, v := range m.peers {
		// note: there shouldn't be null entries in the map
		peers = append(peers, Peer{
			Peer: hubblePeer.Peer{
				Name:    v.Name,
				Address: v.Address,
			},
			Conn: v.conn,
		})
	}
	return peers
}

// ReportOffline implements PeerManager.ReportOffline.
func (m *Manager) ReportOffline(name string) {
	go func() {
		m.mu.Lock()
		p, ok := m.peers[name]
		m.mu.Unlock()
		if !ok {
			return
		}
		p.mu.Lock()
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				// it looks like it's actually online or being brought online
				p.mu.Unlock()
				return
			}
		}
		p.mu.Unlock()
		select {
		case <-m.stop:
		case m.offline <- name:
		}
	}()
}

func (m *Manager) add(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *Manager) remove(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	m.mu.Lock()
	if p, ok := m.peers[hp.Name]; ok {
		m.disconnect(p)
		delete(m.peers, hp.Name)
	}
	m.mu.Unlock()
}

func (m *Manager) update(hp *hubblePeer.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	if old, ok := m.peers[p.Name]; ok {
		m.disconnect(old)
	}
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *Manager) connect(p *peer, ignoreBackoff bool) {
	go func() {
		if p == nil {
			return
		}
		p.mu.Lock()
		defer p.mu.Unlock()
		now := time.Now()
		if p.nextConnAttempt.After(now) && !ignoreBackoff {
			return
		}
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				return // no need to attempt to connect
			default:
				if err := p.conn.Close(); err != nil {
					m.log.WithFields(logrus.Fields{
						"error": err,
					}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
				}
				p.conn = nil
			}
		}

		m.log.WithFields(logrus.Fields{
			"address": p.Address,
		}).Debugf("Connecting peer %s...", p.Name)
		conn, err := m.opts.ClientConnBuilder.ClientConn(p.Address.String())
		if err != nil {
			duration := m.opts.Backoff.Duration(p.connAttempts)
			p.nextConnAttempt = now.Add(duration)
			p.connAttempts++
			m.log.WithFields(logrus.Fields{
				"address": p.Address,
				"error":   err,
			}).Warningf("Failed to create gRPC client connection to peer %s; next attempt after %s", p.Name, duration)
		} else {
			p.nextConnAttempt = time.Time{}
			p.connAttempts = 0
			p.conn = conn
			m.log.Debugf("Peer %s connected", p.Name)
		}
	}()
}

func (m *Manager) disconnect(p *peer) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.conn == nil {
		return
	}
	m.log.Debugf("Disconnecting peer %s...", p.Name)
	if err := p.conn.Close(); err != nil {
		m.log.WithFields(logrus.Fields{
			"error": err,
		}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
	}
	p.conn = nil
	m.log.Debugf("Peer %s disconnected", p.Name)
}
<|endoftext|>


<commit_before>// Copyright 2020 Authors of Cilium
//
// 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 pool

import (
	"context"
	"fmt"
	"sync"
	"time"

	peerpb "github.com/cilium/cilium/api/v1/peer"
	peerTypes "github.com/cilium/cilium/pkg/hubble/peer/types"
	poolTypes "github.com/cilium/cilium/pkg/hubble/relay/pool/types"
	"github.com/cilium/cilium/pkg/inctimer"
	"github.com/cilium/cilium/pkg/lock"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
)

type peer struct {
	mu lock.Mutex
	peerTypes.Peer
	conn            poolTypes.ClientConn
	connAttempts    int
	nextConnAttempt time.Time
}

// PeerManager manages a pool of peers (Peer) and associated gRPC connections.
// Peers and peer change notifications are obtained from a peer gRPC service.
type PeerManager struct {
	opts    options
	offline chan string
	wg      sync.WaitGroup
	stop    chan struct{}
	mu      lock.RWMutex
	peers   map[string]*peer
}

// NewPeerManager creates a new manager that connects to a peer gRPC service to
// manage peers and a connection to every peer's gRPC API.
func NewPeerManager(options ...Option) (*PeerManager, error) {
	opts := defaultOptions
	for _, opt := range options {
		if err := opt(&opts); err != nil {
			return nil, fmt.Errorf("failed to apply option: %v", err)
		}
	}
	return &PeerManager{
		peers:   make(map[string]*peer),
		offline: make(chan string, 100),
		stop:    make(chan struct{}),
		opts:    opts,
	}, nil
}

// Start starts the manager.
func (m *PeerManager) Start() {
	m.wg.Add(2)
	go func() {
		defer m.wg.Done()
		m.watchNotifications()
	}()
	go func() {
		defer m.wg.Done()
		m.manageConnections()
	}()
}

func (m *PeerManager) watchNotifications() {
	ctx, cancel := context.WithCancel(context.Background())
	retryTimer, retryTimerDone := inctimer.New()
	defer retryTimerDone()
connect:
	for {
		cl, err := m.opts.peerClientBuilder.Client(m.opts.peerServiceAddress)
		if err != nil {
			m.opts.log.WithFields(logrus.Fields{
				"error":  err,
				"target": m.opts.peerServiceAddress,
			}).Warning("Failed to create peer client for peers synchronization; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-retryTimer.After(m.opts.retryTimeout):
				continue
			}
		}
		client, err := cl.Notify(ctx, &peerpb.NotifyRequest{})
		if err != nil {
			cl.Close()
			m.opts.log.WithFields(logrus.Fields{
				"error":              err,
				"connection timeout": m.opts.retryTimeout,
			}).Warning("Failed to create peer notify client for peers change notification; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-retryTimer.After(m.opts.retryTimeout):
				continue
			}
		}
		for {
			select {
			case <-m.stop:
				cl.Close()
				cancel()
				return
			default:
			}
			cn, err := client.Recv()
			if err != nil {
				cl.Close()
				m.opts.log.WithFields(logrus.Fields{
					"error":              err,
					"connection timeout": m.opts.retryTimeout,
				}).Warning("Error while receiving peer change notification; will try again after the timeout has expired")
				select {
				case <-m.stop:
					cancel()
					return
				case <-retryTimer.After(m.opts.retryTimeout):
					continue connect
				}
			}
			m.opts.log.WithField("change notification", cn).Debug("Received peer change notification")
			p := peerTypes.FromChangeNotification(cn)
			switch cn.GetType() {
			case peerpb.ChangeNotificationType_PEER_ADDED:
				m.add(p)
			case peerpb.ChangeNotificationType_PEER_DELETED:
				m.remove(p)
			case peerpb.ChangeNotificationType_PEER_UPDATED:
				m.update(p)
			}
		}
	}
}

func (m *PeerManager) manageConnections() {
	connTimer, connTimerDone := inctimer.New()
	defer connTimerDone()
	for {
		select {
		case <-m.stop:
			return
		case name := <-m.offline:
			m.mu.RLock()
			p := m.peers[name]
			m.mu.RUnlock()
			m.wg.Add(1)
			go func() {
				defer m.wg.Done()
				// a connection request has been made, make sure to attempt a connection
				m.connect(p, true)
			}()
		case <-connTimer.After(m.opts.connCheckInterval):
			m.mu.RLock()
			now := time.Now()
			for _, p := range m.peers {
				p.mu.Lock()
				if p.conn != nil {
					switch p.conn.GetState() {
					case connectivity.Connecting, connectivity.Idle, connectivity.Ready, connectivity.Shutdown:
						p.mu.Unlock()
						continue
					}
				}
				switch {
				case p.nextConnAttempt.IsZero(), p.nextConnAttempt.Before(now):
					p.mu.Unlock()
					m.wg.Add(1)
					go func() {
						defer m.wg.Done()
						m.connect(p, false)
					}()
				default:
					p.mu.Unlock()
				}
			}
			m.mu.RUnlock()
		}
	}
}

// Stop stops the manaager.
func (m *PeerManager) Stop() {
	close(m.stop)
	m.wg.Wait()
}

// List implements observer.PeerLister.List.
func (m *PeerManager) List() []poolTypes.Peer {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.peers) == 0 {
		return nil
	}
	peers := make([]poolTypes.Peer, 0, len(m.peers))
	for _, v := range m.peers {
		// note: there shouldn't be null entries in the map
		peers = append(peers, poolTypes.Peer{
			Peer: peerTypes.Peer{
				Name:          v.Name,
				Address:       v.Address,
				TLSEnabled:    v.TLSEnabled,
				TLSServerName: v.TLSServerName,
			},
			Conn: v.conn,
		})
	}
	return peers
}

// ReportOffline implements observer.PeerReporter.ReportOffline.
func (m *PeerManager) ReportOffline(name string) {
	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		m.mu.RLock()
		p, ok := m.peers[name]
		m.mu.RUnlock()
		if !ok {
			return
		}
		p.mu.Lock()
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				// it looks like it's actually online or being brought online
				p.mu.Unlock()
				return
			}
		}
		p.mu.Unlock()
		select {
		case <-m.stop:
		case m.offline <- name:
		}
	}()
}

func (m *PeerManager) add(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *PeerManager) remove(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	m.mu.Lock()
	if p, ok := m.peers[hp.Name]; ok {
		m.disconnect(p)
		delete(m.peers, hp.Name)
	}
	m.mu.Unlock()
}

func (m *PeerManager) update(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	if old, ok := m.peers[p.Name]; ok {
		m.disconnect(old)
	}
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *PeerManager) connect(p *peer, ignoreBackoff bool) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	now := time.Now()
	if p.Address == nil || (p.nextConnAttempt.After(now) && !ignoreBackoff) {
		return
	}
	if p.conn != nil {
		switch p.conn.GetState() {
		case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
			return // no need to attempt to connect
		default:
			if err := p.conn.Close(); err != nil {
				m.opts.log.WithFields(logrus.Fields{
					"error": err,
				}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
			}
			p.conn = nil
		}
	}

	m.opts.log.WithFields(logrus.Fields{
		"address": p.Address,
	}).Debugf("Connecting peer %s...", p.Name)
	conn, err := m.opts.clientConnBuilder.ClientConn(p.Address.String(), p.TLSServerName)
	if err != nil {
		duration := m.opts.backoff.Duration(p.connAttempts)
		p.nextConnAttempt = now.Add(duration)
		p.connAttempts++
		m.opts.log.WithFields(logrus.Fields{
			"address": p.Address,
			"error":   err,
		}).Warningf("Failed to create gRPC client connection to peer %s; next attempt after %s", p.Name, duration)
	} else {
		p.nextConnAttempt = time.Time{}
		p.connAttempts = 0
		p.conn = conn
		m.opts.log.Debugf("Peer %s connected", p.Name)
	}
}

func (m *PeerManager) disconnect(p *peer) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.conn == nil {
		return
	}
	m.opts.log.Debugf("Disconnecting peer %s...", p.Name)
	if err := p.conn.Close(); err != nil {
		m.opts.log.WithFields(logrus.Fields{
			"error": err,
		}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
	}
	p.conn = nil
	m.opts.log.Debugf("Peer %s disconnected", p.Name)
}
<commit_msg>hubble/relay: raise the log level of messages<commit_after>// Copyright 2020 Authors of Cilium
//
// 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 pool

import (
	"context"
	"fmt"
	"sync"
	"time"

	peerpb "github.com/cilium/cilium/api/v1/peer"
	peerTypes "github.com/cilium/cilium/pkg/hubble/peer/types"
	poolTypes "github.com/cilium/cilium/pkg/hubble/relay/pool/types"
	"github.com/cilium/cilium/pkg/inctimer"
	"github.com/cilium/cilium/pkg/lock"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc/connectivity"
)

type peer struct {
	mu lock.Mutex
	peerTypes.Peer
	conn            poolTypes.ClientConn
	connAttempts    int
	nextConnAttempt time.Time
}

// PeerManager manages a pool of peers (Peer) and associated gRPC connections.
// Peers and peer change notifications are obtained from a peer gRPC service.
type PeerManager struct {
	opts    options
	offline chan string
	wg      sync.WaitGroup
	stop    chan struct{}
	mu      lock.RWMutex
	peers   map[string]*peer
}

// NewPeerManager creates a new manager that connects to a peer gRPC service to
// manage peers and a connection to every peer's gRPC API.
func NewPeerManager(options ...Option) (*PeerManager, error) {
	opts := defaultOptions
	for _, opt := range options {
		if err := opt(&opts); err != nil {
			return nil, fmt.Errorf("failed to apply option: %v", err)
		}
	}
	return &PeerManager{
		peers:   make(map[string]*peer),
		offline: make(chan string, 100),
		stop:    make(chan struct{}),
		opts:    opts,
	}, nil
}

// Start starts the manager.
func (m *PeerManager) Start() {
	m.wg.Add(2)
	go func() {
		defer m.wg.Done()
		m.watchNotifications()
	}()
	go func() {
		defer m.wg.Done()
		m.manageConnections()
	}()
}

func (m *PeerManager) watchNotifications() {
	ctx, cancel := context.WithCancel(context.Background())
	retryTimer, retryTimerDone := inctimer.New()
	defer retryTimerDone()
connect:
	for {
		cl, err := m.opts.peerClientBuilder.Client(m.opts.peerServiceAddress)
		if err != nil {
			m.opts.log.WithFields(logrus.Fields{
				"error":  err,
				"target": m.opts.peerServiceAddress,
			}).Warning("Failed to create peer client for peers synchronization; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-retryTimer.After(m.opts.retryTimeout):
				continue
			}
		}
		client, err := cl.Notify(ctx, &peerpb.NotifyRequest{})
		if err != nil {
			cl.Close()
			m.opts.log.WithFields(logrus.Fields{
				"error":              err,
				"connection timeout": m.opts.retryTimeout,
			}).Warning("Failed to create peer notify client for peers change notification; will try again after the timeout has expired")
			select {
			case <-m.stop:
				cancel()
				return
			case <-retryTimer.After(m.opts.retryTimeout):
				continue
			}
		}
		for {
			select {
			case <-m.stop:
				cl.Close()
				cancel()
				return
			default:
			}
			cn, err := client.Recv()
			if err != nil {
				cl.Close()
				m.opts.log.WithFields(logrus.Fields{
					"error":              err,
					"connection timeout": m.opts.retryTimeout,
				}).Warning("Error while receiving peer change notification; will try again after the timeout has expired")
				select {
				case <-m.stop:
					cancel()
					return
				case <-retryTimer.After(m.opts.retryTimeout):
					continue connect
				}
			}
			m.opts.log.WithField("change notification", cn).Info("Received peer change notification")
			p := peerTypes.FromChangeNotification(cn)
			switch cn.GetType() {
			case peerpb.ChangeNotificationType_PEER_ADDED:
				m.add(p)
			case peerpb.ChangeNotificationType_PEER_DELETED:
				m.remove(p)
			case peerpb.ChangeNotificationType_PEER_UPDATED:
				m.update(p)
			}
		}
	}
}

func (m *PeerManager) manageConnections() {
	connTimer, connTimerDone := inctimer.New()
	defer connTimerDone()
	for {
		select {
		case <-m.stop:
			return
		case name := <-m.offline:
			m.mu.RLock()
			p := m.peers[name]
			m.mu.RUnlock()
			m.wg.Add(1)
			go func() {
				defer m.wg.Done()
				// a connection request has been made, make sure to attempt a connection
				m.connect(p, true)
			}()
		case <-connTimer.After(m.opts.connCheckInterval):
			m.mu.RLock()
			now := time.Now()
			for _, p := range m.peers {
				p.mu.Lock()
				if p.conn != nil {
					switch p.conn.GetState() {
					case connectivity.Connecting, connectivity.Idle, connectivity.Ready, connectivity.Shutdown:
						p.mu.Unlock()
						continue
					}
				}
				switch {
				case p.nextConnAttempt.IsZero(), p.nextConnAttempt.Before(now):
					p.mu.Unlock()
					m.wg.Add(1)
					go func() {
						defer m.wg.Done()
						m.connect(p, false)
					}()
				default:
					p.mu.Unlock()
				}
			}
			m.mu.RUnlock()
		}
	}
}

// Stop stops the manaager.
func (m *PeerManager) Stop() {
	close(m.stop)
	m.wg.Wait()
}

// List implements observer.PeerLister.List.
func (m *PeerManager) List() []poolTypes.Peer {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.peers) == 0 {
		return nil
	}
	peers := make([]poolTypes.Peer, 0, len(m.peers))
	for _, v := range m.peers {
		// note: there shouldn't be null entries in the map
		peers = append(peers, poolTypes.Peer{
			Peer: peerTypes.Peer{
				Name:          v.Name,
				Address:       v.Address,
				TLSEnabled:    v.TLSEnabled,
				TLSServerName: v.TLSServerName,
			},
			Conn: v.conn,
		})
	}
	return peers
}

// ReportOffline implements observer.PeerReporter.ReportOffline.
func (m *PeerManager) ReportOffline(name string) {
	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		m.mu.RLock()
		p, ok := m.peers[name]
		m.mu.RUnlock()
		if !ok {
			return
		}
		p.mu.Lock()
		if p.conn != nil {
			switch p.conn.GetState() {
			case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
				// it looks like it's actually online or being brought online
				p.mu.Unlock()
				return
			}
		}
		p.mu.Unlock()
		select {
		case <-m.stop:
		case m.offline <- name:
		}
	}()
}

func (m *PeerManager) add(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *PeerManager) remove(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	m.mu.Lock()
	if p, ok := m.peers[hp.Name]; ok {
		m.disconnect(p)
		delete(m.peers, hp.Name)
	}
	m.mu.Unlock()
}

func (m *PeerManager) update(hp *peerTypes.Peer) {
	if hp == nil {
		return
	}
	p := &peer{Peer: *hp}
	m.mu.Lock()
	if old, ok := m.peers[p.Name]; ok {
		m.disconnect(old)
	}
	m.peers[p.Name] = p
	m.mu.Unlock()
	select {
	case <-m.stop:
	case m.offline <- p.Name:
	}
}

func (m *PeerManager) connect(p *peer, ignoreBackoff bool) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	now := time.Now()
	if p.Address == nil || (p.nextConnAttempt.After(now) && !ignoreBackoff) {
		return
	}
	if p.conn != nil {
		switch p.conn.GetState() {
		case connectivity.Connecting, connectivity.Idle, connectivity.Ready:
			return // no need to attempt to connect
		default:
			if err := p.conn.Close(); err != nil {
				m.opts.log.WithFields(logrus.Fields{
					"error": err,
				}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
			}
			p.conn = nil
		}
	}

	m.opts.log.WithFields(logrus.Fields{
		"address": p.Address,
	}).Infof("Connecting peer %s...", p.Name)
	conn, err := m.opts.clientConnBuilder.ClientConn(p.Address.String(), p.TLSServerName)
	if err != nil {
		duration := m.opts.backoff.Duration(p.connAttempts)
		p.nextConnAttempt = now.Add(duration)
		p.connAttempts++
		m.opts.log.WithFields(logrus.Fields{
			"address": p.Address,
			"error":   err,
		}).Warningf("Failed to create gRPC client connection to peer %s; next attempt after %s", p.Name, duration)
	} else {
		p.nextConnAttempt = time.Time{}
		p.connAttempts = 0
		p.conn = conn
		m.opts.log.Infof("Peer %s connected", p.Name)
	}
}

func (m *PeerManager) disconnect(p *peer) {
	if p == nil {
		return
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.conn == nil {
		return
	}
	m.opts.log.Infof("Disconnecting peer %s...", p.Name)
	if err := p.conn.Close(); err != nil {
		m.opts.log.WithFields(logrus.Fields{
			"error": err,
		}).Warningf("Failed to properly close gRPC client connection to peer %s", p.Name)
	}
	p.conn = nil
	m.opts.log.Infof("Peer %s disconnected", p.Name)
}
<|endoftext|>


<commit_before>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 util

import (
	"fmt"
	"io"
	"strings"

	"k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/kubectl"

	"github.com/spf13/cobra"
)

// AddPrinterFlags adds printing related flags to a command (e.g. output format, no headers, template path)
func AddPrinterFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md].")
	cmd.Flags().String("output-version", "", "Output the formatted object with the given group version (for ex: 'extensions/v1beta1').")
	cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.")
	cmd.Flags().Bool("show-labels", false, "When printing, show all labels as the last column (default hide labels column)")
	cmd.Flags().String("template", "", "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
	cmd.MarkFlagFilename("template")
	cmd.Flags().String("sort-by", "", "If non-empty, sort list types using this field specification.  The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
	cmd.Flags().BoolP("show-all", "a", false, "When printing, show all resources (default hide terminated pods.)")
}

// AddOutputFlagsForMutation adds output related flags to a command. Used by mutations only.
func AddOutputFlagsForMutation(cmd *cobra.Command) {
	cmd.Flags().StringP("output", "o", "", "Output mode. Use \"-o name\" for shorter output (resource/name).")
}

// PrintSuccess prints message after finishing mutating operations
func PrintSuccess(mapper meta.RESTMapper, shortOutput bool, out io.Writer, resource string, name string, operation string) {
	resource, _ = mapper.ResourceSingularizer(resource)
	if shortOutput {
		// -o name: prints resource/name
		if len(resource) > 0 {
			fmt.Fprintf(out, "%s/%s\n", resource, name)
		} else {
			fmt.Fprintf(out, "%s\n", name)
		}
	} else {
		// understandable output by default
		if len(resource) > 0 {
			fmt.Fprintf(out, "%s \"%s\" %s\n", resource, name, operation)
		} else {
			fmt.Fprintf(out, "\"%s\" %s\n", name, operation)
		}
	}
}

// ValidateOutputArgs validates -o flag args for mutations
func ValidateOutputArgs(cmd *cobra.Command) error {
	outputMode := GetFlagString(cmd, "output")
	if outputMode != "" && outputMode != "name" {
		return UsageError(cmd, "Unexpected -o output mode: %v. We only support '-o name'.", outputMode)
	}
	return nil
}

// OutputVersion returns the preferred output version for generic content (JSON, YAML, or templates)
// defaultVersion is never mutated.  Nil simply allows clean passing in common usage from client.Config
func OutputVersion(cmd *cobra.Command, defaultVersion *unversioned.GroupVersion) (unversioned.GroupVersion, error) {
	outputVersionString := GetFlagString(cmd, "output-version")
	if len(outputVersionString) == 0 {
		if defaultVersion == nil {
			return unversioned.GroupVersion{}, nil
		}

		return *defaultVersion, nil
	}

	return unversioned.ParseGroupVersion(outputVersionString)
}

// PrinterForCommand returns the default printer for this command.
// Requires that printer flags have been added to cmd (see AddPrinterFlags).
func PrinterForCommand(cmd *cobra.Command) (kubectl.ResourcePrinter, bool, error) {
	outputFormat := GetFlagString(cmd, "output")

	// templates are logically optional for specifying a format.
	// TODO once https://github.com/kubernetes/kubernetes/issues/12668 is fixed, this should fall back to GetFlagString
	templateFile, _ := cmd.Flags().GetString("template")
	if len(outputFormat) == 0 && len(templateFile) != 0 {
		outputFormat = "template"
	}

	templateFormat := []string{
		"go-template=", "go-template-file=", "jsonpath=", "jsonpath-file=", "custom-columns=", "custom-columns-file=",
	}
	for _, format := range templateFormat {
		if strings.HasPrefix(outputFormat, format) {
			templateFile = outputFormat[len(format):]
			outputFormat = format[:len(format)-1]
		}
	}

	printer, generic, err := kubectl.GetPrinter(outputFormat, templateFile)
	if err != nil {
		return nil, generic, err
	}

	return maybeWrapSortingPrinter(cmd, printer), generic, nil
}

func maybeWrapSortingPrinter(cmd *cobra.Command, printer kubectl.ResourcePrinter) kubectl.ResourcePrinter {
	sorting, err := cmd.Flags().GetString("sort-by")
	if err != nil {
		// error can happen on missing flag or bad flag type.  In either case, this command didn't intent to sort
		return printer
	}

	if len(sorting) != 0 {
		return &kubectl.SortingPrinter{
			Delegate:  printer,
			SortField: fmt.Sprintf("{%s}", sorting),
		}
	}
	return printer
}
<commit_msg>Update the jsonpath template URL<commit_after>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 util

import (
	"fmt"
	"io"
	"strings"

	"k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/kubectl"

	"github.com/spf13/cobra"
)

// AddPrinterFlags adds printing related flags to a command (e.g. output format, no headers, template path)
func AddPrinterFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://kubernetes.io/docs/user-guide/jsonpath].")
	cmd.Flags().String("output-version", "", "Output the formatted object with the given group version (for ex: 'extensions/v1beta1').")
	cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.")
	cmd.Flags().Bool("show-labels", false, "When printing, show all labels as the last column (default hide labels column)")
	cmd.Flags().String("template", "", "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
	cmd.MarkFlagFilename("template")
	cmd.Flags().String("sort-by", "", "If non-empty, sort list types using this field specification.  The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
	cmd.Flags().BoolP("show-all", "a", false, "When printing, show all resources (default hide terminated pods.)")
}

// AddOutputFlagsForMutation adds output related flags to a command. Used by mutations only.
func AddOutputFlagsForMutation(cmd *cobra.Command) {
	cmd.Flags().StringP("output", "o", "", "Output mode. Use \"-o name\" for shorter output (resource/name).")
}

// PrintSuccess prints message after finishing mutating operations
func PrintSuccess(mapper meta.RESTMapper, shortOutput bool, out io.Writer, resource string, name string, operation string) {
	resource, _ = mapper.ResourceSingularizer(resource)
	if shortOutput {
		// -o name: prints resource/name
		if len(resource) > 0 {
			fmt.Fprintf(out, "%s/%s\n", resource, name)
		} else {
			fmt.Fprintf(out, "%s\n", name)
		}
	} else {
		// understandable output by default
		if len(resource) > 0 {
			fmt.Fprintf(out, "%s \"%s\" %s\n", resource, name, operation)
		} else {
			fmt.Fprintf(out, "\"%s\" %s\n", name, operation)
		}
	}
}

// ValidateOutputArgs validates -o flag args for mutations
func ValidateOutputArgs(cmd *cobra.Command) error {
	outputMode := GetFlagString(cmd, "output")
	if outputMode != "" && outputMode != "name" {
		return UsageError(cmd, "Unexpected -o output mode: %v. We only support '-o name'.", outputMode)
	}
	return nil
}

// OutputVersion returns the preferred output version for generic content (JSON, YAML, or templates)
// defaultVersion is never mutated.  Nil simply allows clean passing in common usage from client.Config
func OutputVersion(cmd *cobra.Command, defaultVersion *unversioned.GroupVersion) (unversioned.GroupVersion, error) {
	outputVersionString := GetFlagString(cmd, "output-version")
	if len(outputVersionString) == 0 {
		if defaultVersion == nil {
			return unversioned.GroupVersion{}, nil
		}

		return *defaultVersion, nil
	}

	return unversioned.ParseGroupVersion(outputVersionString)
}

// PrinterForCommand returns the default printer for this command.
// Requires that printer flags have been added to cmd (see AddPrinterFlags).
func PrinterForCommand(cmd *cobra.Command) (kubectl.ResourcePrinter, bool, error) {
	outputFormat := GetFlagString(cmd, "output")

	// templates are logically optional for specifying a format.
	// TODO once https://github.com/kubernetes/kubernetes/issues/12668 is fixed, this should fall back to GetFlagString
	templateFile, _ := cmd.Flags().GetString("template")
	if len(outputFormat) == 0 && len(templateFile) != 0 {
		outputFormat = "template"
	}

	templateFormat := []string{
		"go-template=", "go-template-file=", "jsonpath=", "jsonpath-file=", "custom-columns=", "custom-columns-file=",
	}
	for _, format := range templateFormat {
		if strings.HasPrefix(outputFormat, format) {
			templateFile = outputFormat[len(format):]
			outputFormat = format[:len(format)-1]
		}
	}

	printer, generic, err := kubectl.GetPrinter(outputFormat, templateFile)
	if err != nil {
		return nil, generic, err
	}

	return maybeWrapSortingPrinter(cmd, printer), generic, nil
}

func maybeWrapSortingPrinter(cmd *cobra.Command, printer kubectl.ResourcePrinter) kubectl.ResourcePrinter {
	sorting, err := cmd.Flags().GetString("sort-by")
	if err != nil {
		// error can happen on missing flag or bad flag type.  In either case, this command didn't intent to sort
		return printer
	}

	if len(sorting) != 0 {
		return &kubectl.SortingPrinter{
			Delegate:  printer,
			SortField: fmt.Sprintf("{%s}", sorting),
		}
	}
	return printer
}
<|endoftext|>


<commit_before>/*
Copyright (C) 2017 Red Hat, Inc.

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 profile

import (
	"fmt"
	"io/ioutil"
	"path/filepath"

	"github.com/minishift/minishift/pkg/minikube/constants"
	"github.com/minishift/minishift/pkg/minishift/config"
	"github.com/minishift/minishift/pkg/util/filehelper"
)

// Returns the list of profile names
func GetProfileList() []string {
	var profileList []string
	baseDir := constants.GetMinishiftHomeDir()
	profileBaseDir := filepath.Join(baseDir, "profiles")

	if !filehelper.IsDirectory(baseDir) {
		return profileList
	}
	profileList = append(profileList, constants.DefaultProfileName)

	if !filehelper.IsDirectory(profileBaseDir) {
		return profileList
	}

	files, err := ioutil.ReadDir(profileBaseDir)
	if err != nil {
		return profileList
	}

	for _, f := range files {
		profileList = append(profileList, f.Name())
	}
	return profileList
}

// Set Active Profile and also it makes sure that we have one
// active profile at one point of time.
func SetActiveProfile(name string) error {
	activeProfile := config.AllInstancesConfig.ActiveProfile
	if name != activeProfile {
		config.AllInstancesConfig.ActiveProfile = name
	}
	err := config.AllInstancesConfig.Write()
	if err != nil {
		return fmt.Errorf("Error updating active profile information for '%s' in config. %s", name, err)
	}
	return nil
}

// Get Active Profile from AllInstancesConfig
func GetActiveProfile() string {
	return config.AllInstancesConfig.ActiveProfile
}

// Placeholder function to change variables related to a VM instance
// This needs a better solution than this as these variables should not be
// changed outside of cmd/root.go. However cluster.GetHostStatus(api) uses
// constants.MachineName inside the function.
// This is a temporary fix and we will findout a better way to do it.
func UpdateProfileConstants(profileName string) {
	constants.ProfileName = profileName
	constants.MachineName = constants.ProfileName
	constants.Minipath = constants.GetProfileHomeDir(constants.ProfileName)
}

func SetDefaultProfileActive() error {
	err := SetActiveProfile(constants.DefaultProfileName)
	if err != nil {
		return fmt.Errorf("Error while setting default profile '%s' active: %s", constants.DefaultProfileName, err.Error())
	}
	return nil
}
<commit_msg>Issue #1840 Ignore non-dir artifects inside profiles dir<commit_after>/*
Copyright (C) 2017 Red Hat, Inc.

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 profile

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"regexp"

	"github.com/minishift/minishift/pkg/minikube/constants"
	"github.com/minishift/minishift/pkg/minishift/config"
	"github.com/minishift/minishift/pkg/util/filehelper"
)

// Returns the list of profile names
func GetProfileList() []string {
	var profileList []string
	baseDir := constants.GetMinishiftHomeDir()
	profileBaseDir := filepath.Join(baseDir, "profiles")

	if !filehelper.IsDirectory(baseDir) {
		return profileList
	}
	profileList = append(profileList, constants.DefaultProfileName)

	if !filehelper.IsDirectory(profileBaseDir) {
		return profileList
	}

	files, err := ioutil.ReadDir(profileBaseDir)
	if err != nil {
		return profileList
	}

	for _, f := range files {
		// Skip non-directory and hidden stuffs
		match, _ := regexp.MatchString("^\\.", f.Name())
		if !filehelper.IsDirectory(filepath.Join(profileBaseDir, f.Name())) || match {
			continue
		}
		profileList = append(profileList, f.Name())
	}
	return profileList
}

// Set Active Profile and also it makes sure that we have one
// active profile at one point of time.
func SetActiveProfile(name string) error {
	activeProfile := config.AllInstancesConfig.ActiveProfile
	if name != activeProfile {
		config.AllInstancesConfig.ActiveProfile = name
	}
	err := config.AllInstancesConfig.Write()
	if err != nil {
		return fmt.Errorf("Error updating active profile information for '%s' in config. %s", name, err)
	}
	return nil
}

// Get Active Profile from AllInstancesConfig
func GetActiveProfile() string {
	return config.AllInstancesConfig.ActiveProfile
}

// Placeholder function to change variables related to a VM instance
// This needs a better solution than this as these variables should not be
// changed outside of cmd/root.go. However cluster.GetHostStatus(api) uses
// constants.MachineName inside the function.
// This is a temporary fix and we will findout a better way to do it.
func UpdateProfileConstants(profileName string) {
	constants.ProfileName = profileName
	constants.MachineName = constants.ProfileName
	constants.Minipath = constants.GetProfileHomeDir(constants.ProfileName)
}

func SetDefaultProfileActive() error {
	err := SetActiveProfile(constants.DefaultProfileName)
	if err != nil {
		return fmt.Errorf("Error while setting default profile '%s' active: %s", constants.DefaultProfileName, err.Error())
	}
	return nil
}
<|endoftext|>