<commit_before>// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package agent

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	k8sLabels "k8s.io/apimachinery/pkg/labels"
	"k8s.io/client-go/informers"
	v1 "k8s.io/client-go/listers/core/v1"
	"k8s.io/client-go/tools/cache"

	"github.com/cilium/cilium/pkg/bgpv1"
	"github.com/cilium/cilium/pkg/k8s"
	v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
	"github.com/cilium/cilium/pkg/k8s/client/informers/externalversions"
	"github.com/cilium/cilium/pkg/k8s/client/listers/cilium.io/v2alpha1"
	slimlabels "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
	slimmetav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	nodeaddr "github.com/cilium/cilium/pkg/node"
	nodetypes "github.com/cilium/cilium/pkg/node/types"
)

var (
	log                   = logging.DefaultLogger.WithField(logfields.LogSubsys, "bgp-control-plane")
	defaultInformerResync = 30 * time.Second
)

var (
	// ErrMultiplePolicies is a static error typed when the controller encounters
	// multiple policies which apply to its host.
	ErrMultiplePolicies = fmt.Errorf("more then one CiliumBGPPeeringPolicy applies to this node, please ensure only a single Policy matches this node's labels")
)

// Signaler multiplexes multiple event sources into a single level-triggered
// event.
//
// Signaler should always be constructed with a channel of size 1.
//
// Use of a Signaler allows for bursts of events to be "rolled-up".
// This is a suitable approach since the Controller checks the entire state of
// the world on each iteration of its control loop.
//
// Additionally, this precludes any need for ordering between different event
// sources.
type Signaler struct {
	Sig chan struct{}
}

// NewSignaler constructs a Signaler
func NewSignaler() Signaler {
	return Signaler{
		Sig: make(chan struct{}, 1),
	}
}

// Event adds an edge triggered event to the Signaler.
//
// A controller which uses this Signaler will be notified of this event some
// time after.
//
// This signature adheres to the common event handling signatures of
// cache.ResourceEventHandlerFuncs for convenience.
func (s Signaler) Event(_ interface{}) {
	select {
	case s.Sig <- struct{}{}:
	default:
	}
}

// ControlPlaneState captures a subset of Cilium's runtime state.
//
// This state carries information interesting to various BGP sub-systems
// and provides a contract for information a sub-system will be provided
// about Cilium's runtime state.
//
// ControlPlaneState should be a point-in-time snapshot of Cilium's runtime
// state and remain read-only to all sub systems its passed to.
type ControlPlaneState struct {
	// The Kubernetes Node resource at time of construction.
	Node *corev1.Node
	// Parsed 'cilium.io/bgp-virtual-router' annotations of the the node this
	// control plane is running on.
	Annotations bgpv1.AnnotationMap
	// The current IPv4 address of the agent, reachable externally.
	IPv4 net.IP
	// The current IPv6 address of the agent, reachable externally.
	IPv6 net.IP
}

// Controller is the agent side BGP Control Plane controller.
//
// Controller listens for events and drives BGP related sub-systems
// to maintain a desired state.
type Controller struct {
	// NOTE: Swap the usage of Node here to CiliumNode
	// once #18609 merges
	//
	// NodeInformer is a shared informer for Node
	// resources
	NodeInformer cache.SharedIndexInformer
	// NodeLister provides cached and indexed lookups for
	// Node API objects.
	NodeLister v1.NodeLister
	// BGPPolicyInformer is a shared informer for CiliumBGPPeerinPolicy
	BGPPolicyInformer cache.SharedIndexInformer
	// PolicyLister provides cached and indexed lookups of
	// for CilumBGPPeeringPolicy API objects.
	PolicyLister v2alpha1.CiliumBGPPeeringPolicyLister
	// Sig informs the Controller that a Kubernetes
	// event of interest has occurred.
	//
	// The signal itself provides no other information,
	// when it occurs the Controller will query each
	// informer for the latest API information required
	// to drive it's control loop.
	Sig Signaler
	// BGPMgr is an implementation of the BGPRouterManager interface
	// and provides a declarative API for configuring BGP peers.
	BGPMgr BGPRouterManager
}

// ControllerOpt is a signature for defining configurable options for a
// Controller
type ControllerOpt func(*Controller)

// NewController constructs a new BGP Control Plane Controller.
//
// When the constructor returns the Controller will be actively watching for
// events and configuring BGP related sub-systems.
//
// The constructor requires an implementation of BGPRouterManager to be provided.
// This implementation defines which BGP backend will be used (GoBGP, FRR, Bird, etc...)
// NOTE: only GoBGP currently implemented.
//
// Cancel the provided CTX to stop the Controller.
func NewController(ctx context.Context, rtMgr BGPRouterManager, opts ...ControllerOpt) (*Controller, error) {
	// create the signaler we will use
	sig := NewSignaler()

	// informers setup
	clientset := k8s.CiliumClient()
	factory := externalversions.NewSharedInformerFactory(clientset, defaultInformerResync)
	k8sfactory := informers.NewSharedInformerFactory(k8s.Client(), defaultInformerResync)

	nodeLister := k8sfactory.Core().V1().Nodes().Lister()
	nodeInformer := k8sfactory.Core().V1().Nodes().Informer()
	nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    sig.Event,
		UpdateFunc: func(_ interface{}, _ interface{}) { sig.Event(struct{}{}) },
		DeleteFunc: sig.Event,
	})

	policyLister := factory.Cilium().V2alpha1().CiliumBGPPeeringPolicies().Lister()
	policyInformer := factory.Cilium().V2alpha1().CiliumBGPPeeringPolicies().Informer()
	policyInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    sig.Event,
		UpdateFunc: func(_ interface{}, _ interface{}) { sig.Event(struct{}{}) },
		DeleteFunc: sig.Event,
	})

	c := Controller{
		Sig:               sig,
		NodeInformer:      nodeInformer,
		NodeLister:        nodeLister,
		BGPPolicyInformer: policyInformer,
		PolicyLister:      policyLister,
		BGPMgr:            rtMgr,
	}

	// apply any options.
	for _, opt := range opts {
		opt(&c)
	}

	go c.Run(ctx)
	return &c, nil
}

// Run places the Controller into its control loop.
//
// Kubernetes shared informers are started just before entering the long running
// loop.
//
// When new events trigger a signal the control loop will be evaluated.
//
// A cancel of the provided ctx will kill the control loop along with the running
// informers.
func (c *Controller) Run(ctx context.Context) {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "Controller.Run",
		})
	)
	l.Debug("Starting informers")

	// add an initial signal to kick things off
	c.Sig.Event(struct{}{})

	stop := make(chan struct{})
	go c.NodeInformer.Run(stop)
	go c.BGPPolicyInformer.Run(stop)

	l.Info("Cilium BGP Control Plane Controller now running...")
	for {
		select {
		case <-ctx.Done():
			killCTX, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
			defer cancel()

			close(stop)               // kills the informers
			c.FullWithdrawal(killCTX) // kill any BGP sessions

			l.Info("Cilium BGP Control Plane Controller shut down")
			return
		case <-c.Sig.Sig:
			l.Info("Cilium BGP Control Plane Controller woken for reconciliation")
			if err := c.Reconcile(ctx); err != nil {
				l.WithError(err).Error("Encountered error during reconciliation")
			} else {
				l.Debug("Successfully completed reconciliation")
			}
		}
	}
}

// PolicySelection returns a CiliumBGPPeeringPolicy which applies to the provided
// *corev1.Node, enforced by a set of policy selection rules.
//
// Policy selection follows the following rules:
// - A policy matches a node if said policy's "nodeSelector" field matches
//   the node's
// - If (N > 1) policies match the provided *corev1.Node an error is returned.
//   only a single policy may apply to a node to avoid ambiguity at this stage
//   of development.
func PolicySelection(ctx context.Context, node *corev1.Node, policies []*v2alpha1api.CiliumBGPPeeringPolicy) (*v2alpha1api.CiliumBGPPeeringPolicy, error) {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "PolicySelection",
		})
	)
	// determine which policies match our node's labels.
	var (
		selected   *v2alpha1api.CiliumBGPPeeringPolicy
		slimLabels = slimlabels.Set(node.Labels)
	)

	// range over policies and see if any match this node's labels.
	//
	// for now, only a single BGP policy can be applied to a node. if more then
	// one policy applies to a node, we disconnect from all BGP peers and log
	// an error.
	for _, policy := range policies {
		nodeSelector, err := slimmetav1.LabelSelectorAsSelector(policy.Spec.NodeSelector)
		if err != nil {
			l.WithError(err).Error("Failed to convert CiliumBGPPeeringPolicy's NodeSelector to a label.Selector interface")
		}
		l.WithFields(logrus.Fields{
			"policyNodeSelector": nodeSelector.String(),
			"nodeLabels":         slimLabels,
		}).Debug("Comparing BGP policy node selector with node's labels")
		if nodeSelector.Matches(slimLabels) {
			if selected != nil {
				return nil, ErrMultiplePolicies
			}
			selected = policy
		}
	}

	// no policy was discovered, tell router manager to withdrawal peers if they
	// are configured.
	return selected, nil
}

// Reconcile is the control loop for the Controller.
//
// Reconcile will be invoked when one or more event sources trigger a signal
// via the Controller's Signaler structure.
//
// On signal, Reconcile will obtain the state of the world necessary to drive
// the BGP control plane toward any new BGP peering policies.
//
// Reconcile will only allow a single CiliumBGPPeeringPolicy to apply to the
// node its running on.
func (c *Controller) Reconcile(ctx context.Context) error {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "Controller.Reconcile",
		})
		nodeName = nodetypes.GetName()
	)

	// retrieve Node structure.
	node, err := c.NodeLister.Get(nodeName)
	if err != nil {
		return fmt.Errorf("failed to list Node with name %s: %v", nodeName, err)
	}
	if node == nil {
		return fmt.Errorf("list for Node with %s returned nil", nodeName)
	}
	l.WithFields(logrus.Fields{"Labels": node.Labels, "Name": node.Name}).Debug("Successfully listed Node")

	// retrieve all CiliumBGPPeeringPolicies
	policies, err := c.PolicyLister.List(k8sLabels.NewSelector())
	if err != nil {
		return fmt.Errorf("failed to list CiliumBGPPeeringPolicies")
	}
	l.WithField("count", len(policies)).Debug("Successfully listed CiliumBGPPeeringPolicies")

	// perform policy selection based on node.
	policy, err := PolicySelection(ctx, node, policies)
	if err != nil {
		l.WithError(err).Error("Policy selection failed")
		c.FullWithdrawal(ctx)
		return err
	}
	if policy == nil {
		// no policy was discovered, tell router manager to withdrawal peers if
		// they are configured.
		l.Debug("No BGP peering policy applies to this node, any existing BGP sessions will be removed.")
		c.FullWithdrawal(ctx)
		return nil
	}

	// parse any virtual router specific attributes defined on this node via
	// kubernetes annotations
	//
	// if we notice one or more malformed annotations report the errors up and
	// fail reconciliation.
	annoMap, err := bgpv1.NewAnnotationMap(node)
	if err != nil {
		return fmt.Errorf("failed to parse annotations: %w", err)
	}

	// define our current point-in-time control plane state.
	state := &ControlPlaneState{
		Node:        node,
		Annotations: annoMap,
		IPv4:        nodeaddr.GetIPv4(),
		IPv6:        nodeaddr.GetIPv6(),
	}

	// call bgp sub-systems required to apply this policy's BGP topology.
	l.Debug("Asking configured BGPRouterManager to configure peering")
	if err := c.BGPMgr.ConfigurePeers(ctx, policy, state); err != nil {
		return fmt.Errorf("failed to configure BGP peers, cannot apply BGP peering policy: %w", err)
	}

	return nil
}

// FullWithdrawal will instruct the configured BGPRouterManager to withdraw all
// BGP servers and peers.
func (c *Controller) FullWithdrawal(ctx context.Context) {
	_ = c.BGPMgr.ConfigurePeers(ctx, nil, nil) // cannot fail, no need for error handling
}
<commit_msg>bgp: remove informer resync, server side filter node listings<commit_after>// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package agent

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sLabels "k8s.io/apimachinery/pkg/labels"
	"k8s.io/client-go/informers"
	v1 "k8s.io/client-go/listers/core/v1"
	"k8s.io/client-go/tools/cache"

	"github.com/cilium/cilium/pkg/bgpv1"
	"github.com/cilium/cilium/pkg/k8s"
	v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
	"github.com/cilium/cilium/pkg/k8s/client/informers/externalversions"
	"github.com/cilium/cilium/pkg/k8s/client/listers/cilium.io/v2alpha1"
	slimlabels "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
	slimmetav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	nodeaddr "github.com/cilium/cilium/pkg/node"
	nodetypes "github.com/cilium/cilium/pkg/node/types"
)

var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "bgp-control-plane")
)

var (
	// ErrMultiplePolicies is a static error typed when the controller encounters
	// multiple policies which apply to its host.
	ErrMultiplePolicies = fmt.Errorf("more then one CiliumBGPPeeringPolicy applies to this node, please ensure only a single Policy matches this node's labels")
)

// Signaler multiplexes multiple event sources into a single level-triggered
// event.
//
// Signaler should always be constructed with a channel of size 1.
//
// Use of a Signaler allows for bursts of events to be "rolled-up".
// This is a suitable approach since the Controller checks the entire state of
// the world on each iteration of its control loop.
//
// Additionally, this precludes any need for ordering between different event
// sources.
type Signaler struct {
	Sig chan struct{}
}

// NewSignaler constructs a Signaler
func NewSignaler() Signaler {
	return Signaler{
		Sig: make(chan struct{}, 1),
	}
}

// Event adds an edge triggered event to the Signaler.
//
// A controller which uses this Signaler will be notified of this event some
// time after.
//
// This signature adheres to the common event handling signatures of
// cache.ResourceEventHandlerFuncs for convenience.
func (s Signaler) Event(_ interface{}) {
	select {
	case s.Sig <- struct{}{}:
	default:
	}
}

// ControlPlaneState captures a subset of Cilium's runtime state.
//
// This state carries information interesting to various BGP sub-systems
// and provides a contract for information a sub-system will be provided
// about Cilium's runtime state.
//
// ControlPlaneState should be a point-in-time snapshot of Cilium's runtime
// state and remain read-only to all sub systems its passed to.
type ControlPlaneState struct {
	// The Kubernetes Node resource at time of construction.
	Node *corev1.Node
	// Parsed 'cilium.io/bgp-virtual-router' annotations of the the node this
	// control plane is running on.
	Annotations bgpv1.AnnotationMap
	// The current IPv4 address of the agent, reachable externally.
	IPv4 net.IP
	// The current IPv6 address of the agent, reachable externally.
	IPv6 net.IP
}

// Controller is the agent side BGP Control Plane controller.
//
// Controller listens for events and drives BGP related sub-systems
// to maintain a desired state.
type Controller struct {
	// NOTE: Swap the usage of Node here to CiliumNode
	// once #18609 merges
	//
	// NodeInformer is a shared informer for Node
	// resources
	NodeInformer cache.SharedIndexInformer
	// NodeLister provides cached and indexed lookups for
	// Node API objects.
	NodeLister v1.NodeLister
	// BGPPolicyInformer is a shared informer for CiliumBGPPeerinPolicy
	BGPPolicyInformer cache.SharedIndexInformer
	// PolicyLister provides cached and indexed lookups of
	// for CilumBGPPeeringPolicy API objects.
	PolicyLister v2alpha1.CiliumBGPPeeringPolicyLister
	// Sig informs the Controller that a Kubernetes
	// event of interest has occurred.
	//
	// The signal itself provides no other information,
	// when it occurs the Controller will query each
	// informer for the latest API information required
	// to drive it's control loop.
	Sig Signaler
	// BGPMgr is an implementation of the BGPRouterManager interface
	// and provides a declarative API for configuring BGP peers.
	BGPMgr BGPRouterManager
}

// ControllerOpt is a signature for defining configurable options for a
// Controller
type ControllerOpt func(*Controller)

// NewController constructs a new BGP Control Plane Controller.
//
// When the constructor returns the Controller will be actively watching for
// events and configuring BGP related sub-systems.
//
// The constructor requires an implementation of BGPRouterManager to be provided.
// This implementation defines which BGP backend will be used (GoBGP, FRR, Bird, etc...)
// NOTE: only GoBGP currently implemented.
//
// Cancel the provided CTX to stop the Controller.
func NewController(ctx context.Context, rtMgr BGPRouterManager, opts ...ControllerOpt) (*Controller, error) {
	// create the signaler we will use
	sig := NewSignaler()

	// informers setup
	clientset := k8s.CiliumClient()
	factory := externalversions.NewSharedInformerFactory(clientset, 0)

	selfTweakList := informers.WithTweakListOptions(func(lo *metav1.ListOptions) {
		lo.FieldSelector = "metadata.name=" + nodetypes.GetName()
	})
	k8sfactory := informers.NewSharedInformerFactoryWithOptions(k8s.Client(), 0, selfTweakList)

	nodeLister := k8sfactory.Core().V1().Nodes().Lister()
	nodeInformer := k8sfactory.Core().V1().Nodes().Informer()
	nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    sig.Event,
		UpdateFunc: func(_ interface{}, _ interface{}) { sig.Event(struct{}{}) },
		DeleteFunc: sig.Event,
	})

	policyLister := factory.Cilium().V2alpha1().CiliumBGPPeeringPolicies().Lister()
	policyInformer := factory.Cilium().V2alpha1().CiliumBGPPeeringPolicies().Informer()
	policyInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    sig.Event,
		UpdateFunc: func(_ interface{}, _ interface{}) { sig.Event(struct{}{}) },
		DeleteFunc: sig.Event,
	})

	c := Controller{
		Sig:               sig,
		NodeInformer:      nodeInformer,
		NodeLister:        nodeLister,
		BGPPolicyInformer: policyInformer,
		PolicyLister:      policyLister,
		BGPMgr:            rtMgr,
	}

	// apply any options.
	for _, opt := range opts {
		opt(&c)
	}

	go c.Run(ctx)
	return &c, nil
}

// Run places the Controller into its control loop.
//
// Kubernetes shared informers are started just before entering the long running
// loop.
//
// When new events trigger a signal the control loop will be evaluated.
//
// A cancel of the provided ctx will kill the control loop along with the running
// informers.
func (c *Controller) Run(ctx context.Context) {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "Controller.Run",
		})
	)
	l.Debug("Starting informers")

	// add an initial signal to kick things off
	c.Sig.Event(struct{}{})

	stop := make(chan struct{})
	go c.NodeInformer.Run(stop)
	go c.BGPPolicyInformer.Run(stop)

	l.Info("Cilium BGP Control Plane Controller now running...")
	for {
		select {
		case <-ctx.Done():
			killCTX, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
			defer cancel()

			close(stop)               // kills the informers
			c.FullWithdrawal(killCTX) // kill any BGP sessions

			l.Info("Cilium BGP Control Plane Controller shut down")
			return
		case <-c.Sig.Sig:
			l.Info("Cilium BGP Control Plane Controller woken for reconciliation")
			if err := c.Reconcile(ctx); err != nil {
				l.WithError(err).Error("Encountered error during reconciliation")
			} else {
				l.Debug("Successfully completed reconciliation")
			}
		}
	}
}

// PolicySelection returns a CiliumBGPPeeringPolicy which applies to the provided
// *corev1.Node, enforced by a set of policy selection rules.
//
// Policy selection follows the following rules:
// - A policy matches a node if said policy's "nodeSelector" field matches
//   the node's
// - If (N > 1) policies match the provided *corev1.Node an error is returned.
//   only a single policy may apply to a node to avoid ambiguity at this stage
//   of development.
func PolicySelection(ctx context.Context, node *corev1.Node, policies []*v2alpha1api.CiliumBGPPeeringPolicy) (*v2alpha1api.CiliumBGPPeeringPolicy, error) {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "PolicySelection",
		})
	)
	// determine which policies match our node's labels.
	var (
		selected   *v2alpha1api.CiliumBGPPeeringPolicy
		slimLabels = slimlabels.Set(node.Labels)
	)

	// range over policies and see if any match this node's labels.
	//
	// for now, only a single BGP policy can be applied to a node. if more then
	// one policy applies to a node, we disconnect from all BGP peers and log
	// an error.
	for _, policy := range policies {
		nodeSelector, err := slimmetav1.LabelSelectorAsSelector(policy.Spec.NodeSelector)
		if err != nil {
			l.WithError(err).Error("Failed to convert CiliumBGPPeeringPolicy's NodeSelector to a label.Selector interface")
		}
		l.WithFields(logrus.Fields{
			"policyNodeSelector": nodeSelector.String(),
			"nodeLabels":         slimLabels,
		}).Debug("Comparing BGP policy node selector with node's labels")
		if nodeSelector.Matches(slimLabels) {
			if selected != nil {
				return nil, ErrMultiplePolicies
			}
			selected = policy
		}
	}

	// no policy was discovered, tell router manager to withdrawal peers if they
	// are configured.
	return selected, nil
}

// Reconcile is the control loop for the Controller.
//
// Reconcile will be invoked when one or more event sources trigger a signal
// via the Controller's Signaler structure.
//
// On signal, Reconcile will obtain the state of the world necessary to drive
// the BGP control plane toward any new BGP peering policies.
//
// Reconcile will only allow a single CiliumBGPPeeringPolicy to apply to the
// node its running on.
func (c *Controller) Reconcile(ctx context.Context) error {
	var (
		l = log.WithFields(logrus.Fields{
			"component": "Controller.Reconcile",
		})
		nodeName = nodetypes.GetName()
	)

	// retrieve Node structure.
	node, err := c.NodeLister.Get(nodeName)
	if err != nil {
		return fmt.Errorf("failed to list Node with name %s: %v", nodeName, err)
	}
	if node == nil {
		return fmt.Errorf("list for Node with %s returned nil", nodeName)
	}
	l.WithFields(logrus.Fields{"Labels": node.Labels, "Name": node.Name}).Debug("Successfully listed Node")

	// retrieve all CiliumBGPPeeringPolicies
	policies, err := c.PolicyLister.List(k8sLabels.NewSelector())
	if err != nil {
		return fmt.Errorf("failed to list CiliumBGPPeeringPolicies")
	}
	l.WithField("count", len(policies)).Debug("Successfully listed CiliumBGPPeeringPolicies")

	// perform policy selection based on node.
	policy, err := PolicySelection(ctx, node, policies)
	if err != nil {
		l.WithError(err).Error("Policy selection failed")
		c.FullWithdrawal(ctx)
		return err
	}
	if policy == nil {
		// no policy was discovered, tell router manager to withdrawal peers if
		// they are configured.
		l.Debug("No BGP peering policy applies to this node, any existing BGP sessions will be removed.")
		c.FullWithdrawal(ctx)
		return nil
	}

	// parse any virtual router specific attributes defined on this node via
	// kubernetes annotations
	//
	// if we notice one or more malformed annotations report the errors up and
	// fail reconciliation.
	annoMap, err := bgpv1.NewAnnotationMap(node)
	if err != nil {
		return fmt.Errorf("failed to parse annotations: %w", err)
	}

	// define our current point-in-time control plane state.
	state := &ControlPlaneState{
		Node:        node,
		Annotations: annoMap,
		IPv4:        nodeaddr.GetIPv4(),
		IPv6:        nodeaddr.GetIPv6(),
	}

	// call bgp sub-systems required to apply this policy's BGP topology.
	l.Debug("Asking configured BGPRouterManager to configure peering")
	if err := c.BGPMgr.ConfigurePeers(ctx, policy, state); err != nil {
		return fmt.Errorf("failed to configure BGP peers, cannot apply BGP peering policy: %w", err)
	}

	return nil
}

// FullWithdrawal will instruct the configured BGPRouterManager to withdraw all
// BGP servers and peers.
func (c *Controller) FullWithdrawal(ctx context.Context) {
	_ = c.BGPMgr.ConfigurePeers(ctx, nil, nil) // cannot fail, no need for error handling
}
<|endoftext|>


<commit_before>// Copyright 2017-2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package syslinux implements a syslinux config file parser.
//
// See http://www.syslinux.org/wiki/index.php?title=Config for general syslinux
// config features.
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
package syslinux

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/url"
	"path/filepath"
	"strings"

	"github.com/u-root/u-root/pkg/boot"
	"github.com/u-root/u-root/pkg/boot/multiboot"
	"github.com/u-root/u-root/pkg/curl"
	"github.com/u-root/u-root/pkg/uio"
)

func probeIsolinuxFiles() []string {
	files := make([]string, 0, 10)
	// search order from the syslinux wiki
	// http://wiki.syslinux.org/wiki/index.php?title=Config
	// TODO: do we want to handle extlinux too ?
	dirs := []string{
		"boot/isolinux",
		"isolinux",
		"boot/syslinux",
		"syslinux",
		"",
	}
	confs := []string{
		"isolinux.cfg",
		"syslinux.cfg",
	}
	for _, dir := range dirs {
		for _, conf := range confs {
			if dir == "" {
				files = append(files, conf)
			} else {
				files = append(files, filepath.Join(dir, conf))
			}
		}
	}
	return files
}

// ParseLocalConfig treats diskDir like a mount point on the local file system
// and finds an isolinux config under there.
func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) {
	for _, relname := range probeIsolinuxFiles() {
		dir, name := filepath.Split(relname)

		// "When booting, the initial working directory for SYSLINUX /
		// ISOLINUX will be the directory containing the initial
		// configuration file."
		//
		// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
		wd := &url.URL{
			Scheme: "file",
			Path:   filepath.Join(diskDir, dir),
		}

		imgs, err := ParseConfigFile(ctx, curl.DefaultSchemes, name, wd)
		if curl.IsURLError(err) {
			continue
		}
		return imgs, err
	}
	return nil, fmt.Errorf("no valid syslinux config found on %s", diskDir)
}

// ParseConfigFile parses a Syslinux configuration as specified in
// http://www.syslinux.org/wiki/index.php?title=Config
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
//
// `s` is used to fetch any files that must be parsed or provided.
//
// `wd` is the default scheme, host, and path for any files named as a
// relative path - e.g. kernel, include, and initramfs paths are requested
// relative to the wd.
func ParseConfigFile(ctx context.Context, s curl.Schemes, url string, wd *url.URL) ([]boot.OSImage, error) {
	p := newParser(wd, s)
	if err := p.appendFile(ctx, url); err != nil {
		return nil, err
	}

	// Intended order:
	//
	// 1. nerfDefaultEntry
	// 2. defaultEntry
	// 3. labels in order they appeared in config
	if len(p.labelOrder) == 0 {
		return nil, nil
	}
	if len(p.defaultEntry) > 0 {
		p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...)
	}
	if len(p.nerfDefaultEntry) > 0 {
		p.labelOrder = append([]string{p.nerfDefaultEntry}, p.labelOrder...)
	}
	p.labelOrder = dedupStrings(p.labelOrder)

	var images []boot.OSImage
	for _, label := range p.labelOrder {
		if img, ok := p.linuxEntries[label]; ok && img.Kernel != nil {
			images = append(images, img)
		}
		if img, ok := p.mbEntries[label]; ok && img.Kernel != nil {
			images = append(images, img)
		}
	}
	return images, nil
}

func dedupStrings(list []string) []string {
	var newList []string
	seen := make(map[string]struct{})
	for _, s := range list {
		if _, ok := seen[s]; !ok {
			seen[s] = struct{}{}
			newList = append(newList, s)
		}
	}
	return newList
}

type parser struct {
	// linuxEntries is a map of label name -> label configuration.
	linuxEntries map[string]*boot.LinuxImage
	mbEntries    map[string]*boot.MultibootImage

	// labelOrder is the order of label entries in linuxEntries.
	labelOrder []string

	defaultEntry     string
	nerfDefaultEntry string

	// parser internals.
	globalAppend string
	scope        scope
	curEntry     string
	wd           *url.URL
	schemes      curl.Schemes
}

type scope uint8

const (
	scopeGlobal scope = iota
	scopeEntry
)

// newParser returns a new PXE parser using working directory `wd`
// and schemes `s`.
//
// If a path encountered in a configuration file is relative instead of a full
// URL, `wd` is used as the "working directory" of that relative path; the
// resulting URL is roughly `wd.String()/path`.
//
// `s` is used to get files referred to by URLs.
func newParser(wd *url.URL, s curl.Schemes) *parser {
	return &parser{
		linuxEntries: make(map[string]*boot.LinuxImage),
		mbEntries:    make(map[string]*boot.MultibootImage),
		scope:        scopeGlobal,
		wd:           wd,
		schemes:      s,
	}
}

func parseURL(surl string, wd *url.URL) (*url.URL, error) {
	u, err := url.Parse(surl)
	if err != nil {
		return nil, fmt.Errorf("could not parse URL %q: %v", surl, err)
	}

	if len(u.Scheme) == 0 && wd != nil {
		u.Scheme = wd.Scheme

		if len(u.Host) == 0 {
			// If this is not there, it was likely just a path.
			u.Host = wd.Host
			u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path))
		}
	}
	return u, nil
}

// getFile parses `url` relative to the config's working directory and returns
// an io.Reader for the requested url.
//
// If url is just a relative path and not a full URL, c.wd is used as the
// "working directory" of that relative path; the resulting URL is roughly
// path.Join(wd.String(), url).
func (c *parser) getFile(url string) (io.ReaderAt, error) {
	u, err := parseURL(url, c.wd)
	if err != nil {
		return nil, err
	}

	return c.schemes.LazyFetch(u)
}

// appendFile parses the config file downloaded from `url` and adds it to `c`.
func (c *parser) appendFile(ctx context.Context, url string) error {
	u, err := parseURL(url, c.wd)
	if err != nil {
		return err
	}

	r, err := c.schemes.Fetch(ctx, u)
	if err != nil {
		return err
	}
	config, err := uio.ReadAll(r)
	if err != nil {
		return err
	}
	log.Printf("Got config file %s:\n%s\n", r, string(config))
	return c.append(ctx, string(config))
}

// Append parses `config` and adds the respective configuration to `c`.
func (c *parser) append(ctx context.Context, config string) error {
	// Here's a shitty parser.
	for _, line := range strings.Split(config, "\n") {
		// This is stupid. There should be a FieldsN(...).
		kv := strings.Fields(line)
		if len(kv) <= 1 {
			continue
		}
		directive := strings.ToLower(kv[0])
		var arg string
		if len(kv) == 2 {
			arg = kv[1]
		} else {
			arg = strings.Join(kv[1:], " ")
		}

		switch directive {
		case "default":
			c.defaultEntry = arg

		case "nerfdefault":
			c.nerfDefaultEntry = arg

		case "include":
			if err := c.appendFile(ctx, arg); curl.IsURLError(err) {
				log.Printf("failed to parse %s: %v", arg, err)
				// Means we didn't find the file. Just ignore
				// it.
				// TODO(hugelgupf): plumb a logger through here.
				continue
			} else if err != nil {
				return err
			}

		case "menu":
			opt := strings.Fields(arg)
			if len(opt) < 1 {
				continue
			}
			switch strings.ToLower(opt[0]) {
			case "label":
				// Note that "menu label" only changes the
				// displayed label, not the identifier for this
				// entry.
				if e, ok := c.linuxEntries[c.curEntry]; ok && len(opt) > 1 {
					e.Name = strings.Join(opt[1:], " ")
				}

			case "default":
				// Are we in label scope?
				//
				// "Only valid after a LABEL statement" -syslinux wiki.
				if c.scope == scopeEntry {
					c.defaultEntry = c.curEntry
				}
			}

		case "label":
			// We forever enter label scope.
			c.scope = scopeEntry
			c.curEntry = arg
			c.linuxEntries[c.curEntry] = &boot.LinuxImage{
				Cmdline: c.globalAppend,
				Name:    c.curEntry,
			}
			c.labelOrder = append(c.labelOrder, c.curEntry)

		case "kernel":
			// I hate special cases like these, but we aren't gonna
			// implement syslinux modules.
			if arg == "mboot.c32" {
				// Prepare for a multiboot kernel.
				delete(c.linuxEntries, c.curEntry)
				c.mbEntries[c.curEntry] = &boot.MultibootImage{
					Name: c.curEntry,
				}
			}
			fallthrough

		case "linux":
			if e, ok := c.linuxEntries[c.curEntry]; ok {
				k, err := c.getFile(arg)
				if err != nil {
					return err
				}
				e.Kernel = k
			}

		case "initrd":
			if e, ok := c.linuxEntries[c.curEntry]; ok {
				// TODO: support multiple comma-separated initrds.
				// TODO: append "initrd=$arg" to the cmdline.
				//
				// For how this interacts with global appends,
				// read
				// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
				i, err := c.getFile(arg)
				if err != nil {
					return err
				}
				e.Initrd = i
			}

		case "append":
			switch c.scope {
			case scopeGlobal:
				c.globalAppend = arg

			case scopeEntry:
				if e, ok := c.mbEntries[c.curEntry]; ok {
					modules := strings.Split(arg, "---")
					// The first module is special -- the kernel.
					if len(modules) > 0 {
						kernel := strings.Fields(modules[0])
						k, err := c.getFile(kernel[0])
						if err != nil {
							return err
						}
						e.Kernel = k
						if len(kernel) > 0 {
							e.Cmdline = strings.Join(kernel[1:], " ")
						}
						modules = modules[1:]
					}
					for _, cmdline := range modules {
						m := strings.Fields(cmdline)
						if len(m) == 0 {
							continue
						}
						name := m[0]
						file, err := c.getFile(name)
						if err != nil {
							return err
						}
						e.Modules = append(e.Modules, multiboot.Module{
							CmdLine: strings.Join(m, " "),
							Name:    name,
							Module:  file,
						})
					}
				}
				if e, ok := c.linuxEntries[c.curEntry]; ok {
					if arg == "-" {
						e.Cmdline = ""
					} else {
						// Yes, we explicitly _override_, not
						// concatenate. If a specific append
						// directive is present, a global
						// append directive is ignored.
						//
						// Also, "If you enter multiple APPEND
						// statements in a single LABEL entry,
						// only the last one will be used".
						//
						// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
						e.Cmdline = arg
					}
				}
			}
		}
	}

	// Go through all labels and download the initrds.
	for _, label := range c.linuxEntries {
		// If the initrd was set via the INITRD directive, don't
		// overwrite that.
		//
		// TODO(hugelgupf): Is this really what syslinux does? Does
		// INITRD trump cmdline? Does it trump global? What if both the
		// directive and cmdline initrd= are set? Does it depend on the
		// order in the config file? (My current best guess: order.)
		//
		// Answer: Normally, the INITRD directive appends to the
		// cmdline, and the _last_ effective initrd= parameter is used
		// for loading initrd files.
		if label.Initrd != nil {
			continue
		}

		for _, opt := range strings.Fields(label.Cmdline) {
			optkv := strings.Split(opt, "=")
			if optkv[0] != "initrd" {
				continue
			}

			i, err := c.getFile(optkv[1])
			if err != nil {
				return err
			}
			label.Initrd = i
		}
	}
	return nil

}
<commit_msg>syslinux: fix menu label implementatation<commit_after>// Copyright 2017-2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package syslinux implements a syslinux config file parser.
//
// See http://www.syslinux.org/wiki/index.php?title=Config for general syslinux
// config features.
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
package syslinux

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/url"
	"path/filepath"
	"strings"

	"github.com/u-root/u-root/pkg/boot"
	"github.com/u-root/u-root/pkg/boot/multiboot"
	"github.com/u-root/u-root/pkg/curl"
	"github.com/u-root/u-root/pkg/uio"
)

func probeIsolinuxFiles() []string {
	files := make([]string, 0, 10)
	// search order from the syslinux wiki
	// http://wiki.syslinux.org/wiki/index.php?title=Config
	// TODO: do we want to handle extlinux too ?
	dirs := []string{
		"boot/isolinux",
		"isolinux",
		"boot/syslinux",
		"syslinux",
		"",
	}
	confs := []string{
		"isolinux.cfg",
		"syslinux.cfg",
	}
	for _, dir := range dirs {
		for _, conf := range confs {
			if dir == "" {
				files = append(files, conf)
			} else {
				files = append(files, filepath.Join(dir, conf))
			}
		}
	}
	return files
}

// ParseLocalConfig treats diskDir like a mount point on the local file system
// and finds an isolinux config under there.
func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) {
	for _, relname := range probeIsolinuxFiles() {
		dir, name := filepath.Split(relname)

		// "When booting, the initial working directory for SYSLINUX /
		// ISOLINUX will be the directory containing the initial
		// configuration file."
		//
		// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
		wd := &url.URL{
			Scheme: "file",
			Path:   filepath.Join(diskDir, dir),
		}

		imgs, err := ParseConfigFile(ctx, curl.DefaultSchemes, name, wd)
		if curl.IsURLError(err) {
			continue
		}
		return imgs, err
	}
	return nil, fmt.Errorf("no valid syslinux config found on %s", diskDir)
}

// ParseConfigFile parses a Syslinux configuration as specified in
// http://www.syslinux.org/wiki/index.php?title=Config
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
//
// `s` is used to fetch any files that must be parsed or provided.
//
// `wd` is the default scheme, host, and path for any files named as a
// relative path - e.g. kernel, include, and initramfs paths are requested
// relative to the wd.
func ParseConfigFile(ctx context.Context, s curl.Schemes, url string, wd *url.URL) ([]boot.OSImage, error) {
	p := newParser(wd, s)
	if err := p.appendFile(ctx, url); err != nil {
		return nil, err
	}

	// Assign the right label to display to users.
	for label, displayLabel := range p.menuLabel {
		if e, ok := p.linuxEntries[label]; ok {
			e.Name = displayLabel
		}
		if e, ok := p.mbEntries[label]; ok {
			e.Name = displayLabel
		}
	}

	// Intended order:
	//
	// 1. nerfDefaultEntry
	// 2. defaultEntry
	// 3. labels in order they appeared in config
	if len(p.labelOrder) == 0 {
		return nil, nil
	}
	if len(p.defaultEntry) > 0 {
		p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...)
	}
	if len(p.nerfDefaultEntry) > 0 {
		p.labelOrder = append([]string{p.nerfDefaultEntry}, p.labelOrder...)
	}
	p.labelOrder = dedupStrings(p.labelOrder)

	var images []boot.OSImage
	for _, label := range p.labelOrder {
		if img, ok := p.linuxEntries[label]; ok && img.Kernel != nil {
			images = append(images, img)
		}
		if img, ok := p.mbEntries[label]; ok && img.Kernel != nil {
			images = append(images, img)
		}
	}
	return images, nil
}

func dedupStrings(list []string) []string {
	var newList []string
	seen := make(map[string]struct{})
	for _, s := range list {
		if _, ok := seen[s]; !ok {
			seen[s] = struct{}{}
			newList = append(newList, s)
		}
	}
	return newList
}

type parser struct {
	// linuxEntries is a map of label name -> label configuration.
	linuxEntries map[string]*boot.LinuxImage
	mbEntries    map[string]*boot.MultibootImage

	// labelOrder is the order of label entries in linuxEntries.
	labelOrder []string

	// menuLabel are human-readable labels defined by the "menu label" directive.
	menuLabel map[string]string

	defaultEntry     string
	nerfDefaultEntry string

	// parser internals.
	globalAppend string
	scope        scope
	curEntry     string
	wd           *url.URL
	schemes      curl.Schemes
}

type scope uint8

const (
	scopeGlobal scope = iota
	scopeEntry
)

// newParser returns a new PXE parser using working directory `wd`
// and schemes `s`.
//
// If a path encountered in a configuration file is relative instead of a full
// URL, `wd` is used as the "working directory" of that relative path; the
// resulting URL is roughly `wd.String()/path`.
//
// `s` is used to get files referred to by URLs.
func newParser(wd *url.URL, s curl.Schemes) *parser {
	return &parser{
		linuxEntries: make(map[string]*boot.LinuxImage),
		mbEntries:    make(map[string]*boot.MultibootImage),
		scope:        scopeGlobal,
		wd:           wd,
		schemes:      s,
		menuLabel:    make(map[string]string),
	}
}

func parseURL(surl string, wd *url.URL) (*url.URL, error) {
	u, err := url.Parse(surl)
	if err != nil {
		return nil, fmt.Errorf("could not parse URL %q: %v", surl, err)
	}

	if len(u.Scheme) == 0 && wd != nil {
		u.Scheme = wd.Scheme

		if len(u.Host) == 0 {
			// If this is not there, it was likely just a path.
			u.Host = wd.Host
			u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path))
		}
	}
	return u, nil
}

// getFile parses `url` relative to the config's working directory and returns
// an io.Reader for the requested url.
//
// If url is just a relative path and not a full URL, c.wd is used as the
// "working directory" of that relative path; the resulting URL is roughly
// path.Join(wd.String(), url).
func (c *parser) getFile(url string) (io.ReaderAt, error) {
	u, err := parseURL(url, c.wd)
	if err != nil {
		return nil, err
	}

	return c.schemes.LazyFetch(u)
}

// appendFile parses the config file downloaded from `url` and adds it to `c`.
func (c *parser) appendFile(ctx context.Context, url string) error {
	u, err := parseURL(url, c.wd)
	if err != nil {
		return err
	}

	r, err := c.schemes.Fetch(ctx, u)
	if err != nil {
		return err
	}
	config, err := uio.ReadAll(r)
	if err != nil {
		return err
	}
	log.Printf("Got config file %s:\n%s\n", r, string(config))
	return c.append(ctx, string(config))
}

// Append parses `config` and adds the respective configuration to `c`.
func (c *parser) append(ctx context.Context, config string) error {
	// Here's a shitty parser.
	for _, line := range strings.Split(config, "\n") {
		// This is stupid. There should be a FieldsN(...).
		kv := strings.Fields(line)
		if len(kv) <= 1 {
			continue
		}
		directive := strings.ToLower(kv[0])
		var arg string
		if len(kv) == 2 {
			arg = kv[1]
		} else {
			arg = strings.Join(kv[1:], " ")
		}

		switch directive {
		case "default":
			c.defaultEntry = arg

		case "nerfdefault":
			c.nerfDefaultEntry = arg

		case "include":
			if err := c.appendFile(ctx, arg); curl.IsURLError(err) {
				log.Printf("failed to parse %s: %v", arg, err)
				// Means we didn't find the file. Just ignore
				// it.
				// TODO(hugelgupf): plumb a logger through here.
				continue
			} else if err != nil {
				return err
			}

		case "menu":
			opt := strings.Fields(arg)
			if len(opt) < 1 {
				continue
			}
			switch strings.ToLower(opt[0]) {
			case "label":
				// Note that "menu label" only changes the
				// displayed label, not the identifier for this
				// entry.
				//
				// We track these separately because "menu
				// label" directives may happen before we know
				// whether this is a Linux or Multiboot entry.
				c.menuLabel[c.curEntry] = strings.Join(opt[1:], " ")

			case "default":
				// Are we in label scope?
				//
				// "Only valid after a LABEL statement" -syslinux wiki.
				if c.scope == scopeEntry {
					c.defaultEntry = c.curEntry
				}
			}

		case "label":
			// We forever enter label scope.
			c.scope = scopeEntry
			c.curEntry = arg
			c.linuxEntries[c.curEntry] = &boot.LinuxImage{
				Cmdline: c.globalAppend,
				Name:    c.curEntry,
			}
			c.labelOrder = append(c.labelOrder, c.curEntry)

		case "kernel":
			// I hate special cases like these, but we aren't gonna
			// implement syslinux modules.
			if arg == "mboot.c32" {
				// Prepare for a multiboot kernel.
				delete(c.linuxEntries, c.curEntry)
				c.mbEntries[c.curEntry] = &boot.MultibootImage{
					Name: c.curEntry,
				}
			}
			fallthrough

		case "linux":
			if e, ok := c.linuxEntries[c.curEntry]; ok {
				k, err := c.getFile(arg)
				if err != nil {
					return err
				}
				e.Kernel = k
			}

		case "initrd":
			if e, ok := c.linuxEntries[c.curEntry]; ok {
				// TODO: support multiple comma-separated initrds.
				// TODO: append "initrd=$arg" to the cmdline.
				//
				// For how this interacts with global appends,
				// read
				// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
				i, err := c.getFile(arg)
				if err != nil {
					return err
				}
				e.Initrd = i
			}

		case "append":
			switch c.scope {
			case scopeGlobal:
				c.globalAppend = arg

			case scopeEntry:
				if e, ok := c.mbEntries[c.curEntry]; ok {
					modules := strings.Split(arg, "---")
					// The first module is special -- the kernel.
					if len(modules) > 0 {
						kernel := strings.Fields(modules[0])
						k, err := c.getFile(kernel[0])
						if err != nil {
							return err
						}
						e.Kernel = k
						if len(kernel) > 0 {
							e.Cmdline = strings.Join(kernel[1:], " ")
						}
						modules = modules[1:]
					}
					for _, cmdline := range modules {
						m := strings.Fields(cmdline)
						if len(m) == 0 {
							continue
						}
						name := m[0]
						file, err := c.getFile(name)
						if err != nil {
							return err
						}
						e.Modules = append(e.Modules, multiboot.Module{
							CmdLine: strings.Join(m, " "),
							Name:    name,
							Module:  file,
						})
					}
				}
				if e, ok := c.linuxEntries[c.curEntry]; ok {
					if arg == "-" {
						e.Cmdline = ""
					} else {
						// Yes, we explicitly _override_, not
						// concatenate. If a specific append
						// directive is present, a global
						// append directive is ignored.
						//
						// Also, "If you enter multiple APPEND
						// statements in a single LABEL entry,
						// only the last one will be used".
						//
						// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
						e.Cmdline = arg
					}
				}
			}
		}
	}

	// Go through all labels and download the initrds.
	for _, label := range c.linuxEntries {
		// If the initrd was set via the INITRD directive, don't
		// overwrite that.
		//
		// TODO(hugelgupf): Is this really what syslinux does? Does
		// INITRD trump cmdline? Does it trump global? What if both the
		// directive and cmdline initrd= are set? Does it depend on the
		// order in the config file? (My current best guess: order.)
		//
		// Answer: Normally, the INITRD directive appends to the
		// cmdline, and the _last_ effective initrd= parameter is used
		// for loading initrd files.
		if label.Initrd != nil {
			continue
		}

		for _, opt := range strings.Fields(label.Cmdline) {
			optkv := strings.Split(opt, "=")
			if optkv[0] != "initrd" {
				continue
			}

			i, err := c.getFile(optkv[1])
			if err != nil {
				return err
			}
			label.Initrd = i
		}
	}
	return nil

}
<|endoftext|>


<commit_before>/*
Copyright 2021 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dispatcher

import (
	"context"
	"fmt"
	nethttp "net/http"
	"strings"
	"sync"

	"github.com/Shopify/sarama"
	protocolkafka "github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2"
	"github.com/cloudevents/sdk-go/v2/binding"
	"github.com/google/uuid"
	"go.opencensus.io/trace"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	"knative.dev/pkg/logging"

	eventingchannels "knative.dev/eventing/pkg/channel"
	"knative.dev/eventing/pkg/kncloudevents"
	"knative.dev/pkg/kmeta"

	"knative.dev/eventing-kafka/pkg/channel/consolidated/utils"
	"knative.dev/eventing-kafka/pkg/channel/distributed/common/env"
	"knative.dev/eventing-kafka/pkg/common/client"
	"knative.dev/eventing-kafka/pkg/common/consumer"
	"knative.dev/eventing-kafka/pkg/common/tracing"
)

const (
	dispatcherReadySubHeader = "K-Subscriber-Status"
)

type TopicFunc func(separator, namespace, name string) string

type KafkaDispatcherArgs struct {
	KnCEConnectionArgs       *kncloudevents.ConnectionArgs
	ClientID                 string
	Brokers                  []string
	KafkaAuthConfig          *client.KafkaAuthConfig
	SaramaSettingsYamlString string
	TopicFunc                TopicFunc
}

type KafkaDispatcher struct {
	receiver   *eventingchannels.MessageReceiver
	dispatcher *eventingchannels.MessageDispatcherImpl

	// Receiver data structures
	// map[string]eventingchannels.ChannelReference
	hostToChannelMap  sync.Map
	kafkaSyncProducer sarama.SyncProducer

	// Dispatcher data structures
	// consumerUpdateLock must be used to update all the below maps
	consumerUpdateLock   sync.Mutex
	channelSubscriptions map[types.NamespacedName]*KafkaSubscription
	subsConsumerGroups   map[types.UID]sarama.ConsumerGroup
	subscriptions        map[types.UID]Subscription
	kafkaConsumerFactory consumer.KafkaConsumerGroupFactory

	topicFunc TopicFunc
	logger    *zap.SugaredLogger
}

func NewDispatcher(ctx context.Context, args *KafkaDispatcherArgs) (*KafkaDispatcher, error) {
	conf, err := client.NewConfigBuilder().
		WithClientId(args.ClientID).
		WithDefaults().
		FromYaml(args.SaramaSettingsYamlString).
		WithAuth(args.KafkaAuthConfig).
		Build(ctx)

	if err != nil {
		return nil, fmt.Errorf("Error updating the Sarama Auth config: %w", err)
	}

	producer, err := sarama.NewSyncProducer(args.Brokers, conf)
	if err != nil {
		return nil, fmt.Errorf("unable to create kafka producer against Kafka bootstrap servers %v : %v", args.Brokers, err)
	}

	dispatcher := &KafkaDispatcher{
		dispatcher:           eventingchannels.NewMessageDispatcher(logging.FromContext(ctx).Desugar()),
		kafkaConsumerFactory: consumer.NewConsumerGroupFactory(args.Brokers, conf),
		channelSubscriptions: make(map[types.NamespacedName]*KafkaSubscription),
		subsConsumerGroups:   make(map[types.UID]sarama.ConsumerGroup),
		subscriptions:        make(map[types.UID]Subscription),
		kafkaSyncProducer:    producer,
		logger:               logging.FromContext(ctx),
		topicFunc:            args.TopicFunc,
	}

	// initialize and start the subscription endpoint server
	subscriptionEndpoint := &subscriptionEndpoint{
		dispatcher: dispatcher,
		logger:     logging.FromContext(ctx),
	}
	go func() {
		subscriptionEndpoint.start()
	}()

	podName, err := env.GetRequiredConfigValue(logging.FromContext(ctx).Desugar(), env.PodNameEnvVarKey)
	if err != nil {
		return nil, err
	}
	containerName, err := env.GetRequiredConfigValue(logging.FromContext(ctx).Desugar(), env.ContainerNameEnvVarKey)
	if err != nil {
		return nil, err
	}
	reporter := eventingchannels.NewStatsReporter(containerName, kmeta.ChildName(podName, uuid.New().String()))
	receiverFunc, err := eventingchannels.NewMessageReceiver(
		func(ctx context.Context, channel eventingchannels.ChannelReference, message binding.Message, transformers []binding.Transformer, _ nethttp.Header) error {
			kafkaProducerMessage := sarama.ProducerMessage{
				Topic: dispatcher.topicFunc(utils.KafkaChannelSeparator, channel.Namespace, channel.Name),
			}

			dispatcher.logger.Debugw("Received a new message from MessageReceiver, dispatching to Kafka", zap.Any("channel", channel))
			err := protocolkafka.WriteProducerMessage(ctx, message, &kafkaProducerMessage, transformers...)
			if err != nil {
				return err
			}

			kafkaProducerMessage.Headers = append(kafkaProducerMessage.Headers, tracing.SerializeTrace(trace.FromContext(ctx).SpanContext())...)

			partition, offset, err := dispatcher.kafkaSyncProducer.SendMessage(&kafkaProducerMessage)

			if err == nil {
				dispatcher.logger.Debugw("message sent", zap.Int32("partition", partition), zap.Int64("offset", offset))
			} else {
				dispatcher.logger.Warnw("message not sent", zap.Error(err))
			}

			return err
		},
		logging.FromContext(ctx).Desugar(),
		reporter,
		eventingchannels.ResolveMessageChannelFromHostHeader(dispatcher.getChannelReferenceFromHost))
	if err != nil {
		return nil, err
	}

	dispatcher.receiver = receiverFunc
	return dispatcher, nil
}

// Start starts the kafka dispatcher's message processing.
func (d *KafkaDispatcher) Start(ctx context.Context) error {
	if d.receiver == nil {
		return fmt.Errorf("message receiver is not set")
	}

	return d.receiver.Start(ctx)
}

// UpdateError is the error returned from the ReconcileConsumers method, with the details of which
// subscriptions failed to subscribe to.
type UpdateError map[types.UID]error

func (k UpdateError) Error() string {
	errs := make([]string, 0, len(k))
	for uid, err := range k {
		errs = append(errs, fmt.Sprintf("subscription %s: %v", uid, err))
	}
	return strings.Join(errs, ",")
}

// ReconcileConsumers will be called by new CRD based kafka channel dispatcher controller.
func (d *KafkaDispatcher) ReconcileConsumers(config *ChannelConfig) error {
	channelNamespacedName := types.NamespacedName{
		Namespace: config.Namespace,
		Name:      config.Name,
	}

	// Aux data structures to reconcile
	toAddSubs := make(map[types.UID]Subscription)
	toRemoveSubs := sets.NewString()

	d.consumerUpdateLock.Lock()
	defer d.consumerUpdateLock.Unlock()

	// This loop takes care of filling toAddSubs and toRemoveSubs for new and existing channels
	thisChannelKafkaSubscriptions := d.channelSubscriptions[channelNamespacedName]

	var existingSubsForThisChannel sets.String
	if thisChannelKafkaSubscriptions != nil {
		existingSubsForThisChannel = thisChannelKafkaSubscriptions.subs
	} else {
		existingSubsForThisChannel = sets.NewString()
	}

	newSubsForThisChannel := sets.NewString(config.SubscriptionsUIDs()...)

	// toRemoveSubs += existing subs of this channel - new subs of this channel
	thisChannelToRemoveSubs := existingSubsForThisChannel.Difference(newSubsForThisChannel).UnsortedList()
	toRemoveSubs.Insert(
		thisChannelToRemoveSubs...,
	)

	// toAddSubs += new subs of this channel - existing subs of this channel
	thisChannelToAddSubs := newSubsForThisChannel.Difference(existingSubsForThisChannel)
	for _, subSpec := range config.Subscriptions {
		if thisChannelToAddSubs.Has(string(subSpec.UID)) {
			toAddSubs[subSpec.UID] = subSpec
		}
	}

	d.logger.Debug("Number of new subs", zap.Any("subs", len(toAddSubs)))
	d.logger.Debug("Number of old subs", zap.Any("subs", len(toRemoveSubs)))

	failedToSubscribe := make(UpdateError)
	for subUid, subSpec := range toAddSubs {
		if err := d.subscribe(channelNamespacedName, subSpec); err != nil {
			failedToSubscribe[subUid] = err
		}
	}
	d.logger.Debug("Number of subs failed to subscribe", zap.Any("subs", len(failedToSubscribe)))

	for _, subUid := range toRemoveSubs.UnsortedList() {
		// We don't signal to the caller the unsubscribe invocation
		if err := d.unsubscribe(channelNamespacedName, d.subscriptions[types.UID(subUid)]); err != nil {
			d.logger.Warnw("Error while unsubscribing", zap.Error(err))
		}
	}

	if len(failedToSubscribe) == 0 {
		return nil
	}
	return failedToSubscribe
}

// RegisterChannelHost adds a new channel to the host-channel mapping.
func (d *KafkaDispatcher) RegisterChannelHost(channelConfig *ChannelConfig) error {
	old, ok := d.hostToChannelMap.LoadOrStore(channelConfig.HostName, eventingchannels.ChannelReference{
		Name:      channelConfig.Name,
		Namespace: channelConfig.Namespace,
	})
	if ok {
		oldChannelRef := old.(eventingchannels.ChannelReference)
		if !(oldChannelRef.Namespace == channelConfig.Namespace && oldChannelRef.Name == channelConfig.Name) {
			// If something is already there, but it's not the same channel, then fail
			return fmt.Errorf(
				"duplicate hostName found. Each channel must have a unique host header. HostName:%s, channel:%s.%s, channel:%s.%s",
				channelConfig.HostName,
				old.(eventingchannels.ChannelReference).Namespace,
				old.(eventingchannels.ChannelReference).Name,
				channelConfig.Namespace,
				channelConfig.Name,
			)
		}
	}
	return nil
}

func (d *KafkaDispatcher) CleanupChannel(name, namespace, hostname string) error {
	channelRef := types.NamespacedName{
		Name:      name,
		Namespace: namespace,
	}

	// Remove from the hostToChannel map the mapping with this channel
	d.hostToChannelMap.Delete(hostname)

	// Remove all subs
	d.consumerUpdateLock.Lock()
	defer d.consumerUpdateLock.Unlock()

	if d.channelSubscriptions[channelRef] == nil {
		// No subs to remove
		return nil
	}

	for _, s := range d.channelSubscriptions[channelRef].subs.UnsortedList() {
		if err := d.unsubscribe(channelRef, d.subscriptions[types.UID(s)]); err != nil {
			return err
		}
	}

	return nil
}

// subscribe reads kafkaConsumers which gets updated in UpdateConfig in a separate go-routine.
// subscribe must be called under updateLock.
func (d *KafkaDispatcher) subscribe(channelRef types.NamespacedName, sub Subscription) error {
	d.logger.Infow("Subscribing to Kafka Channel", zap.Any("channelRef", channelRef), zap.Any("subscription", sub.UID))

	topicName := d.topicFunc(utils.KafkaChannelSeparator, channelRef.Namespace, channelRef.Name)
	groupID := fmt.Sprintf("kafka.%s.%s.%s", channelRef.Namespace, channelRef.Name, string(sub.UID))

	// Get or create the channel kafka subscription
	kafkaSubscription, ok := d.channelSubscriptions[channelRef]
	if !ok {
		kafkaSubscription = NewKafkaSubscription(d.logger)
		d.channelSubscriptions[channelRef] = kafkaSubscription
	}

	handler := &consumerMessageHandler{
		d.logger,
		sub,
		d.dispatcher,
		kafkaSubscription,
		groupID,
	}
	d.logger.Debugw("Starting consumer group", zap.Any("channelRef", channelRef),
		zap.Any("subscription", sub.UID), zap.String("topic", topicName), zap.String("consumer group", groupID))
	consumerGroup, err := d.kafkaConsumerFactory.StartConsumerGroup(groupID, []string{topicName}, d.logger, handler)

	if err != nil {
		// we can not create a consumer - logging that, with reason
		d.logger.Infow("Could not create proper consumer", zap.Error(err))
		return err
	}

	// sarama reports error in consumerGroup.Error() channel
	// this goroutine logs errors incoming
	go func() {
		for err = range consumerGroup.Errors() {
			d.logger.Warnw("Error in consumer group", zap.Error(err))
		}
	}()

	// Update the data structures that holds the reconciliation data
	kafkaSubscription.subs.Insert(string(sub.UID))
	d.subscriptions[sub.UID] = sub
	d.subsConsumerGroups[sub.UID] = consumerGroup

	return nil
}

// unsubscribe reads kafkaConsumers which gets updated in UpdateConfig in a separate go-routine.
// unsubscribe must be called under updateLock.
func (d *KafkaDispatcher) unsubscribe(channelRef types.NamespacedName, sub Subscription) error {
	d.logger.Infow("Unsubscribing from channel", zap.Any("channel", channelRef), zap.Any("subscription", sub.UID))

	// Remove the sub spec
	delete(d.subscriptions, sub.UID)

	// Remove the sub from the channel
	kafkaSubscription, ok := d.channelSubscriptions[channelRef]
	if !ok {
		// If this happens, then there's a bug somewhere...
		return nil
	}
	kafkaSubscription.subs.Delete(string(sub.UID))
	if kafkaSubscription.subs.Len() == 0 {
		// We can get rid of this
		delete(d.channelSubscriptions, channelRef)
	}

	// Delete the consumer group
	if consumerGroup, ok := d.subsConsumerGroups[sub.UID]; ok {
		delete(d.subsConsumerGroups, sub.UID)
		d.logger.Debugw("Closing cached consumerGroup group", zap.Any("consumer group", consumerGroup))
		return consumerGroup.Close()
	}
	return nil
}

func (d *KafkaDispatcher) getChannelReferenceFromHost(host string) (eventingchannels.ChannelReference, error) {
	cr, ok := d.hostToChannelMap.Load(host)
	if !ok {
		return eventingchannels.ChannelReference{}, eventingchannels.UnknownHostError(host)
	}
	return cr.(eventingchannels.ChannelReference), nil
}
<commit_msg>Remove unused Kafka dispatcher arg (#542)<commit_after>/*
Copyright 2021 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dispatcher

import (
	"context"
	"fmt"
	nethttp "net/http"
	"strings"
	"sync"

	"github.com/Shopify/sarama"
	protocolkafka "github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2"
	"github.com/cloudevents/sdk-go/v2/binding"
	"github.com/google/uuid"
	"go.opencensus.io/trace"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	"knative.dev/pkg/logging"

	eventingchannels "knative.dev/eventing/pkg/channel"
	"knative.dev/pkg/kmeta"

	"knative.dev/eventing-kafka/pkg/channel/consolidated/utils"
	"knative.dev/eventing-kafka/pkg/channel/distributed/common/env"
	"knative.dev/eventing-kafka/pkg/common/client"
	"knative.dev/eventing-kafka/pkg/common/consumer"
	"knative.dev/eventing-kafka/pkg/common/tracing"
)

const (
	dispatcherReadySubHeader = "K-Subscriber-Status"
)

type TopicFunc func(separator, namespace, name string) string

type KafkaDispatcherArgs struct {
	ClientID                 string
	Brokers                  []string
	KafkaAuthConfig          *client.KafkaAuthConfig
	SaramaSettingsYamlString string
	TopicFunc                TopicFunc
}

type KafkaDispatcher struct {
	receiver   *eventingchannels.MessageReceiver
	dispatcher *eventingchannels.MessageDispatcherImpl

	// Receiver data structures
	// map[string]eventingchannels.ChannelReference
	hostToChannelMap  sync.Map
	kafkaSyncProducer sarama.SyncProducer

	// Dispatcher data structures
	// consumerUpdateLock must be used to update all the below maps
	consumerUpdateLock   sync.Mutex
	channelSubscriptions map[types.NamespacedName]*KafkaSubscription
	subsConsumerGroups   map[types.UID]sarama.ConsumerGroup
	subscriptions        map[types.UID]Subscription
	kafkaConsumerFactory consumer.KafkaConsumerGroupFactory

	topicFunc TopicFunc
	logger    *zap.SugaredLogger
}

func NewDispatcher(ctx context.Context, args *KafkaDispatcherArgs) (*KafkaDispatcher, error) {
	conf, err := client.NewConfigBuilder().
		WithClientId(args.ClientID).
		WithDefaults().
		FromYaml(args.SaramaSettingsYamlString).
		WithAuth(args.KafkaAuthConfig).
		Build(ctx)

	if err != nil {
		return nil, fmt.Errorf("Error updating the Sarama Auth config: %w", err)
	}

	producer, err := sarama.NewSyncProducer(args.Brokers, conf)
	if err != nil {
		return nil, fmt.Errorf("unable to create kafka producer against Kafka bootstrap servers %v : %v", args.Brokers, err)
	}

	dispatcher := &KafkaDispatcher{
		dispatcher:           eventingchannels.NewMessageDispatcher(logging.FromContext(ctx).Desugar()),
		kafkaConsumerFactory: consumer.NewConsumerGroupFactory(args.Brokers, conf),
		channelSubscriptions: make(map[types.NamespacedName]*KafkaSubscription),
		subsConsumerGroups:   make(map[types.UID]sarama.ConsumerGroup),
		subscriptions:        make(map[types.UID]Subscription),
		kafkaSyncProducer:    producer,
		logger:               logging.FromContext(ctx),
		topicFunc:            args.TopicFunc,
	}

	// initialize and start the subscription endpoint server
	subscriptionEndpoint := &subscriptionEndpoint{
		dispatcher: dispatcher,
		logger:     logging.FromContext(ctx),
	}
	go func() {
		subscriptionEndpoint.start()
	}()

	podName, err := env.GetRequiredConfigValue(logging.FromContext(ctx).Desugar(), env.PodNameEnvVarKey)
	if err != nil {
		return nil, err
	}
	containerName, err := env.GetRequiredConfigValue(logging.FromContext(ctx).Desugar(), env.ContainerNameEnvVarKey)
	if err != nil {
		return nil, err
	}
	reporter := eventingchannels.NewStatsReporter(containerName, kmeta.ChildName(podName, uuid.New().String()))
	receiverFunc, err := eventingchannels.NewMessageReceiver(
		func(ctx context.Context, channel eventingchannels.ChannelReference, message binding.Message, transformers []binding.Transformer, _ nethttp.Header) error {
			kafkaProducerMessage := sarama.ProducerMessage{
				Topic: dispatcher.topicFunc(utils.KafkaChannelSeparator, channel.Namespace, channel.Name),
			}

			dispatcher.logger.Debugw("Received a new message from MessageReceiver, dispatching to Kafka", zap.Any("channel", channel))
			err := protocolkafka.WriteProducerMessage(ctx, message, &kafkaProducerMessage, transformers...)
			if err != nil {
				return err
			}

			kafkaProducerMessage.Headers = append(kafkaProducerMessage.Headers, tracing.SerializeTrace(trace.FromContext(ctx).SpanContext())...)

			partition, offset, err := dispatcher.kafkaSyncProducer.SendMessage(&kafkaProducerMessage)

			if err == nil {
				dispatcher.logger.Debugw("message sent", zap.Int32("partition", partition), zap.Int64("offset", offset))
			} else {
				dispatcher.logger.Warnw("message not sent", zap.Error(err))
			}

			return err
		},
		logging.FromContext(ctx).Desugar(),
		reporter,
		eventingchannels.ResolveMessageChannelFromHostHeader(dispatcher.getChannelReferenceFromHost))
	if err != nil {
		return nil, err
	}

	dispatcher.receiver = receiverFunc
	return dispatcher, nil
}

// Start starts the kafka dispatcher's message processing.
func (d *KafkaDispatcher) Start(ctx context.Context) error {
	if d.receiver == nil {
		return fmt.Errorf("message receiver is not set")
	}

	return d.receiver.Start(ctx)
}

// UpdateError is the error returned from the ReconcileConsumers method, with the details of which
// subscriptions failed to subscribe to.
type UpdateError map[types.UID]error

func (k UpdateError) Error() string {
	errs := make([]string, 0, len(k))
	for uid, err := range k {
		errs = append(errs, fmt.Sprintf("subscription %s: %v", uid, err))
	}
	return strings.Join(errs, ",")
}

// ReconcileConsumers will be called by new CRD based kafka channel dispatcher controller.
func (d *KafkaDispatcher) ReconcileConsumers(config *ChannelConfig) error {
	channelNamespacedName := types.NamespacedName{
		Namespace: config.Namespace,
		Name:      config.Name,
	}

	// Aux data structures to reconcile
	toAddSubs := make(map[types.UID]Subscription)
	toRemoveSubs := sets.NewString()

	d.consumerUpdateLock.Lock()
	defer d.consumerUpdateLock.Unlock()

	// This loop takes care of filling toAddSubs and toRemoveSubs for new and existing channels
	thisChannelKafkaSubscriptions := d.channelSubscriptions[channelNamespacedName]

	var existingSubsForThisChannel sets.String
	if thisChannelKafkaSubscriptions != nil {
		existingSubsForThisChannel = thisChannelKafkaSubscriptions.subs
	} else {
		existingSubsForThisChannel = sets.NewString()
	}

	newSubsForThisChannel := sets.NewString(config.SubscriptionsUIDs()...)

	// toRemoveSubs += existing subs of this channel - new subs of this channel
	thisChannelToRemoveSubs := existingSubsForThisChannel.Difference(newSubsForThisChannel).UnsortedList()
	toRemoveSubs.Insert(
		thisChannelToRemoveSubs...,
	)

	// toAddSubs += new subs of this channel - existing subs of this channel
	thisChannelToAddSubs := newSubsForThisChannel.Difference(existingSubsForThisChannel)
	for _, subSpec := range config.Subscriptions {
		if thisChannelToAddSubs.Has(string(subSpec.UID)) {
			toAddSubs[subSpec.UID] = subSpec
		}
	}

	d.logger.Debug("Number of new subs", zap.Any("subs", len(toAddSubs)))
	d.logger.Debug("Number of old subs", zap.Any("subs", len(toRemoveSubs)))

	failedToSubscribe := make(UpdateError)
	for subUid, subSpec := range toAddSubs {
		if err := d.subscribe(channelNamespacedName, subSpec); err != nil {
			failedToSubscribe[subUid] = err
		}
	}
	d.logger.Debug("Number of subs failed to subscribe", zap.Any("subs", len(failedToSubscribe)))

	for _, subUid := range toRemoveSubs.UnsortedList() {
		// We don't signal to the caller the unsubscribe invocation
		if err := d.unsubscribe(channelNamespacedName, d.subscriptions[types.UID(subUid)]); err != nil {
			d.logger.Warnw("Error while unsubscribing", zap.Error(err))
		}
	}

	if len(failedToSubscribe) == 0 {
		return nil
	}
	return failedToSubscribe
}

// RegisterChannelHost adds a new channel to the host-channel mapping.
func (d *KafkaDispatcher) RegisterChannelHost(channelConfig *ChannelConfig) error {
	old, ok := d.hostToChannelMap.LoadOrStore(channelConfig.HostName, eventingchannels.ChannelReference{
		Name:      channelConfig.Name,
		Namespace: channelConfig.Namespace,
	})
	if ok {
		oldChannelRef := old.(eventingchannels.ChannelReference)
		if !(oldChannelRef.Namespace == channelConfig.Namespace && oldChannelRef.Name == channelConfig.Name) {
			// If something is already there, but it's not the same channel, then fail
			return fmt.Errorf(
				"duplicate hostName found. Each channel must have a unique host header. HostName:%s, channel:%s.%s, channel:%s.%s",
				channelConfig.HostName,
				old.(eventingchannels.ChannelReference).Namespace,
				old.(eventingchannels.ChannelReference).Name,
				channelConfig.Namespace,
				channelConfig.Name,
			)
		}
	}
	return nil
}

func (d *KafkaDispatcher) CleanupChannel(name, namespace, hostname string) error {
	channelRef := types.NamespacedName{
		Name:      name,
		Namespace: namespace,
	}

	// Remove from the hostToChannel map the mapping with this channel
	d.hostToChannelMap.Delete(hostname)

	// Remove all subs
	d.consumerUpdateLock.Lock()
	defer d.consumerUpdateLock.Unlock()

	if d.channelSubscriptions[channelRef] == nil {
		// No subs to remove
		return nil
	}

	for _, s := range d.channelSubscriptions[channelRef].subs.UnsortedList() {
		if err := d.unsubscribe(channelRef, d.subscriptions[types.UID(s)]); err != nil {
			return err
		}
	}

	return nil
}

// subscribe reads kafkaConsumers which gets updated in UpdateConfig in a separate go-routine.
// subscribe must be called under updateLock.
func (d *KafkaDispatcher) subscribe(channelRef types.NamespacedName, sub Subscription) error {
	d.logger.Infow("Subscribing to Kafka Channel", zap.Any("channelRef", channelRef), zap.Any("subscription", sub.UID))

	topicName := d.topicFunc(utils.KafkaChannelSeparator, channelRef.Namespace, channelRef.Name)
	groupID := fmt.Sprintf("kafka.%s.%s.%s", channelRef.Namespace, channelRef.Name, string(sub.UID))

	// Get or create the channel kafka subscription
	kafkaSubscription, ok := d.channelSubscriptions[channelRef]
	if !ok {
		kafkaSubscription = NewKafkaSubscription(d.logger)
		d.channelSubscriptions[channelRef] = kafkaSubscription
	}

	handler := &consumerMessageHandler{
		d.logger,
		sub,
		d.dispatcher,
		kafkaSubscription,
		groupID,
	}
	d.logger.Debugw("Starting consumer group", zap.Any("channelRef", channelRef),
		zap.Any("subscription", sub.UID), zap.String("topic", topicName), zap.String("consumer group", groupID))
	consumerGroup, err := d.kafkaConsumerFactory.StartConsumerGroup(groupID, []string{topicName}, d.logger, handler)

	if err != nil {
		// we can not create a consumer - logging that, with reason
		d.logger.Infow("Could not create proper consumer", zap.Error(err))
		return err
	}

	// sarama reports error in consumerGroup.Error() channel
	// this goroutine logs errors incoming
	go func() {
		for err = range consumerGroup.Errors() {
			d.logger.Warnw("Error in consumer group", zap.Error(err))
		}
	}()

	// Update the data structures that holds the reconciliation data
	kafkaSubscription.subs.Insert(string(sub.UID))
	d.subscriptions[sub.UID] = sub
	d.subsConsumerGroups[sub.UID] = consumerGroup

	return nil
}

// unsubscribe reads kafkaConsumers which gets updated in UpdateConfig in a separate go-routine.
// unsubscribe must be called under updateLock.
func (d *KafkaDispatcher) unsubscribe(channelRef types.NamespacedName, sub Subscription) error {
	d.logger.Infow("Unsubscribing from channel", zap.Any("channel", channelRef), zap.Any("subscription", sub.UID))

	// Remove the sub spec
	delete(d.subscriptions, sub.UID)

	// Remove the sub from the channel
	kafkaSubscription, ok := d.channelSubscriptions[channelRef]
	if !ok {
		// If this happens, then there's a bug somewhere...
		return nil
	}
	kafkaSubscription.subs.Delete(string(sub.UID))
	if kafkaSubscription.subs.Len() == 0 {
		// We can get rid of this
		delete(d.channelSubscriptions, channelRef)
	}

	// Delete the consumer group
	if consumerGroup, ok := d.subsConsumerGroups[sub.UID]; ok {
		delete(d.subsConsumerGroups, sub.UID)
		d.logger.Debugw("Closing cached consumerGroup group", zap.Any("consumer group", consumerGroup))
		return consumerGroup.Close()
	}
	return nil
}

func (d *KafkaDispatcher) getChannelReferenceFromHost(host string) (eventingchannels.ChannelReference, error) {
	cr, ok := d.hostToChannelMap.Load(host)
	if !ok {
		return eventingchannels.ChannelReference{}, eventingchannels.UnknownHostError(host)
	}
	return cr.(eventingchannels.ChannelReference), nil
}
<|endoftext|>


<commit_before>// Package config provides data structure to configure rate-limiter.
package config

import (
	"github.com/juju/ratelimit"
	"time"
)

// NewLimiter is a constructor for Limiter.
func NewLimiter(max int64, ttl time.Duration) *Limiter {
	limiter := &Limiter{Max: max, TTL: ttl}
	limiter.Message = "You have reached maximum request limit."
	limiter.StatusCode = 429
	limiter.tokenBuckets = make(map[string]*ratelimit.Bucket)

	return limiter
}

// Limiter is a config struct to limit a particular request handler.
type Limiter struct {
	// HTTP message when limit is reached.
	Message string

	// HTTP status code when limit is reached.
	StatusCode int

	// Maximum number of requests to limit per duration.
	Max int64

	// Duration of rate-limiter.
	TTL time.Duration

	// List of HTTP Methods to limit (GET, POST, PUT, etc.).
	// Empty means limit all methods.
	Methods []string

	// List of HTTP headers to limit.
	// Empty means skip headers checking.
	Headers map[string][]string

	// List of basic auth usernames to limit.
	BasicAuthUsers []string

	// Throttler struct
	tokenBuckets map[string]*ratelimit.Bucket
}

// LimitReached returns a bool indicating if the Bucket identified by key ran out of tokens.
func (l *Limiter) LimitReached(key string) bool {
	if _, found := l.tokenBuckets[key]; !found {
		l.tokenBuckets[key] = ratelimit.NewBucket(l.TTL, l.Max)
	}

	_, isSoonerThanMaxWait := l.tokenBuckets[key].TakeMaxDuration(1, l.TTL)
	if isSoonerThanMaxWait {
		return false
	}

	return true
}
<commit_msg>Forgot mutex locking/unlocking when accessing map of token buckets.<commit_after>// Package config provides data structure to configure rate-limiter.
package config

import (
	"github.com/juju/ratelimit"
	"sync"
	"time"
)

// NewLimiter is a constructor for Limiter.
func NewLimiter(max int64, ttl time.Duration) *Limiter {
	limiter := &Limiter{Max: max, TTL: ttl}
	limiter.Message = "You have reached maximum request limit."
	limiter.StatusCode = 429
	limiter.tokenBuckets = make(map[string]*ratelimit.Bucket)

	return limiter
}

// Limiter is a config struct to limit a particular request handler.
type Limiter struct {
	// HTTP message when limit is reached.
	Message string

	// HTTP status code when limit is reached.
	StatusCode int

	// Maximum number of requests to limit per duration.
	Max int64

	// Duration of rate-limiter.
	TTL time.Duration

	// List of HTTP Methods to limit (GET, POST, PUT, etc.).
	// Empty means limit all methods.
	Methods []string

	// List of HTTP headers to limit.
	// Empty means skip headers checking.
	Headers map[string][]string

	// List of basic auth usernames to limit.
	BasicAuthUsers []string

	// Throttler struct
	tokenBuckets map[string]*ratelimit.Bucket

	sync.RWMutex
}

// LimitReached returns a bool indicating if the Bucket identified by key ran out of tokens.
func (l *Limiter) LimitReached(key string) bool {
	if _, found := l.tokenBuckets[key]; !found {
		l.tokenBuckets[key] = ratelimit.NewBucket(l.TTL, l.Max)
	}

	l.Lock()
	_, isSoonerThanMaxWait := l.tokenBuckets[key].TakeMaxDuration(1, l.TTL)
	l.Unlock()

	if isSoonerThanMaxWait {
		return false
	}

	return true
}
<|endoftext|>


<commit_before>package server

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"strings"

	log "github.com/Sirupsen/logrus"
	"github.com/docker/distribution"
	"github.com/docker/distribution/digest"
	"github.com/docker/distribution/manifest"
	repomw "github.com/docker/distribution/registry/middleware/repository"
	"github.com/docker/libtrust"
	"github.com/openshift/origin/pkg/client"
	imageapi "github.com/openshift/origin/pkg/image/api"
	"golang.org/x/net/context"
	kapi "k8s.io/kubernetes/pkg/api"
	kerrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/client/unversioned"
)

func init() {
	repomw.Register("openshift", repomw.InitFunc(newRepository))
}

type repository struct {
	distribution.Repository

	registryClient *client.Client
	registryAddr   string
	namespace      string
	name           string
}

// newRepository returns a new repository middleware.
func newRepository(repo distribution.Repository, options map[string]interface{}) (distribution.Repository, error) {
	registryAddr := os.Getenv("REGISTRY_URL")
	if len(registryAddr) == 0 {
		return nil, errors.New("REGISTRY_URL is required")
	}

	registryClient, err := NewRegistryOpenShiftClient()
	if err != nil {
		return nil, err
	}

	nameParts := strings.SplitN(repo.Name(), "/", 2)
	if len(nameParts) != 2 {
		return nil, fmt.Errorf("invalid repository name %q: it must be of the format <project>/<name>", repo.Name())
	}

	return &repository{
		Repository:     repo,
		registryClient: registryClient,
		registryAddr:   registryAddr,
		namespace:      nameParts[0],
		name:           nameParts[1],
	}, nil
}

// Manifests returns r, which implements distribution.ManifestService.
func (r *repository) Manifests() distribution.ManifestService {
	return r
}

// Tags lists the tags under the named repository.
func (r *repository) Tags(ctx context.Context) ([]string, error) {
	imageStream, err := r.getImageStream(ctx)
	if err != nil {
		return []string{}, nil
	}
	tags := []string{}
	for tag := range imageStream.Status.Tags {
		tags = append(tags, tag)
	}

	return tags, nil
}

// Exists returns true if the manifest specified by dgst exists.
func (r *repository) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
	image, err := r.getImage(dgst)
	if err != nil {
		return false, err
	}
	return image != nil, nil
}

// ExistsByTag returns true if the manifest with tag `tag` exists.
func (r *repository) ExistsByTag(ctx context.Context, tag string) (bool, error) {
	imageStream, err := r.getImageStream(ctx)
	if err != nil {
		return false, err
	}
	_, found := imageStream.Status.Tags[tag]
	return found, nil
}

// Get retrieves the manifest with digest `dgst`.
func (r *repository) Get(ctx context.Context, dgst digest.Digest) (*manifest.SignedManifest, error) {
	if _, err := r.getImageStreamImage(ctx, dgst); err != nil {
		log.Errorf("Error retrieving ImageStreamImage %s/%s@%s: %v", r.namespace, r.name, dgst.String(), err)
		return nil, err
	}

	image, err := r.getImage(dgst)
	if err != nil {
		log.Errorf("Error retrieving image %s: %v", dgst.String(), err)
		return nil, err
	}

	return r.manifestFromImage(image)
}

// GetByTag retrieves the named manifest with the provided tag
func (r *repository) GetByTag(ctx context.Context, tag string) (*manifest.SignedManifest, error) {
	imageStreamTag, err := r.getImageStreamTag(ctx, tag)
	if err != nil {
		log.Errorf("Error getting ImageStreamTag %q: %v", tag, err)
		return nil, err
	}
	image := &imageStreamTag.Image

	dgst, err := digest.ParseDigest(imageStreamTag.Image.Name)
	if err != nil {
		log.Errorf("Error parsing digest %q: %v", imageStreamTag.Image.Name, err)
		return nil, err
	}

	image, err = r.getImage(dgst)
	if err != nil {
		log.Errorf("Error getting image %q: %v", dgst.String(), err)
		return nil, err
	}

	return r.manifestFromImage(image)
}

// Put creates or updates the named manifest.
func (r *repository) Put(ctx context.Context, manifest *manifest.SignedManifest) error {
	// Resolve the payload in the manifest.
	payload, err := manifest.Payload()
	if err != nil {
		return err
	}

	// Calculate digest
	dgst, err := digest.FromBytes(payload)
	if err != nil {
		return err
	}

	// Upload to openshift
	ism := imageapi.ImageStreamMapping{
		ObjectMeta: kapi.ObjectMeta{
			Namespace: r.namespace,
			Name:      r.name,
		},
		Tag: manifest.Tag,
		Image: imageapi.Image{
			ObjectMeta: kapi.ObjectMeta{
				Name: dgst.String(),
				Annotations: map[string]string{
					imageapi.ManagedByOpenShiftAnnotation: "true",
				},
			},
			DockerImageReference: fmt.Sprintf("%s/%s/%s@%s", r.registryAddr, r.namespace, r.name, dgst.String()),
			DockerImageManifest:  string(payload),
		},
	}

	if err := r.registryClient.ImageStreamMappings(r.namespace).Create(&ism); err != nil {
		// if the error was that the image stream wasn't found, try to auto provision it
		statusErr, ok := err.(*kerrors.StatusError)
		if !ok {
			log.Errorf("Error creating ImageStreamMapping: %s", err)
			return err
		}

		status := statusErr.ErrStatus
		if status.Code != http.StatusNotFound || status.Details.Kind != "imageStream" || status.Details.Name != r.name {
			log.Errorf("Error creating ImageStreamMapping: %s", err)
			return err
		}

		stream := imageapi.ImageStream{
			ObjectMeta: kapi.ObjectMeta{
				Name: r.name,
			},
		}

		client, ok := UserClientFrom(ctx)
		if !ok {
			log.Errorf("Error creating user client to auto provision image stream: Origin user client unavailable")
			return statusErr
		}

		if _, err := client.ImageStreams(r.namespace).Create(&stream); err != nil {
			log.Errorf("Error auto provisioning image stream: %s", err)
			return statusErr
		}

		// try to create the ISM again
		if err := unversioned.RetryOnConflict(unversioned.DefaultRetry, func() error {
			return r.registryClient.ImageStreamMappings(r.namespace).Create(&ism)
		}); err != nil {
			log.Errorf("Error creating image stream mapping: %s", err)
			return err
		}
	}

	// Grab each json signature and store them.
	signatures, err := manifest.Signatures()
	if err != nil {
		return err
	}

	for _, signature := range signatures {
		if err := r.Signatures().Put(dgst, signature); err != nil {
			log.Errorf("Error storing signature: %s", err)
			return err
		}
	}

	return nil
}

// Delete deletes the manifest with digest `dgst`. Note: Image resources
// in OpenShift are deleted via 'oadm prune images'. This function deletes
// the content related to the manifest in the registry's storage (signatures).
func (r *repository) Delete(ctx context.Context, dgst digest.Digest) error {
	return r.Repository.Manifests().Delete(ctx, dgst)
}

// getImageStream retrieves the ImageStream for r.
func (r *repository) getImageStream(ctx context.Context) (*imageapi.ImageStream, error) {
	return r.registryClient.ImageStreams(r.namespace).Get(r.name)
}

// getImage retrieves the Image with digest `dgst`.
func (r *repository) getImage(dgst digest.Digest) (*imageapi.Image, error) {
	return r.registryClient.Images().Get(dgst.String())
}

// getImageStreamTag retrieves the Image with tag `tag` for the ImageStream
// associated with r.
func (r *repository) getImageStreamTag(ctx context.Context, tag string) (*imageapi.ImageStreamTag, error) {
	return r.registryClient.ImageStreamTags(r.namespace).Get(r.name, tag)
}

// getImageStreamImage retrieves the Image with digest `dgst` for the ImageStream
// associated with r. This ensures the image belongs to the image stream.
func (r *repository) getImageStreamImage(ctx context.Context, dgst digest.Digest) (*imageapi.ImageStreamImage, error) {
	return r.registryClient.ImageStreamImages(r.namespace).Get(r.name, dgst.String())
}

// manifestFromImage converts an Image to a SignedManifest.
func (r *repository) manifestFromImage(image *imageapi.Image) (*manifest.SignedManifest, error) {
	dgst, err := digest.ParseDigest(image.Name)
	if err != nil {
		return nil, err
	}

	// Fetch the signatures for the manifest
	signatures, err := r.Signatures().Get(dgst)
	if err != nil {
		return nil, err
	}

	jsig, err := libtrust.NewJSONSignature([]byte(image.DockerImageManifest), signatures...)
	if err != nil {
		return nil, err
	}

	// Extract the pretty JWS
	raw, err := jsig.PrettySignature("signatures")
	if err != nil {
		return nil, err
	}

	var sm manifest.SignedManifest
	if err := json.Unmarshal(raw, &sm); err != nil {
		return nil, err
	}
	return &sm, err
}
<commit_msg>Revert 'Retry ISM save upon conflicting IS error' commit upon retry mechanism introduced in ISM create method<commit_after>package server

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"strings"

	log "github.com/Sirupsen/logrus"
	"github.com/docker/distribution"
	"github.com/docker/distribution/digest"
	"github.com/docker/distribution/manifest"
	repomw "github.com/docker/distribution/registry/middleware/repository"
	"github.com/docker/libtrust"
	"github.com/openshift/origin/pkg/client"
	imageapi "github.com/openshift/origin/pkg/image/api"
	"golang.org/x/net/context"
	kapi "k8s.io/kubernetes/pkg/api"
	kerrors "k8s.io/kubernetes/pkg/api/errors"
)

func init() {
	repomw.Register("openshift", repomw.InitFunc(newRepository))
}

type repository struct {
	distribution.Repository

	registryClient *client.Client
	registryAddr   string
	namespace      string
	name           string
}

// newRepository returns a new repository middleware.
func newRepository(repo distribution.Repository, options map[string]interface{}) (distribution.Repository, error) {
	registryAddr := os.Getenv("REGISTRY_URL")
	if len(registryAddr) == 0 {
		return nil, errors.New("REGISTRY_URL is required")
	}

	registryClient, err := NewRegistryOpenShiftClient()
	if err != nil {
		return nil, err
	}

	nameParts := strings.SplitN(repo.Name(), "/", 2)
	if len(nameParts) != 2 {
		return nil, fmt.Errorf("invalid repository name %q: it must be of the format <project>/<name>", repo.Name())
	}

	return &repository{
		Repository:     repo,
		registryClient: registryClient,
		registryAddr:   registryAddr,
		namespace:      nameParts[0],
		name:           nameParts[1],
	}, nil
}

// Manifests returns r, which implements distribution.ManifestService.
func (r *repository) Manifests() distribution.ManifestService {
	return r
}

// Tags lists the tags under the named repository.
func (r *repository) Tags(ctx context.Context) ([]string, error) {
	imageStream, err := r.getImageStream(ctx)
	if err != nil {
		return []string{}, nil
	}
	tags := []string{}
	for tag := range imageStream.Status.Tags {
		tags = append(tags, tag)
	}

	return tags, nil
}

// Exists returns true if the manifest specified by dgst exists.
func (r *repository) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
	image, err := r.getImage(dgst)
	if err != nil {
		return false, err
	}
	return image != nil, nil
}

// ExistsByTag returns true if the manifest with tag `tag` exists.
func (r *repository) ExistsByTag(ctx context.Context, tag string) (bool, error) {
	imageStream, err := r.getImageStream(ctx)
	if err != nil {
		return false, err
	}
	_, found := imageStream.Status.Tags[tag]
	return found, nil
}

// Get retrieves the manifest with digest `dgst`.
func (r *repository) Get(ctx context.Context, dgst digest.Digest) (*manifest.SignedManifest, error) {
	if _, err := r.getImageStreamImage(ctx, dgst); err != nil {
		log.Errorf("Error retrieving ImageStreamImage %s/%s@%s: %v", r.namespace, r.name, dgst.String(), err)
		return nil, err
	}

	image, err := r.getImage(dgst)
	if err != nil {
		log.Errorf("Error retrieving image %s: %v", dgst.String(), err)
		return nil, err
	}

	return r.manifestFromImage(image)
}

// GetByTag retrieves the named manifest with the provided tag
func (r *repository) GetByTag(ctx context.Context, tag string) (*manifest.SignedManifest, error) {
	imageStreamTag, err := r.getImageStreamTag(ctx, tag)
	if err != nil {
		log.Errorf("Error getting ImageStreamTag %q: %v", tag, err)
		return nil, err
	}
	image := &imageStreamTag.Image

	dgst, err := digest.ParseDigest(imageStreamTag.Image.Name)
	if err != nil {
		log.Errorf("Error parsing digest %q: %v", imageStreamTag.Image.Name, err)
		return nil, err
	}

	image, err = r.getImage(dgst)
	if err != nil {
		log.Errorf("Error getting image %q: %v", dgst.String(), err)
		return nil, err
	}

	return r.manifestFromImage(image)
}

// Put creates or updates the named manifest.
func (r *repository) Put(ctx context.Context, manifest *manifest.SignedManifest) error {
	// Resolve the payload in the manifest.
	payload, err := manifest.Payload()
	if err != nil {
		return err
	}

	// Calculate digest
	dgst, err := digest.FromBytes(payload)
	if err != nil {
		return err
	}

	// Upload to openshift
	ism := imageapi.ImageStreamMapping{
		ObjectMeta: kapi.ObjectMeta{
			Namespace: r.namespace,
			Name:      r.name,
		},
		Tag: manifest.Tag,
		Image: imageapi.Image{
			ObjectMeta: kapi.ObjectMeta{
				Name: dgst.String(),
				Annotations: map[string]string{
					imageapi.ManagedByOpenShiftAnnotation: "true",
				},
			},
			DockerImageReference: fmt.Sprintf("%s/%s/%s@%s", r.registryAddr, r.namespace, r.name, dgst.String()),
			DockerImageManifest:  string(payload),
		},
	}

	if err := r.registryClient.ImageStreamMappings(r.namespace).Create(&ism); err != nil {
		// if the error was that the image stream wasn't found, try to auto provision it
		statusErr, ok := err.(*kerrors.StatusError)
		if !ok {
			log.Errorf("Error creating ImageStreamMapping: %s", err)
			return err
		}

		status := statusErr.ErrStatus
		if status.Code != http.StatusNotFound || status.Details.Kind != "imageStream" || status.Details.Name != r.name {
			log.Errorf("Error creating ImageStreamMapping: %s", err)
			return err
		}

		stream := imageapi.ImageStream{
			ObjectMeta: kapi.ObjectMeta{
				Name: r.name,
			},
		}

		client, ok := UserClientFrom(ctx)
		if !ok {
			log.Errorf("Error creating user client to auto provision image stream: Origin user client unavailable")
			return statusErr
		}

		if _, err := client.ImageStreams(r.namespace).Create(&stream); err != nil {
			log.Errorf("Error auto provisioning image stream: %s", err)
			return statusErr
		}

		// try to create the ISM again
		if err := r.registryClient.ImageStreamMappings(r.namespace).Create(&ism); err != nil {
			log.Errorf("Error creating image stream mapping: %s", err)
			return err
		}
	}

	// Grab each json signature and store them.
	signatures, err := manifest.Signatures()
	if err != nil {
		return err
	}

	for _, signature := range signatures {
		if err := r.Signatures().Put(dgst, signature); err != nil {
			log.Errorf("Error storing signature: %s", err)
			return err
		}
	}

	return nil
}

// Delete deletes the manifest with digest `dgst`. Note: Image resources
// in OpenShift are deleted via 'oadm prune images'. This function deletes
// the content related to the manifest in the registry's storage (signatures).
func (r *repository) Delete(ctx context.Context, dgst digest.Digest) error {
	return r.Repository.Manifests().Delete(ctx, dgst)
}

// getImageStream retrieves the ImageStream for r.
func (r *repository) getImageStream(ctx context.Context) (*imageapi.ImageStream, error) {
	return r.registryClient.ImageStreams(r.namespace).Get(r.name)
}

// getImage retrieves the Image with digest `dgst`.
func (r *repository) getImage(dgst digest.Digest) (*imageapi.Image, error) {
	return r.registryClient.Images().Get(dgst.String())
}

// getImageStreamTag retrieves the Image with tag `tag` for the ImageStream
// associated with r.
func (r *repository) getImageStreamTag(ctx context.Context, tag string) (*imageapi.ImageStreamTag, error) {
	return r.registryClient.ImageStreamTags(r.namespace).Get(r.name, tag)
}

// getImageStreamImage retrieves the Image with digest `dgst` for the ImageStream
// associated with r. This ensures the image belongs to the image stream.
func (r *repository) getImageStreamImage(ctx context.Context, dgst digest.Digest) (*imageapi.ImageStreamImage, error) {
	return r.registryClient.ImageStreamImages(r.namespace).Get(r.name, dgst.String())
}

// manifestFromImage converts an Image to a SignedManifest.
func (r *repository) manifestFromImage(image *imageapi.Image) (*manifest.SignedManifest, error) {
	dgst, err := digest.ParseDigest(image.Name)
	if err != nil {
		return nil, err
	}

	// Fetch the signatures for the manifest
	signatures, err := r.Signatures().Get(dgst)
	if err != nil {
		return nil, err
	}

	jsig, err := libtrust.NewJSONSignature([]byte(image.DockerImageManifest), signatures...)
	if err != nil {
		return nil, err
	}

	// Extract the pretty JWS
	raw, err := jsig.PrettySignature("signatures")
	if err != nil {
		return nil, err
	}

	var sm manifest.SignedManifest
	if err := json.Unmarshal(raw, &sm); err != nil {
		return nil, err
	}
	return &sm, err
}
<|endoftext|>