<commit_before>/*
Copyright 2017 The Kubernetes 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 controller

import (
	"bytes"
	"github.com/golang/glog"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/version"
	"github.com/spf13/pflag"
	"io/ioutil"
	api "k8s.io/client-go/pkg/api/v1"
	"k8s.io/ingress/core/pkg/ingress"
	"k8s.io/ingress/core/pkg/ingress/controller"
	"k8s.io/ingress/core/pkg/ingress/defaults"
	"net/http"
	"os"
	"os/exec"
)

// HAProxyController has internal data of a HAProxyController instance
type HAProxyController struct {
	controller     *controller.GenericController
	configMap      *api.ConfigMap
	storeLister    *ingress.StoreLister
	command        string
	reloadStrategy *string
	configFile     string
	template       *template
}

// NewHAProxyController constructor
func NewHAProxyController() *HAProxyController {
	return &HAProxyController{}
}

// Info provides controller name and repository infos
func (haproxy *HAProxyController) Info() *ingress.BackendInfo {
	return &ingress.BackendInfo{
		Name:       "HAProxy",
		Release:    version.RELEASE,
		Build:      version.COMMIT,
		Repository: version.REPO,
	}
}

// Start starts the controller
func (haproxy *HAProxyController) Start() {
	haproxy.controller = controller.NewIngressController(haproxy)
	haproxy.controller.Start()
}

// Stop shutdown the controller process
func (haproxy *HAProxyController) Stop() error {
	err := haproxy.controller.Stop()
	return err
}

// Name provides the complete name of the controller
func (haproxy *HAProxyController) Name() string {
	return "HAProxy Ingress Controller"
}

// DefaultIngressClass returns the ingress class name
func (haproxy *HAProxyController) DefaultIngressClass() string {
	return "haproxy"
}

// Check health check implementation
func (haproxy *HAProxyController) Check(_ *http.Request) error {
	return nil
}

// SetListers give access to the store listers
func (haproxy *HAProxyController) SetListers(lister ingress.StoreLister) {
	haproxy.storeLister = &lister
}

// ConfigureFlags allow to configure more flags before the parsing of
// command line arguments
func (haproxy *HAProxyController) ConfigureFlags(flags *pflag.FlagSet) {
	haproxy.reloadStrategy = flags.String("reload-strategy", "native",
		`Name of the reload strategy. Options are: native (default) or multibinder`)
}

// OverrideFlags allows controller to override command line parameter flags
func (haproxy *HAProxyController) OverrideFlags(flags *pflag.FlagSet) {
	if *haproxy.reloadStrategy == "native" {
		haproxy.configFile = "/etc/haproxy/haproxy.cfg"
		haproxy.template = newTemplate("haproxy.tmpl", "/etc/haproxy/template/haproxy.tmpl")
	} else if *haproxy.reloadStrategy == "multibinder" {
		haproxy.configFile = "/etc/haproxy/haproxy.cfg.erb"
		haproxy.template = newTemplate("haproxy.cfg.erb.tmpl", "/etc/haproxy/haproxy.cfg.erb.tmpl")
	} else {
		glog.Fatalf("Unsupported reload strategy: %v", *haproxy.reloadStrategy)
	}
	haproxy.command = "/haproxy-reload.sh"
}

// SetConfig receives the ConfigMap the user has configured
func (haproxy *HAProxyController) SetConfig(configMap *api.ConfigMap) {
	haproxy.configMap = configMap
}

// BackendDefaults defines default values to the ingress core
func (haproxy *HAProxyController) BackendDefaults() defaults.Backend {
	return newHAProxyConfig(haproxy).Backend
}

// OnUpdate regenerate the configuration file of the backend
func (haproxy *HAProxyController) OnUpdate(cfg ingress.Configuration) ([]byte, error) {
	data, err := haproxy.template.execute(newControllerConfig(&cfg, haproxy))
	if err != nil {
		return nil, err
	}
	return data, nil
}

// Reload reload the backend if the configuration file has changed
func (haproxy *HAProxyController) Reload(data []byte) ([]byte, bool, error) {
	if !haproxy.configChanged(data) {
		return nil, false, nil
	}
	// TODO missing HAProxy validation before overwrite and try to reload
	err := ioutil.WriteFile(haproxy.configFile, data, 0644)
	if err != nil {
		return nil, false, err
	}
	out, err := haproxy.reloadHaproxy()
	if len(out) > 0 {
		glog.Infof("HAProxy output:\n%v", string(out))
	}
	return out, true, err
}

func (haproxy *HAProxyController) configChanged(data []byte) bool {
	if _, err := os.Stat(haproxy.configFile); os.IsNotExist(err) {
		return true
	}
	cfg, err := ioutil.ReadFile(haproxy.configFile)
	if err != nil {
		glog.Warningf("error reading haproxy config: %v")
		return false
	}
	return !bytes.Equal(cfg, data)
}

func (haproxy *HAProxyController) reloadHaproxy() ([]byte, error) {
	out, err := exec.Command(haproxy.command, *haproxy.reloadStrategy, haproxy.configFile).CombinedOutput()
	return out, err
}
<commit_msg>Add haproxy as default value of --ingress-class<commit_after>/*
Copyright 2017 The Kubernetes 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 controller

import (
	"bytes"
	"github.com/golang/glog"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/version"
	"github.com/spf13/pflag"
	"io/ioutil"
	api "k8s.io/client-go/pkg/api/v1"
	"k8s.io/ingress/core/pkg/ingress"
	"k8s.io/ingress/core/pkg/ingress/controller"
	"k8s.io/ingress/core/pkg/ingress/defaults"
	"net/http"
	"os"
	"os/exec"
)

// HAProxyController has internal data of a HAProxyController instance
type HAProxyController struct {
	controller     *controller.GenericController
	configMap      *api.ConfigMap
	storeLister    *ingress.StoreLister
	command        string
	reloadStrategy *string
	configFile     string
	template       *template
}

// NewHAProxyController constructor
func NewHAProxyController() *HAProxyController {
	return &HAProxyController{}
}

// Info provides controller name and repository infos
func (haproxy *HAProxyController) Info() *ingress.BackendInfo {
	return &ingress.BackendInfo{
		Name:       "HAProxy",
		Release:    version.RELEASE,
		Build:      version.COMMIT,
		Repository: version.REPO,
	}
}

// Start starts the controller
func (haproxy *HAProxyController) Start() {
	haproxy.controller = controller.NewIngressController(haproxy)
	haproxy.controller.Start()
}

// Stop shutdown the controller process
func (haproxy *HAProxyController) Stop() error {
	err := haproxy.controller.Stop()
	return err
}

// Name provides the complete name of the controller
func (haproxy *HAProxyController) Name() string {
	return "HAProxy Ingress Controller"
}

// DefaultIngressClass returns the ingress class name
func (haproxy *HAProxyController) DefaultIngressClass() string {
	return "haproxy"
}

// Check health check implementation
func (haproxy *HAProxyController) Check(_ *http.Request) error {
	return nil
}

// SetListers give access to the store listers
func (haproxy *HAProxyController) SetListers(lister ingress.StoreLister) {
	haproxy.storeLister = &lister
}

// ConfigureFlags allow to configure more flags before the parsing of
// command line arguments
func (haproxy *HAProxyController) ConfigureFlags(flags *pflag.FlagSet) {
	haproxy.reloadStrategy = flags.String("reload-strategy", "native",
		`Name of the reload strategy. Options are: native (default) or multibinder`)
	ingressClass := flags.Lookup("ingress-class")
	if ingressClass != nil {
		ingressClass.Value.Set("haproxy")
		ingressClass.DefValue = "haproxy"
	}
}

// OverrideFlags allows controller to override command line parameter flags
func (haproxy *HAProxyController) OverrideFlags(flags *pflag.FlagSet) {
	if *haproxy.reloadStrategy == "native" {
		haproxy.configFile = "/etc/haproxy/haproxy.cfg"
		haproxy.template = newTemplate("haproxy.tmpl", "/etc/haproxy/template/haproxy.tmpl")
	} else if *haproxy.reloadStrategy == "multibinder" {
		haproxy.configFile = "/etc/haproxy/haproxy.cfg.erb"
		haproxy.template = newTemplate("haproxy.cfg.erb.tmpl", "/etc/haproxy/haproxy.cfg.erb.tmpl")
	} else {
		glog.Fatalf("Unsupported reload strategy: %v", *haproxy.reloadStrategy)
	}
	haproxy.command = "/haproxy-reload.sh"
}

// SetConfig receives the ConfigMap the user has configured
func (haproxy *HAProxyController) SetConfig(configMap *api.ConfigMap) {
	haproxy.configMap = configMap
}

// BackendDefaults defines default values to the ingress core
func (haproxy *HAProxyController) BackendDefaults() defaults.Backend {
	return newHAProxyConfig(haproxy).Backend
}

// OnUpdate regenerate the configuration file of the backend
func (haproxy *HAProxyController) OnUpdate(cfg ingress.Configuration) ([]byte, error) {
	data, err := haproxy.template.execute(newControllerConfig(&cfg, haproxy))
	if err != nil {
		return nil, err
	}
	return data, nil
}

// Reload reload the backend if the configuration file has changed
func (haproxy *HAProxyController) Reload(data []byte) ([]byte, bool, error) {
	if !haproxy.configChanged(data) {
		return nil, false, nil
	}
	// TODO missing HAProxy validation before overwrite and try to reload
	err := ioutil.WriteFile(haproxy.configFile, data, 0644)
	if err != nil {
		return nil, false, err
	}
	out, err := haproxy.reloadHaproxy()
	if len(out) > 0 {
		glog.Infof("HAProxy output:\n%v", string(out))
	}
	return out, true, err
}

func (haproxy *HAProxyController) configChanged(data []byte) bool {
	if _, err := os.Stat(haproxy.configFile); os.IsNotExist(err) {
		return true
	}
	cfg, err := ioutil.ReadFile(haproxy.configFile)
	if err != nil {
		glog.Warningf("error reading haproxy config: %v")
		return false
	}
	return !bytes.Equal(cfg, data)
}

func (haproxy *HAProxyController) reloadHaproxy() ([]byte, error) {
	out, err := exec.Command(haproxy.command, *haproxy.reloadStrategy, haproxy.configFile).CombinedOutput()
	return out, err
}
<|endoftext|>


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

package controller

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/inctimer"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/metrics"
	"github.com/cilium/cilium/pkg/option"

	"github.com/go-openapi/strfmt"
	"github.com/sirupsen/logrus"
)

const (
	success = "success"
	failure = "failure"
)

// ControllerFunc is a function that the controller runs. This type is used for
// DoFunc and StopFunc.
type ControllerFunc func(ctx context.Context) error

// ExitReason is a returnable type from DoFunc that causes the
// controller to exit. This reason is recorded in the controller's status. The
// controller is not removed from any manager.
// Construct one with NewExitReason("a reason")
type ExitReason struct {
	// This is constucted in this odd way because the type assertion in
	// runController didn't work otherwise.
	error
}

// NewExitReason returns a new ExitReason
func NewExitReason(reason string) ExitReason {
	return ExitReason{errors.New(reason)}
}

// ControllerParams contains all parameters of a controller
type ControllerParams struct {
	// DoFunc is the function that will be run until it succeeds and/or
	// using the interval RunInterval if not 0.
	// An unset DoFunc is an error and will be logged as one.
	DoFunc ControllerFunc

	// StopFunc is called when the controller stops. It is intended to run any
	// clean-up tasks for the controller (e.g. deallocate/release resources)
	// It is guaranteed that DoFunc is called at least once before StopFunc is
	// called.
	// An unset StopFunc is not an error (and will be a no-op)
	// Note: Since this occurs on controller exit, error counts and tracking may
	// not be checked after StopFunc is run.
	StopFunc ControllerFunc

	// If set to any other value than 0, will cause DoFunc to be run in the
	// specified interval. The interval starts from when the DoFunc has
	// returned last
	RunInterval time.Duration

	// ErrorRetryBaseDuration is the initial time to wait to run DoFunc
	// again on return of an error. On each consecutive error, this value
	// is multiplied by the number of consecutive errors to provide a
	// constant back off. The default is 1s.
	ErrorRetryBaseDuration time.Duration

	// NoErrorRetry when set to true, disabled retries on errors
	NoErrorRetry bool

	Context context.Context
}

// undefinedDoFunc is used when no DoFunc is set. controller.DoFunc is set to this
// when the controller is incorrectly initialised.
func undefinedDoFunc(name string) error {
	return fmt.Errorf("controller %s DoFunc is nil", name)
}

// NoopFunc is a no-op placeholder for DoFunc & StopFunc.
// It is automatically used when StopFunc is undefined, and can be used as a
// DoFunc stub when the controller should only run StopFunc.
func NoopFunc(ctx context.Context) error {
	return nil
}

// Controller is a simple pattern that allows to perform the following
// tasks:
//   - Run an operation in the background and retry until it succeeds
//   - Perform a regular sync operation in the background
//
// A controller has configurable retry intervals and will collect statistics
// on number of successful runs, number of failures, last error message,
// and last error timestamp.
//
// Controllers have a name and are tied to a Manager. The manager is typically
// bound to higher level objects such as endpoint. These higher level objects
// can then run multiple controllers to perform async tasks such as:
//  - Annotating k8s resources with values
//  - Synchronizing an object with the kvstore
//  - Any other async operation to may fail and require retries
//
// Embedding the Manager into higher level resources allows to bind controllers
// to the lifetime of that object. Controllers also have a UUID to allow
// correlating all log messages of a controller instance.
//
// Guidelines to writing controllers:
// * Make sure that the task the controller performs is done in an atomic
//   fashion, e.g. if a controller modifies a resource in multiple steps, an
//   intermediate manipulation operation failing should not leave behind
//   an inconsistent state. This can typically be achieved by locking the
//   resource and rolling back or by using transactions.
// * Controllers typically act on behalf of a higher level object such as an
//   endpoint. The controller must ensure that the higher level object is
//   properly locked when accessing any fields.
// * Controllers run asynchronously in the background, it is the responsibility
//   of the controller to be aware of the lifecycle of the owning higher level
//   object. This is typically achieved by removing all controllers when the
//   owner dies. It is the responsibility of the owner to either lock the owner
//   in a way that will delay destruction throughout the controller run or to
//   check for the destruction throughout the run.
type Controller struct {
	mutex             lock.RWMutex
	name              string
	params            ControllerParams
	successCount      int
	lastSuccessStamp  time.Time
	failureCount      int
	consecutiveErrors int
	lastError         error
	lastErrorStamp    time.Time
	lastDuration      time.Duration
	uuid              string
	stop              chan struct{}
	update            chan struct{}
	trigger           chan struct{}
	ctxDoFunc         context.Context
	cancelDoFunc      context.CancelFunc

	// terminated is closed after the controller has been terminated
	terminated chan struct{}
}

// GetSuccessCount returns the number of successful controller runs
func (c *Controller) GetSuccessCount() int {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.successCount
}

// GetFailureCount returns the number of failed controller runs
func (c *Controller) GetFailureCount() int {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.failureCount
}

// GetLastError returns the last error returned
func (c *Controller) GetLastError() error {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.lastError
}

// Trigger triggers the controller
func (c *Controller) Trigger() {
	select {
	case c.trigger <- struct{}{}:
	default:
	}
}

// GetLastErrorTimestamp returns the last error returned
func (c *Controller) GetLastErrorTimestamp() time.Time {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.lastErrorStamp
}

func (c *Controller) runController() {
	errorRetries := 1

	c.mutex.RLock()
	params := c.params
	c.mutex.RUnlock()
	runFunc := true
	interval := 10 * time.Minute
	runTimer, timerDone := inctimer.New()
	defer timerDone()

	for {
		var err error
		if runFunc {
			interval = params.RunInterval

			start := time.Now()
			err = params.DoFunc(c.ctxDoFunc)
			duration := time.Since(start)

			c.mutex.Lock()
			c.lastDuration = duration
			c.getLogger().Debug("Controller func execution time: ", c.lastDuration)

			if err != nil {
				switch err := err.(type) {
				case ExitReason:
					// This is actually not an error case, but it causes an exit
					c.recordSuccess()
					c.lastError = err // This will be shown in the controller status

					// Don't exit the goroutine, since that only happens when the
					// controller is explicitly stopped. Instead, just wait for
					// the next update.
					c.getLogger().Debug("Controller run succeeded; waiting for next controller update or stop")
					runFunc = false
					interval = 10 * time.Minute

				default:
					c.getLogger().WithField(fieldConsecutiveErrors, errorRetries).
						WithError(err).Debug("Controller run failed")
					c.recordError(err)

					if !params.NoErrorRetry {
						if params.ErrorRetryBaseDuration != time.Duration(0) {
							interval = time.Duration(errorRetries) * params.ErrorRetryBaseDuration
						} else {
							interval = time.Duration(errorRetries) * time.Second
						}

						errorRetries++
					}
				}
			} else {
				c.recordSuccess()

				// reset error retries after successful attempt
				errorRetries = 1

				// If no run interval is specified, no further updates
				// are required.
				if interval == time.Duration(0) {
					// Don't exit the goroutine, since that only happens when the
					// controller is explicitly stopped. Instead, just wait for
					// the next update.
					c.getLogger().Debug("Controller run succeeded; waiting for next controller update or stop")
					runFunc = false
					interval = 10 * time.Minute
				}
			}

			c.mutex.Unlock()
		}
		select {
		case <-c.stop:
			goto shutdown

		case <-c.update:
			// If we receive a signal on both channels c.stop and c.update,
			// golang will pick either c.stop or c.update randomly.
			// This select will make sure we don't execute the controller
			// while we are shutting down.
			select {
			case <-c.stop:
				goto shutdown
			default:
			}
			// Pick up any changes to the parameters in case the controller has
			// been updated.
			c.mutex.RLock()
			params = c.params
			c.mutex.RUnlock()
			runFunc = true

		case <-runTimer.After(interval):
		case <-c.trigger:
		}

	}

shutdown:
	c.getLogger().Debug("Shutting down controller")

	if err := params.StopFunc(context.TODO()); err != nil {
		c.mutex.Lock()
		c.recordError(err)
		c.mutex.Unlock()
		c.getLogger().WithField(fieldConsecutiveErrors, errorRetries).
			WithError(err).Warn("Error on Controller stop")
	}

	close(c.terminated)
}

// updateParamsLocked sets the specified controller's parameters.
//
// If the RunInterval exceeds ControllerMaxInterval, it will be capped.
func (c *Controller) updateParamsLocked(params ControllerParams) {
	c.params = params

	maxInterval := time.Duration(option.Config.MaxControllerInterval) * time.Second
	if maxInterval > 0 && params.RunInterval > maxInterval {
		c.getLogger().Infof("Limiting interval to %s", maxInterval)
		c.params.RunInterval = maxInterval
	}
}

func (c *Controller) stopController() {
	if c.cancelDoFunc != nil {
		c.cancelDoFunc()
	}

	close(c.stop)
	close(c.update)
}

// logger returns a logrus object with controllerName and UUID fields.
func (c *Controller) getLogger() *logrus.Entry {
	return log.WithFields(logrus.Fields{
		fieldControllerName: c.name,
		fieldUUID:           c.uuid,
	})
}

// GetStatusModel returns a models.ControllerStatus representing the
// controller's configuration & status
func (c *Controller) GetStatusModel() *models.ControllerStatus {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	status := &models.ControllerStatus{
		Name: c.name,
		UUID: strfmt.UUID(c.uuid),
		Configuration: &models.ControllerStatusConfiguration{
			ErrorRetry:     !c.params.NoErrorRetry,
			ErrorRetryBase: strfmt.Duration(c.params.ErrorRetryBaseDuration),
			Interval:       strfmt.Duration(c.params.RunInterval),
		},
		Status: &models.ControllerStatusStatus{
			SuccessCount:            int64(c.successCount),
			LastSuccessTimestamp:    strfmt.DateTime(c.lastSuccessStamp),
			FailureCount:            int64(c.failureCount),
			LastFailureTimestamp:    strfmt.DateTime(c.lastErrorStamp),
			ConsecutiveFailureCount: int64(c.consecutiveErrors),
		},
	}

	if c.lastError != nil {
		status.Status.LastFailureMsg = c.lastError.Error()
	}

	return status
}

// recordError updates all statistic collection variables on error
// c.mutex must be held.
func (c *Controller) recordError(err error) {
	c.lastError = err
	c.lastErrorStamp = time.Now()
	c.failureCount++
	c.consecutiveErrors++
	metrics.ControllerRuns.WithLabelValues(failure).Inc()
	metrics.ControllerRunsDuration.WithLabelValues(failure).Observe(c.lastDuration.Seconds())
}

// recordSuccess updates all statistic collection variables on success
// c.mutex must be held.
func (c *Controller) recordSuccess() {
	c.lastError = nil
	c.lastSuccessStamp = time.Now()
	c.successCount++
	c.consecutiveErrors = 0

	metrics.ControllerRuns.WithLabelValues(success).Inc()
	metrics.ControllerRunsDuration.WithLabelValues(success).Observe(c.lastDuration.Seconds())
}
<commit_msg>pkg/controller: set runFunc to when controller is manually triggered<commit_after>// SPDX-License-Identifier: Apache-2.0
// Copyright 2018 Authors of Cilium

package controller

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/inctimer"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/metrics"
	"github.com/cilium/cilium/pkg/option"

	"github.com/go-openapi/strfmt"
	"github.com/sirupsen/logrus"
)

const (
	success = "success"
	failure = "failure"
)

// ControllerFunc is a function that the controller runs. This type is used for
// DoFunc and StopFunc.
type ControllerFunc func(ctx context.Context) error

// ExitReason is a returnable type from DoFunc that causes the
// controller to exit. This reason is recorded in the controller's status. The
// controller is not removed from any manager.
// Construct one with NewExitReason("a reason")
type ExitReason struct {
	// This is constucted in this odd way because the type assertion in
	// runController didn't work otherwise.
	error
}

// NewExitReason returns a new ExitReason
func NewExitReason(reason string) ExitReason {
	return ExitReason{errors.New(reason)}
}

// ControllerParams contains all parameters of a controller
type ControllerParams struct {
	// DoFunc is the function that will be run until it succeeds and/or
	// using the interval RunInterval if not 0.
	// An unset DoFunc is an error and will be logged as one.
	DoFunc ControllerFunc

	// StopFunc is called when the controller stops. It is intended to run any
	// clean-up tasks for the controller (e.g. deallocate/release resources)
	// It is guaranteed that DoFunc is called at least once before StopFunc is
	// called.
	// An unset StopFunc is not an error (and will be a no-op)
	// Note: Since this occurs on controller exit, error counts and tracking may
	// not be checked after StopFunc is run.
	StopFunc ControllerFunc

	// If set to any other value than 0, will cause DoFunc to be run in the
	// specified interval. The interval starts from when the DoFunc has
	// returned last
	RunInterval time.Duration

	// ErrorRetryBaseDuration is the initial time to wait to run DoFunc
	// again on return of an error. On each consecutive error, this value
	// is multiplied by the number of consecutive errors to provide a
	// constant back off. The default is 1s.
	ErrorRetryBaseDuration time.Duration

	// NoErrorRetry when set to true, disabled retries on errors
	NoErrorRetry bool

	Context context.Context
}

// undefinedDoFunc is used when no DoFunc is set. controller.DoFunc is set to this
// when the controller is incorrectly initialised.
func undefinedDoFunc(name string) error {
	return fmt.Errorf("controller %s DoFunc is nil", name)
}

// NoopFunc is a no-op placeholder for DoFunc & StopFunc.
// It is automatically used when StopFunc is undefined, and can be used as a
// DoFunc stub when the controller should only run StopFunc.
func NoopFunc(ctx context.Context) error {
	return nil
}

// Controller is a simple pattern that allows to perform the following
// tasks:
//   - Run an operation in the background and retry until it succeeds
//   - Perform a regular sync operation in the background
//
// A controller has configurable retry intervals and will collect statistics
// on number of successful runs, number of failures, last error message,
// and last error timestamp.
//
// Controllers have a name and are tied to a Manager. The manager is typically
// bound to higher level objects such as endpoint. These higher level objects
// can then run multiple controllers to perform async tasks such as:
//  - Annotating k8s resources with values
//  - Synchronizing an object with the kvstore
//  - Any other async operation to may fail and require retries
//
// Embedding the Manager into higher level resources allows to bind controllers
// to the lifetime of that object. Controllers also have a UUID to allow
// correlating all log messages of a controller instance.
//
// Guidelines to writing controllers:
// * Make sure that the task the controller performs is done in an atomic
//   fashion, e.g. if a controller modifies a resource in multiple steps, an
//   intermediate manipulation operation failing should not leave behind
//   an inconsistent state. This can typically be achieved by locking the
//   resource and rolling back or by using transactions.
// * Controllers typically act on behalf of a higher level object such as an
//   endpoint. The controller must ensure that the higher level object is
//   properly locked when accessing any fields.
// * Controllers run asynchronously in the background, it is the responsibility
//   of the controller to be aware of the lifecycle of the owning higher level
//   object. This is typically achieved by removing all controllers when the
//   owner dies. It is the responsibility of the owner to either lock the owner
//   in a way that will delay destruction throughout the controller run or to
//   check for the destruction throughout the run.
type Controller struct {
	mutex             lock.RWMutex
	name              string
	params            ControllerParams
	successCount      int
	lastSuccessStamp  time.Time
	failureCount      int
	consecutiveErrors int
	lastError         error
	lastErrorStamp    time.Time
	lastDuration      time.Duration
	uuid              string
	stop              chan struct{}
	update            chan struct{}
	trigger           chan struct{}
	ctxDoFunc         context.Context
	cancelDoFunc      context.CancelFunc

	// terminated is closed after the controller has been terminated
	terminated chan struct{}
}

// GetSuccessCount returns the number of successful controller runs
func (c *Controller) GetSuccessCount() int {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.successCount
}

// GetFailureCount returns the number of failed controller runs
func (c *Controller) GetFailureCount() int {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.failureCount
}

// GetLastError returns the last error returned
func (c *Controller) GetLastError() error {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.lastError
}

// Trigger triggers the controller
func (c *Controller) Trigger() {
	select {
	case c.trigger <- struct{}{}:
	default:
	}
}

// GetLastErrorTimestamp returns the last error returned
func (c *Controller) GetLastErrorTimestamp() time.Time {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.lastErrorStamp
}

func (c *Controller) runController() {
	errorRetries := 1

	c.mutex.RLock()
	params := c.params
	c.mutex.RUnlock()
	runFunc := true
	interval := 10 * time.Minute
	runTimer, timerDone := inctimer.New()
	defer timerDone()

	for {
		var err error
		if runFunc {
			interval = params.RunInterval

			start := time.Now()
			err = params.DoFunc(c.ctxDoFunc)
			duration := time.Since(start)

			c.mutex.Lock()
			c.lastDuration = duration
			c.getLogger().Debug("Controller func execution time: ", c.lastDuration)

			if err != nil {
				switch err := err.(type) {
				case ExitReason:
					// This is actually not an error case, but it causes an exit
					c.recordSuccess()
					c.lastError = err // This will be shown in the controller status

					// Don't exit the goroutine, since that only happens when the
					// controller is explicitly stopped. Instead, just wait for
					// the next update.
					c.getLogger().Debug("Controller run succeeded; waiting for next controller update or stop")
					runFunc = false
					interval = 10 * time.Minute

				default:
					c.getLogger().WithField(fieldConsecutiveErrors, errorRetries).
						WithError(err).Debug("Controller run failed")
					c.recordError(err)

					if !params.NoErrorRetry {
						if params.ErrorRetryBaseDuration != time.Duration(0) {
							interval = time.Duration(errorRetries) * params.ErrorRetryBaseDuration
						} else {
							interval = time.Duration(errorRetries) * time.Second
						}

						errorRetries++
					}
				}
			} else {
				c.recordSuccess()

				// reset error retries after successful attempt
				errorRetries = 1

				// If no run interval is specified, no further updates
				// are required.
				if interval == time.Duration(0) {
					// Don't exit the goroutine, since that only happens when the
					// controller is explicitly stopped. Instead, just wait for
					// the next update.
					c.getLogger().Debug("Controller run succeeded; waiting for next controller update or stop")
					runFunc = false
					interval = 10 * time.Minute
				}
			}

			c.mutex.Unlock()
		}
		select {
		case <-c.stop:
			goto shutdown

		case <-c.update:
			// If we receive a signal on both channels c.stop and c.update,
			// golang will pick either c.stop or c.update randomly.
			// This select will make sure we don't execute the controller
			// while we are shutting down.
			select {
			case <-c.stop:
				goto shutdown
			default:
			}
			// Pick up any changes to the parameters in case the controller has
			// been updated.
			c.mutex.RLock()
			params = c.params
			c.mutex.RUnlock()
			runFunc = true

		case <-runTimer.After(interval):
		case <-c.trigger:
			runFunc = true
		}

	}

shutdown:
	c.getLogger().Debug("Shutting down controller")

	if err := params.StopFunc(context.TODO()); err != nil {
		c.mutex.Lock()
		c.recordError(err)
		c.mutex.Unlock()
		c.getLogger().WithField(fieldConsecutiveErrors, errorRetries).
			WithError(err).Warn("Error on Controller stop")
	}

	close(c.terminated)
}

// updateParamsLocked sets the specified controller's parameters.
//
// If the RunInterval exceeds ControllerMaxInterval, it will be capped.
func (c *Controller) updateParamsLocked(params ControllerParams) {
	c.params = params

	maxInterval := time.Duration(option.Config.MaxControllerInterval) * time.Second
	if maxInterval > 0 && params.RunInterval > maxInterval {
		c.getLogger().Infof("Limiting interval to %s", maxInterval)
		c.params.RunInterval = maxInterval
	}
}

func (c *Controller) stopController() {
	if c.cancelDoFunc != nil {
		c.cancelDoFunc()
	}

	close(c.stop)
	close(c.update)
}

// logger returns a logrus object with controllerName and UUID fields.
func (c *Controller) getLogger() *logrus.Entry {
	return log.WithFields(logrus.Fields{
		fieldControllerName: c.name,
		fieldUUID:           c.uuid,
	})
}

// GetStatusModel returns a models.ControllerStatus representing the
// controller's configuration & status
func (c *Controller) GetStatusModel() *models.ControllerStatus {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	status := &models.ControllerStatus{
		Name: c.name,
		UUID: strfmt.UUID(c.uuid),
		Configuration: &models.ControllerStatusConfiguration{
			ErrorRetry:     !c.params.NoErrorRetry,
			ErrorRetryBase: strfmt.Duration(c.params.ErrorRetryBaseDuration),
			Interval:       strfmt.Duration(c.params.RunInterval),
		},
		Status: &models.ControllerStatusStatus{
			SuccessCount:            int64(c.successCount),
			LastSuccessTimestamp:    strfmt.DateTime(c.lastSuccessStamp),
			FailureCount:            int64(c.failureCount),
			LastFailureTimestamp:    strfmt.DateTime(c.lastErrorStamp),
			ConsecutiveFailureCount: int64(c.consecutiveErrors),
		},
	}

	if c.lastError != nil {
		status.Status.LastFailureMsg = c.lastError.Error()
	}

	return status
}

// recordError updates all statistic collection variables on error
// c.mutex must be held.
func (c *Controller) recordError(err error) {
	c.lastError = err
	c.lastErrorStamp = time.Now()
	c.failureCount++
	c.consecutiveErrors++
	metrics.ControllerRuns.WithLabelValues(failure).Inc()
	metrics.ControllerRunsDuration.WithLabelValues(failure).Observe(c.lastDuration.Seconds())
}

// recordSuccess updates all statistic collection variables on success
// c.mutex must be held.
func (c *Controller) recordSuccess() {
	c.lastError = nil
	c.lastSuccessStamp = time.Now()
	c.successCount++
	c.consecutiveErrors = 0

	metrics.ControllerRuns.WithLabelValues(success).Inc()
	metrics.ControllerRunsDuration.WithLabelValues(success).Observe(c.lastDuration.Seconds())
}
<|endoftext|>


<commit_before>package core

import (
	"fmt"
	"strings"
	"time"

	"github.com/supergiant/supergiant/pkg/kubernetes"
	"github.com/supergiant/supergiant/pkg/model"
)

var (
	minAgeToExist           = 20 * time.Minute // this is used to prevent adding more nodes while still-pending pods are scheduling to a new node
	maxClusteredPodsPerNode = 2                // prevent putting all nodes of a cluster on one host node
	maxDisksPerNode         = 11
	trackedEventMessages    = [...]string{
		"MatchNodeSelector",
		"PodExceedsMaxPodNumber",
		"PodExceedsFreeMemory",
		"PodExceedsFreeCPU",
		"no nodes available to schedule pods",
		"failed to fit in any node"
		"Insufficient cpu",
		"Insufficient memory",
	}
)

type CapacityService struct {
	Core            *Core
	WaitBeforeScale time.Duration
}

func (s *CapacityService) Perform() error {
	var kubes []*model.Kube
	if err := s.Core.DB.Preload("CloudAccount").Find(&kubes, "ready = ?", true); err != nil {
		return err
	}

	// TODO
	// 1. Concurrency        -- inParallel? probably shouldn't be on collection
	// 2. "scaling" should be an action on Kube, so we can see the status (actually that may not make sense, just use Nodes ?)

	for _, kube := range kubes {
		if err := newKubeScaler(s, kube).Scale(); err != nil {
			return err
		}
	}

	return nil
}

//------------------------------------------------------------------------------

type KubeScaler struct {
	service         *CapacityService
	kube            *model.Kube
	nodeSizes       []*NodeSize
	largestNodeSize *NodeSize
}

func newKubeScaler(service *CapacityService, kube *model.Kube) *KubeScaler {
	s := &KubeScaler{service: service, kube: kube}
	// We iterate on all nodeSizes here first to preserve the cost order
	for _, it := range service.Core.NodeSizes[kube.CloudAccount.Provider] {
		for _, nodeSizeID := range kube.NodeSizes {
			if it.Name == nodeSizeID {
				s.nodeSizes = append(s.nodeSizes, it)
				break
			}
		}
	}
	s.largestNodeSize = s.nodeSizes[len(s.nodeSizes)-1]
	return s
}

//------------------------------------------------------------------------------

func (s *KubeScaler) Scale() error {
	incomingPods, err := s.incomingPods()
	if err != nil {
		return fmt.Errorf("Capacity service error when fetching incoming pods: %s", err)
	}

	var projectedNodes []*projectedNode
	for _, pod := range incomingPods {
		projectedNodes = append(projectedNodes, &projectedNode{
			false,
			s.largestNodeSize,
			[]*kubernetes.Pod{pod},
		})
	}

	for {
		var (
			pnode1      *projectedNode
			pnode2      *projectedNode
			pnode2Index int
		)

		//==========================================================================
		// find an uncommitted nodeAndPod
		//==========================================================================

		for _, pnode := range projectedNodes {
			if !pnode.Committed {
				pnode1 = pnode
				break
			}
		}

		if pnode1 == nil {
			break
		}

		//==========================================================================
		// find a pnode2 you can merge pnode1 with
		//==========================================================================

		for pnode2IndexCandidate, pnode2Candidate := range projectedNodes {
			if pnode2Candidate == pnode1 { // don't want to merge with self
				continue
			}

			if pnode1.canMergeWith(pnode2Candidate) {
				pnode2 = pnode2Candidate
				pnode2Index = pnode2IndexCandidate
				break
			}
		}

		//==========================================================================
		// merge if found, OR scale down to the smallest instance size it can use and commit it
		//==========================================================================

		if pnode2 != nil {
			// Delete the partner being merged, and merge pods
			i := pnode2Index
			projectedNodes = append(projectedNodes[:i], projectedNodes[i+1:]...)
			pnode1.Pods = append(pnode1.Pods, pnode2.Pods...)
		} else {
			// If we can't merge with anyone, can we scale down to the lowest cost?
			// nodeSizes are asc. by cost, so the first we find is the cheapest.
			for _, nodeSize := range s.nodeSizes {
				if nodeSize.CPUCores >= pnode1.usedCPU() && nodeSize.RAMGIB >= pnode1.usedRAM() {
					pnode1.Size = nodeSize
					pnode1.Committed = true
					break
				}
			}

			if !pnode1.Committed {
				return fmt.Errorf("There is no Node size configured large enough to support %.1f Cores and %1.fGiB RAM", pnode1.usedCPU(), pnode1.usedRAM())
			}
		}
	}

	// Load existing Nodes
	// s.kube.Nodes = make([]*model.Node, 0)
	if err := s.service.Core.DB.Preload("Kube.CloudAccount").Find(&s.kube.Nodes, "kube_name = ?", s.kube.Name); err != nil {
		return err
	}

	for _, node := range s.kube.Nodes {

		// TODO ---- need to label them to prevent disk overflow

		// eventual option to delete nodes when there are pods (w/ or wo/ volumes?) that could move to other nodes (we would have to calculate that)

		hasPods, err := hasPodsWithReservedResources(s.service.Core, node)
		if err != nil {
			return fmt.Errorf("Capacity service error when fetching Pods for Node: %s", err)
		}

		if !hasPods && time.Since(node.ProviderCreationTimestamp) > minAgeToExist {

			s.service.Core.Log.Infof("Terminating node %s", node.Name)

			if err := s.service.Core.Nodes.Delete(node.ID, node).Now(); err != nil {
				return fmt.Errorf("Capacity service error when deleting Node: %s", err)
			}
		}
	}

	//----------------------------------------------------------------------------
	// for _, pnode := range projectedNodes {
	// 	fmt.Println(pnode.Size.Name)
	// 	for _, pod := range pnode.Pods {
	// 		fmt.Println(pod.Metadata.Name)
	// 		for _, container := range pod.Spec.Containers {
	// 			fmt.Println("CPU", container.Resources.Limits.CPU)
	// 			fmt.Println("RAM", container.Resources.Limits.Memory)
	// 		}
	// 	}
	// 	fmt.Println("")
	// }
	//----------------------------------------------------------------------------

	for _, pnode := range projectedNodes {
		node := &model.Node{
			KubeName: s.kube.Name,
			Size:     pnode.Size.Name,
		}

		// If there's an existing node which is spinning up with this type, then
		// don't create.
		// This is a big TODO -- the logic should be much tighter, allowing ongoing
		// projection of pods onto nodes that are still spinning up.

		alreadySpinningUp := false
		for _, existingNode := range s.kube.Nodes {

			if existingNode.Size == node.Size && time.Since(existingNode.ProviderCreationTimestamp) < minAgeToExist {
				// This may be a node that is already being created, or NOTE it could
				// be a broken node that we erroneously identify as spinning up.
				alreadySpinningUp = true
				break
			}
		}
		if alreadySpinningUp {
			s.service.Core.Log.Infof("Capacity service is already waiting on new node with size %s", node.Size)
			continue
		}

		s.service.Core.Log.Infof("Capacity service is creating node with size %s", node.Size)

		if err := s.service.Core.Nodes.Create(node); err != nil {
			return fmt.Errorf("Capacity service error when creating Node: %s", err)
		}
	}
	return nil
}

////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

func (s *KubeScaler) hasTrackedEvent(pod *kubernetes.Pod) (bool, error) {
	k8s := s.service.Core.K8S(s.kube)

	events, err := k8s.ListEvents("fieldSelector=involvedObject.name=" + pod.Metadata.Name)
	if err != nil {
		return false, err
	}

	for _, event := range events {
		for _, message := range trackedEventMessages {
			if strings.Contains(event.Message, message) {
				return true, nil
			}
		}
	}
	return false, nil
}

func (s *KubeScaler) incomingPods() (incomingPods []*kubernetes.Pod, err error) {
	waitStart := time.Now()

	k8s := s.service.Core.K8S(s.kube)

	for {
		incomingPods = incomingPods[:0] // reset

		pendingPods, err := k8s.ListPods("fieldSelector=status.phase=Pending")
		if err != nil {
			return nil, err
		}

		for _, pod := range pendingPods {
			hasTrackedEvent, err := s.hasTrackedEvent(pod)
			if err != nil {
				return nil, err
			}
			if hasTrackedEvent {
				incomingPods = append(incomingPods, pod)
			}
		}

		elapsed := time.Since(waitStart)
		incomingCount := len(incomingPods)

		if incomingCount > 0 && elapsed < s.service.WaitBeforeScale {
			s.service.Core.Log.Infof("Waiting to add nodes for %d pods; %.1f seconds elapsed", incomingCount, elapsed.Seconds())

			for _, pod := range incomingPods {
				fmt.Println(pod.Metadata.Name)
			}

			time.Sleep(5 * time.Second)
		} else {
			break
		}
	}

	return incomingPods, nil
}

//------------------------------------------------------------------------------

type projectedNode struct {
	Committed bool
	Size      *NodeSize
	Pods      []*kubernetes.Pod
}

func (pnode *projectedNode) usedRAM() (u float64) {
	for _, pod := range pnode.Pods {
		for _, container := range pod.Spec.Containers {

			// NOTE we use limits here, and not requests, because we want to spin up
			// nodes that are at least slightly bigger than the user thinks the pod
			// could utilize. This will ensure that the user's limit CAN BE FILLED AT
			// ALL. This is at the Core of our increased-utilization strategy.

			memStr := container.Resources.Limits.Memory
			if memStr == "" {
				memStr = container.Resources.Requests.Memory
			}

			gib, err := kubernetes.GiBFromMemString(memStr)
			if err != nil {
				panic(err)
			}

			u += gib
		}
	}
	return
}

func (pnode *projectedNode) usedCPU() (u float64) {
	for _, pod := range pnode.Pods {
		for _, container := range pod.Spec.Containers {

			// NOTE above in usedRAM

			cpuStr := container.Resources.Limits.CPU
			if cpuStr == "" {
				cpuStr = container.Resources.Requests.CPU
			}

			Cores, err := kubernetes.CoresFromCPUString(cpuStr)
			if err != nil {
				panic(err)
			}

			u += Cores
		}
	}
	return
}

func (pnode *projectedNode) usedVolumes() (u int) {
	for _, pod := range pnode.Pods {
		for _, vol := range pod.Spec.Volumes {
			if vol.AwsElasticBlockStore != nil || vol.FlexVolume != nil {
				u++
			}
		}
	}
	return
}

func (pnode1 *projectedNode) canMergeWith(pnode2 *projectedNode) bool {
	usedCPU := pnode1.usedCPU() + pnode2.usedCPU()
	usedRAM := pnode1.usedRAM() + pnode2.usedRAM()
	usedVolumes := pnode1.usedVolumes() + pnode2.usedVolumes()
	return pnode1.Size.CPUCores >= usedCPU && pnode1.Size.RAMGIB >= usedRAM && usedVolumes <= maxDisksPerNode
}

//------------------------------------------------------------------------------

func hasPodsWithReservedResources(c *Core, node *model.Node) (bool, error) {
	k8s := c.K8S(node.Kube)
	pods, err := k8s.ListPods("fieldSelector=spec.nodeName=" + node.Name + ",status.phase=Running")
	if err != nil {
		return false, err
	}

	for _, pod := range pods {
		for _, container := range pod.Spec.Containers {

			// TODO
			//
			// These should be moved to json Unmarshal method, so these floats can be parsed once
			//
			gib, err := kubernetes.GiBFromMemString(container.Resources.Requests.Memory)
			if err != nil {
				return false, err
			}
			cores, err := kubernetes.CoresFromCPUString(container.Resources.Requests.CPU)
			if err != nil {
				return false, err
			}

			if gib > 0 || cores > 0 {
				return true, nil
			}
		}
	}

	return false, nil
}
<commit_msg>Adding missing comma<commit_after>package core

import (
	"fmt"
	"strings"
	"time"

	"github.com/supergiant/supergiant/pkg/kubernetes"
	"github.com/supergiant/supergiant/pkg/model"
)

var (
	minAgeToExist           = 20 * time.Minute // this is used to prevent adding more nodes while still-pending pods are scheduling to a new node
	maxClusteredPodsPerNode = 2                // prevent putting all nodes of a cluster on one host node
	maxDisksPerNode         = 11
	trackedEventMessages    = [...]string{
		"MatchNodeSelector",
		"PodExceedsMaxPodNumber",
		"PodExceedsFreeMemory",
		"PodExceedsFreeCPU",
		"no nodes available to schedule pods",
		"failed to fit in any node",
		"Insufficient cpu",
		"Insufficient memory",
	}
)

type CapacityService struct {
	Core            *Core
	WaitBeforeScale time.Duration
}

func (s *CapacityService) Perform() error {
	var kubes []*model.Kube
	if err := s.Core.DB.Preload("CloudAccount").Find(&kubes, "ready = ?", true); err != nil {
		return err
	}

	// TODO
	// 1. Concurrency        -- inParallel? probably shouldn't be on collection
	// 2. "scaling" should be an action on Kube, so we can see the status (actually that may not make sense, just use Nodes ?)

	for _, kube := range kubes {
		if err := newKubeScaler(s, kube).Scale(); err != nil {
			return err
		}
	}

	return nil
}

//------------------------------------------------------------------------------

type KubeScaler struct {
	service         *CapacityService
	kube            *model.Kube
	nodeSizes       []*NodeSize
	largestNodeSize *NodeSize
}

func newKubeScaler(service *CapacityService, kube *model.Kube) *KubeScaler {
	s := &KubeScaler{service: service, kube: kube}
	// We iterate on all nodeSizes here first to preserve the cost order
	for _, it := range service.Core.NodeSizes[kube.CloudAccount.Provider] {
		for _, nodeSizeID := range kube.NodeSizes {
			if it.Name == nodeSizeID {
				s.nodeSizes = append(s.nodeSizes, it)
				break
			}
		}
	}
	s.largestNodeSize = s.nodeSizes[len(s.nodeSizes)-1]
	return s
}

//------------------------------------------------------------------------------

func (s *KubeScaler) Scale() error {
	incomingPods, err := s.incomingPods()
	if err != nil {
		return fmt.Errorf("Capacity service error when fetching incoming pods: %s", err)
	}

	var projectedNodes []*projectedNode
	for _, pod := range incomingPods {
		projectedNodes = append(projectedNodes, &projectedNode{
			false,
			s.largestNodeSize,
			[]*kubernetes.Pod{pod},
		})
	}

	for {
		var (
			pnode1      *projectedNode
			pnode2      *projectedNode
			pnode2Index int
		)

		//==========================================================================
		// find an uncommitted nodeAndPod
		//==========================================================================

		for _, pnode := range projectedNodes {
			if !pnode.Committed {
				pnode1 = pnode
				break
			}
		}

		if pnode1 == nil {
			break
		}

		//==========================================================================
		// find a pnode2 you can merge pnode1 with
		//==========================================================================

		for pnode2IndexCandidate, pnode2Candidate := range projectedNodes {
			if pnode2Candidate == pnode1 { // don't want to merge with self
				continue
			}

			if pnode1.canMergeWith(pnode2Candidate) {
				pnode2 = pnode2Candidate
				pnode2Index = pnode2IndexCandidate
				break
			}
		}

		//==========================================================================
		// merge if found, OR scale down to the smallest instance size it can use and commit it
		//==========================================================================

		if pnode2 != nil {
			// Delete the partner being merged, and merge pods
			i := pnode2Index
			projectedNodes = append(projectedNodes[:i], projectedNodes[i+1:]...)
			pnode1.Pods = append(pnode1.Pods, pnode2.Pods...)
		} else {
			// If we can't merge with anyone, can we scale down to the lowest cost?
			// nodeSizes are asc. by cost, so the first we find is the cheapest.
			for _, nodeSize := range s.nodeSizes {
				if nodeSize.CPUCores >= pnode1.usedCPU() && nodeSize.RAMGIB >= pnode1.usedRAM() {
					pnode1.Size = nodeSize
					pnode1.Committed = true
					break
				}
			}

			if !pnode1.Committed {
				return fmt.Errorf("There is no Node size configured large enough to support %.1f Cores and %1.fGiB RAM", pnode1.usedCPU(), pnode1.usedRAM())
			}
		}
	}

	// Load existing Nodes
	// s.kube.Nodes = make([]*model.Node, 0)
	if err := s.service.Core.DB.Preload("Kube.CloudAccount").Find(&s.kube.Nodes, "kube_name = ?", s.kube.Name); err != nil {
		return err
	}

	for _, node := range s.kube.Nodes {

		// TODO ---- need to label them to prevent disk overflow

		// eventual option to delete nodes when there are pods (w/ or wo/ volumes?) that could move to other nodes (we would have to calculate that)

		hasPods, err := hasPodsWithReservedResources(s.service.Core, node)
		if err != nil {
			return fmt.Errorf("Capacity service error when fetching Pods for Node: %s", err)
		}

		if !hasPods && time.Since(node.ProviderCreationTimestamp) > minAgeToExist {

			s.service.Core.Log.Infof("Terminating node %s", node.Name)

			if err := s.service.Core.Nodes.Delete(node.ID, node).Now(); err != nil {
				return fmt.Errorf("Capacity service error when deleting Node: %s", err)
			}
		}
	}

	//----------------------------------------------------------------------------
	// for _, pnode := range projectedNodes {
	// 	fmt.Println(pnode.Size.Name)
	// 	for _, pod := range pnode.Pods {
	// 		fmt.Println(pod.Metadata.Name)
	// 		for _, container := range pod.Spec.Containers {
	// 			fmt.Println("CPU", container.Resources.Limits.CPU)
	// 			fmt.Println("RAM", container.Resources.Limits.Memory)
	// 		}
	// 	}
	// 	fmt.Println("")
	// }
	//----------------------------------------------------------------------------

	for _, pnode := range projectedNodes {
		node := &model.Node{
			KubeName: s.kube.Name,
			Size:     pnode.Size.Name,
		}

		// If there's an existing node which is spinning up with this type, then
		// don't create.
		// This is a big TODO -- the logic should be much tighter, allowing ongoing
		// projection of pods onto nodes that are still spinning up.

		alreadySpinningUp := false
		for _, existingNode := range s.kube.Nodes {

			if existingNode.Size == node.Size && time.Since(existingNode.ProviderCreationTimestamp) < minAgeToExist {
				// This may be a node that is already being created, or NOTE it could
				// be a broken node that we erroneously identify as spinning up.
				alreadySpinningUp = true
				break
			}
		}
		if alreadySpinningUp {
			s.service.Core.Log.Infof("Capacity service is already waiting on new node with size %s", node.Size)
			continue
		}

		s.service.Core.Log.Infof("Capacity service is creating node with size %s", node.Size)

		if err := s.service.Core.Nodes.Create(node); err != nil {
			return fmt.Errorf("Capacity service error when creating Node: %s", err)
		}
	}
	return nil
}

////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
////////////////////////////////////////////////////////////////////////////////
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

func (s *KubeScaler) hasTrackedEvent(pod *kubernetes.Pod) (bool, error) {
	k8s := s.service.Core.K8S(s.kube)

	events, err := k8s.ListEvents("fieldSelector=involvedObject.name=" + pod.Metadata.Name)
	if err != nil {
		return false, err
	}

	for _, event := range events {
		for _, message := range trackedEventMessages {
			if strings.Contains(event.Message, message) {
				return true, nil
			}
		}
	}
	return false, nil
}

func (s *KubeScaler) incomingPods() (incomingPods []*kubernetes.Pod, err error) {
	waitStart := time.Now()

	k8s := s.service.Core.K8S(s.kube)

	for {
		incomingPods = incomingPods[:0] // reset

		pendingPods, err := k8s.ListPods("fieldSelector=status.phase=Pending")
		if err != nil {
			return nil, err
		}

		for _, pod := range pendingPods {
			hasTrackedEvent, err := s.hasTrackedEvent(pod)
			if err != nil {
				return nil, err
			}
			if hasTrackedEvent {
				incomingPods = append(incomingPods, pod)
			}
		}

		elapsed := time.Since(waitStart)
		incomingCount := len(incomingPods)

		if incomingCount > 0 && elapsed < s.service.WaitBeforeScale {
			s.service.Core.Log.Infof("Waiting to add nodes for %d pods; %.1f seconds elapsed", incomingCount, elapsed.Seconds())

			for _, pod := range incomingPods {
				fmt.Println(pod.Metadata.Name)
			}

			time.Sleep(5 * time.Second)
		} else {
			break
		}
	}

	return incomingPods, nil
}

//------------------------------------------------------------------------------

type projectedNode struct {
	Committed bool
	Size      *NodeSize
	Pods      []*kubernetes.Pod
}

func (pnode *projectedNode) usedRAM() (u float64) {
	for _, pod := range pnode.Pods {
		for _, container := range pod.Spec.Containers {

			// NOTE we use limits here, and not requests, because we want to spin up
			// nodes that are at least slightly bigger than the user thinks the pod
			// could utilize. This will ensure that the user's limit CAN BE FILLED AT
			// ALL. This is at the Core of our increased-utilization strategy.

			memStr := container.Resources.Limits.Memory
			if memStr == "" {
				memStr = container.Resources.Requests.Memory
			}

			gib, err := kubernetes.GiBFromMemString(memStr)
			if err != nil {
				panic(err)
			}

			u += gib
		}
	}
	return
}

func (pnode *projectedNode) usedCPU() (u float64) {
	for _, pod := range pnode.Pods {
		for _, container := range pod.Spec.Containers {

			// NOTE above in usedRAM

			cpuStr := container.Resources.Limits.CPU
			if cpuStr == "" {
				cpuStr = container.Resources.Requests.CPU
			}

			Cores, err := kubernetes.CoresFromCPUString(cpuStr)
			if err != nil {
				panic(err)
			}

			u += Cores
		}
	}
	return
}

func (pnode *projectedNode) usedVolumes() (u int) {
	for _, pod := range pnode.Pods {
		for _, vol := range pod.Spec.Volumes {
			if vol.AwsElasticBlockStore != nil || vol.FlexVolume != nil {
				u++
			}
		}
	}
	return
}

func (pnode1 *projectedNode) canMergeWith(pnode2 *projectedNode) bool {
	usedCPU := pnode1.usedCPU() + pnode2.usedCPU()
	usedRAM := pnode1.usedRAM() + pnode2.usedRAM()
	usedVolumes := pnode1.usedVolumes() + pnode2.usedVolumes()
	return pnode1.Size.CPUCores >= usedCPU && pnode1.Size.RAMGIB >= usedRAM && usedVolumes <= maxDisksPerNode
}

//------------------------------------------------------------------------------

func hasPodsWithReservedResources(c *Core, node *model.Node) (bool, error) {
	k8s := c.K8S(node.Kube)
	pods, err := k8s.ListPods("fieldSelector=spec.nodeName=" + node.Name + ",status.phase=Running")
	if err != nil {
		return false, err
	}

	for _, pod := range pods {
		for _, container := range pod.Spec.Containers {

			// TODO
			//
			// These should be moved to json Unmarshal method, so these floats can be parsed once
			//
			gib, err := kubernetes.GiBFromMemString(container.Resources.Requests.Memory)
			if err != nil {
				return false, err
			}
			cores, err := kubernetes.CoresFromCPUString(container.Resources.Requests.CPU)
			if err != nil {
				return false, err
			}

			if gib > 0 || cores > 0 {
				return true, nil
			}
		}
	}

	return false, nil
}
<|endoftext|>


<commit_before>package main

import (
	"io/ioutil"
	"os"
	"path"
	"time"

	metrics "github.com/uber-go/tally/m3"
	zap "github.com/uber-go/zap"
	zanzibar "github.com/uber/zanzibar/runtime"
	yaml "gopkg.in/yaml.v2"

	"github.com/uber/zanzibar/examples/example-gateway/clients"
	exampleConfig "github.com/uber/zanzibar/examples/example-gateway/config"
	endpoints "github.com/uber/zanzibar/examples/example-gateway/endpoints"
)

const defaultM3MaxQueueSize = 10000
const defaultM3MaxPacketSize = 1440 // 1440kb in UDP M3MaxPacketSize
const defaultM3FlushInterval = 500 * time.Millisecond

func loadConfig(config *exampleConfig.Config) error {
	configRoot := os.Getenv("UBER_CONFIG_DIR")
	filePath := path.Join(configRoot, "production.yaml")

	data, err := ioutil.ReadFile(filePath)
	if err != nil {
		return err
	}

	if err := yaml.Unmarshal(data, config); err != nil {
		return err
	}

	return nil
}

func main() {
	gatewayConfig := &exampleConfig.Config{}
	tempLogger := zap.New(
		zap.NewJSONEncoder(),
		zap.Output(os.Stderr),
	)

	if err := loadConfig(gatewayConfig); err != nil {
		tempLogger.Error("Error initializing configuration",
			zap.String("error", err.Error()),
		)
		os.Exit(1)
	}

	clientOpts := &clients.Options{}
	clientOpts.Contacts.IP = gatewayConfig.Clients.Contacts.IP
	clientOpts.Contacts.Port = gatewayConfig.Clients.Contacts.Port
	clientOpts.GoogleNow.IP = gatewayConfig.Clients.GoogleNow.IP
	clientOpts.GoogleNow.Port = gatewayConfig.Clients.GoogleNow.Port
	clients := clients.CreateClients(clientOpts)

	m3FlushIntervalConfig := gatewayConfig.Metrics.M3.FlushInterval
	var m3FlushInterval time.Duration
	if m3FlushIntervalConfig == 0 {
		m3FlushInterval = defaultM3FlushInterval
	} else {
		m3FlushInterval = m3FlushIntervalConfig
	}

	commonTags := map[string]string{"env": "example"}
	m3Backend, err := metrics.NewM3Backend(
		gatewayConfig.Metrics.M3.HostPort,
		gatewayConfig.Metrics.Tally.Service,
		commonTags, // default tags
		false,      // include host
		defaultM3MaxQueueSize,
		defaultM3MaxPacketSize,
		m3FlushInterval,
	)
	if err != nil {
		tempLogger.Error("Error initializing m3backend",
			zap.String("error", err.Error()),
		)
		os.Exit(1)
	}

	server, err := zanzibar.CreateGateway(&zanzibar.Options{
		IP:   gatewayConfig.IP,
		Port: gatewayConfig.Port,
		Logger: zanzibar.LoggerOptions{
			FileName: gatewayConfig.Logger.FileName,
		},
		Metrics: zanzibar.MetricsOptions{
			FlushInterval: gatewayConfig.Metrics.Tally.FlushInterval,
			Service:       gatewayConfig.Metrics.Tally.Service,
		},
		TChannel: zanzibar.TChannelOptions{
			ServiceName: gatewayConfig.TChannel.ServiceName,
			ProcessName: gatewayConfig.TChannel.ProcessName,
		},

		Clients:        clients,
		MetricsBackend: m3Backend,
	})
	if err != nil {
		os.Exit(1)
	}

	err = server.Bootstrap(endpoints.Register)
	if err != nil {
		os.Exit(1)
	}

	server.Logger.Info("Started EdgeGateway",
		zap.String("realAddr", server.RealAddr),
		zap.Object("config", gatewayConfig),
	)

	// TODO: handle sigterm gracefully

	server.Wait()

	// log.Configure(&cfg.Logging, cfg.Verbose)

	// metrics, err := cfg.Metrics.New()
	// if err != nil {
	// 	log.Fatalf("Could not connect to metrics: %v", err)
	// }
	// metrics.Counter("boot").Inc(1)

	// closer, err := xjaeger.InitGlobalTracer(cfg.Jaeger, cfg.ServiceName, metrics)
	// if err != nil {
	// 	log.Fatalf("Jaeger.InitGlobalTracer failed: %v", err)
	// }
	// defer closer.Close()
}
<commit_msg>examples/main.go: add panic for more information<commit_after>package main

import (
	"io/ioutil"
	"os"
	"path"
	"time"

	metrics "github.com/uber-go/tally/m3"
	zap "github.com/uber-go/zap"
	zanzibar "github.com/uber/zanzibar/runtime"
	yaml "gopkg.in/yaml.v2"

	"github.com/uber/zanzibar/examples/example-gateway/clients"
	exampleConfig "github.com/uber/zanzibar/examples/example-gateway/config"
	endpoints "github.com/uber/zanzibar/examples/example-gateway/endpoints"
)

const defaultM3MaxQueueSize = 10000
const defaultM3MaxPacketSize = 1440 // 1440kb in UDP M3MaxPacketSize
const defaultM3FlushInterval = 500 * time.Millisecond

func loadConfig(config *exampleConfig.Config) error {
	configRoot := os.Getenv("UBER_CONFIG_DIR")
	filePath := path.Join(configRoot, "production.yaml")

	data, err := ioutil.ReadFile(filePath)
	if err != nil {
		return err
	}

	if err := yaml.Unmarshal(data, config); err != nil {
		return err
	}

	return nil
}

func main() {
	gatewayConfig := &exampleConfig.Config{}
	tempLogger := zap.New(
		zap.NewJSONEncoder(),
		zap.Output(os.Stderr),
	)

	if err := loadConfig(gatewayConfig); err != nil {
		tempLogger.Error("Error initializing configuration",
			zap.String("error", err.Error()),
		)
		os.Exit(1)
	}

	clientOpts := &clients.Options{}
	clientOpts.Contacts.IP = gatewayConfig.Clients.Contacts.IP
	clientOpts.Contacts.Port = gatewayConfig.Clients.Contacts.Port
	clientOpts.GoogleNow.IP = gatewayConfig.Clients.GoogleNow.IP
	clientOpts.GoogleNow.Port = gatewayConfig.Clients.GoogleNow.Port
	clients := clients.CreateClients(clientOpts)

	m3FlushIntervalConfig := gatewayConfig.Metrics.M3.FlushInterval
	var m3FlushInterval time.Duration
	if m3FlushIntervalConfig == 0 {
		m3FlushInterval = defaultM3FlushInterval
	} else {
		m3FlushInterval = m3FlushIntervalConfig
	}

	commonTags := map[string]string{"env": "example"}
	m3Backend, err := metrics.NewM3Backend(
		gatewayConfig.Metrics.M3.HostPort,
		gatewayConfig.Metrics.Tally.Service,
		commonTags, // default tags
		false,      // include host
		defaultM3MaxQueueSize,
		defaultM3MaxPacketSize,
		m3FlushInterval,
	)
	if err != nil {
		tempLogger.Error("Error initializing m3backend",
			zap.String("error", err.Error()),
		)
		os.Exit(1)
	}

	server, err := zanzibar.CreateGateway(&zanzibar.Options{
		IP:   gatewayConfig.IP,
		Port: gatewayConfig.Port,
		Logger: zanzibar.LoggerOptions{
			FileName: gatewayConfig.Logger.FileName,
		},
		Metrics: zanzibar.MetricsOptions{
			FlushInterval: gatewayConfig.Metrics.Tally.FlushInterval,
			Service:       gatewayConfig.Metrics.Tally.Service,
		},
		TChannel: zanzibar.TChannelOptions{
			ServiceName: gatewayConfig.TChannel.ServiceName,
			ProcessName: gatewayConfig.TChannel.ProcessName,
		},

		Clients:        clients,
		MetricsBackend: m3Backend,
	})
	if err != nil {
		panic(err)
	}

	err = server.Bootstrap(endpoints.Register)
	if err != nil {
		panic(err)
	}

	server.Logger.Info("Started EdgeGateway",
		zap.String("realAddr", server.RealAddr),
		zap.Object("config", gatewayConfig),
	)

	// TODO: handle sigterm gracefully

	server.Wait()

	// log.Configure(&cfg.Logging, cfg.Verbose)

	// metrics, err := cfg.Metrics.New()
	// if err != nil {
	// 	log.Fatalf("Could not connect to metrics: %v", err)
	// }
	// metrics.Counter("boot").Inc(1)

	// closer, err := xjaeger.InitGlobalTracer(cfg.Jaeger, cfg.ServiceName, metrics)
	// if err != nil {
	// 	log.Fatalf("Jaeger.InitGlobalTracer failed: %v", err)
	// }
	// defer closer.Close()
}
<|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 milestone

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubeclient "knative.dev/pkg/client/injection/kube/client"

	"knative.dev/reconciler-test/pkg/feature"
)

type logEmitter struct {
	ctx       context.Context
	namespace string
	t         feature.T
}

// NewLogEmitter creates an Emitter that logs milestone events.
func NewLogEmitter(ctx context.Context, namespace string, t feature.T) Emitter {
	return &logEmitter{ctx: ctx, namespace: namespace, t: t}
}

func (l logEmitter) Environment(env map[string]string) {
	bytes, err := json.MarshalIndent(env, " ", " ")
	if err != nil {
		l.log(err)
		return
	}
	l.log("Environment", "Namespace", l.namespace, "\n", string(bytes))
}

func (l logEmitter) NamespaceCreated(namespace string) {
	l.log("Namespace created", namespace)
}

func (l logEmitter) NamespaceDeleted(namespace string) {
	l.log("Namespace deleted", namespace)
}

func (l logEmitter) TestStarted(feature string, t feature.T) {
	l.log(t, feature, "Test started")
}

func (l logEmitter) TestFinished(feature string, t feature.T) {
	l.log(t, feature, "Test Finished")
}

func (l logEmitter) StepsPlanned(feature string, steps map[feature.Timing][]feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(steps, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Steps Planned", "\n", string(bytes))
}

func (l logEmitter) StepStarted(feature string, step *feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(step, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Step Started", "\n", string(bytes))
}

func (l logEmitter) StepFinished(feature string, step *feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(step, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Step Finished", "\n", string(bytes))
}

func (l logEmitter) TestSetStarted(featureSet string, t feature.T) {
	l.log(t, featureSet, "FeatureSet Started")
}

func (l logEmitter) TestSetFinished(featureSet string, t feature.T) {
	l.log(t, featureSet, "FeatureSet Finished")
}

func (l logEmitter) Finished() {
	l.log("Finished")
	l.dumpEvents()
}

func (l logEmitter) Exception(reason, messageFormat string, messageA ...interface{}) {
	l.log("Exception", reason, fmt.Sprintf(messageFormat, messageA...))
}

func (l logEmitter) dumpEvents() {
	if l.t == nil {
		return
	}
	events, err := kubeclient.Get(l.ctx).CoreV1().Events(l.namespace).List(l.ctx, metav1.ListOptions{})
	if err != nil {
		l.log("failed to list events", err)
		return
	}
	if len(events.Items) == 0 {
		l.log("No events found")
		return
	}
	for _, e := range sortEventsByTime(events.Items) {
		l.log(formatEvent(e))
	}
}

func sortEventsByTime(items []corev1.Event) []corev1.Event {
	sort.SliceStable(items, func(i, j int) bool {
		return eventTime(items[i]).Before(eventTime(items[j]))
	})
	return items
}

func eventTime(e corev1.Event) time.Time {
	// Some events might not contain last timestamp, in that case
	// we fall back to the event time.
	if e.LastTimestamp.Time.IsZero() {
		return e.EventTime.Time
	}
	return e.LastTimestamp.Time
}

func formatEvent(e corev1.Event) string {
	return strings.Join([]string{`Event{`,
		`ObjectMeta:` + strings.Replace(e.ObjectMeta.String(), `&`, ``, 1),
		`InvolvedObject:` + strings.Replace(e.InvolvedObject.String(), `&`, ``, 1),
		`Reason:` + e.Reason,
		`Message:` + e.Message,
		`Source:` + strings.Replace(e.Source.String(), `&`, ``, 1),
		`FirstTimestamp:` + e.FirstTimestamp.String(),
		`LastTimestamp:` + e.LastTimestamp.String(),
		`Count:` + fmt.Sprintf("%d", e.Count),
		`Type:` + e.Type,
		`EventTime:` + e.EventTime.String(),
		`Series:` + e.Series.String(),
		`Action:` + e.Action,
		`Related:` + e.Related.String(),
		`ReportingController:` + e.ReportingController,
		`ReportingInstance:` + e.ReportingInstance,
		`}`,
	}, "\n")
}

func (l logEmitter) log(args ...interface{}) {
	ctxArgs := []interface{}{
		"namespace", l.namespace,
		"timestamp", time.Now().Format(time.RFC3339),
		"\n",
	}
	args = append(ctxArgs, args...)
	if l.t != nil {
		l.t.Log(args...)
	}
}
<commit_msg>Remove log emitter noise (#282)<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 milestone

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubeclient "knative.dev/pkg/client/injection/kube/client"

	"knative.dev/reconciler-test/pkg/feature"
)

type logEmitter struct {
	ctx       context.Context
	namespace string
	t         feature.T
}

// NewLogEmitter creates an Emitter that logs milestone events.
func NewLogEmitter(ctx context.Context, namespace string, t feature.T) Emitter {
	return &logEmitter{ctx: ctx, namespace: namespace, t: t}
}

func (l logEmitter) Environment(env map[string]string) {
	bytes, err := json.MarshalIndent(env, " ", " ")
	if err != nil {
		l.log(nil, err)
		return
	}
	l.log(nil, "Environment", "Namespace", l.namespace, "\n", string(bytes))
}

func (l logEmitter) NamespaceCreated(namespace string) {
	l.log(nil, "Namespace created", namespace)
}

func (l logEmitter) NamespaceDeleted(namespace string) {
	l.log(nil, "Namespace deleted", namespace)
}

func (l logEmitter) TestStarted(feature string, t feature.T) {
	l.log(t, feature, "Test started")
}

func (l logEmitter) TestFinished(feature string, t feature.T) {
	l.log(t, feature, "Test Finished")
}

func (l logEmitter) StepsPlanned(feature string, steps map[feature.Timing][]feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(steps, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Steps Planned", "\n", string(bytes))
}

func (l logEmitter) StepStarted(feature string, step *feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(step, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Step Started", "\n", string(bytes))
}

func (l logEmitter) StepFinished(feature string, step *feature.Step, t feature.T) {
	bytes, err := json.MarshalIndent(step, " ", " ")
	if err != nil {
		l.log(t, err)
		return
	}
	l.log(t, feature, "Step Finished", "\n", string(bytes))
}

func (l logEmitter) TestSetStarted(featureSet string, t feature.T) {
	l.log(t, featureSet, "FeatureSet Started")
}

func (l logEmitter) TestSetFinished(featureSet string, t feature.T) {
	l.log(t, featureSet, "FeatureSet Finished")
}

func (l logEmitter) Finished() {
	l.log(nil, "Finished")
	l.dumpEvents()
}

func (l logEmitter) Exception(reason, messageFormat string, messageA ...interface{}) {
	l.log(nil, "Exception", reason, fmt.Sprintf(messageFormat, messageA...))
}

func (l logEmitter) dumpEvents() {
	if l.t == nil {
		return
	}
	events, err := kubeclient.Get(l.ctx).CoreV1().Events(l.namespace).List(l.ctx, metav1.ListOptions{})
	if err != nil {
		l.log(nil, "failed to list events", err)
		return
	}
	if len(events.Items) == 0 {
		l.log(nil, "No events found")
		return
	}
	for _, e := range sortEventsByTime(events.Items) {
		l.log(nil, formatEvent(e))
	}
}

func sortEventsByTime(items []corev1.Event) []corev1.Event {
	sort.SliceStable(items, func(i, j int) bool {
		return eventTime(items[i]).Before(eventTime(items[j]))
	})
	return items
}

func eventTime(e corev1.Event) time.Time {
	// Some events might not contain last timestamp, in that case
	// we fall back to the event time.
	if e.LastTimestamp.Time.IsZero() {
		return e.EventTime.Time
	}
	return e.LastTimestamp.Time
}

func formatEvent(e corev1.Event) string {
	return strings.Join([]string{`Event{`,
		`ObjectMeta:` + strings.Replace(e.ObjectMeta.String(), `&`, ``, 1),
		`InvolvedObject:` + strings.Replace(e.InvolvedObject.String(), `&`, ``, 1),
		`Reason:` + e.Reason,
		`Message:` + e.Message,
		`Source:` + strings.Replace(e.Source.String(), `&`, ``, 1),
		`FirstTimestamp:` + e.FirstTimestamp.String(),
		`LastTimestamp:` + e.LastTimestamp.String(),
		`Count:` + fmt.Sprintf("%d", e.Count),
		`Type:` + e.Type,
		`EventTime:` + e.EventTime.String(),
		`Series:` + e.Series.String(),
		`Action:` + e.Action,
		`Related:` + e.Related.String(),
		`ReportingController:` + e.ReportingController,
		`ReportingInstance:` + e.ReportingInstance,
		`}`,
	}, "\n")
}

func (l logEmitter) log(t feature.T, args ...interface{}) {
	ctxArgs := []interface{}{
		"namespace", l.namespace,
		"timestamp", time.Now().Format(time.RFC3339),
		"\n",
	}
	if t == nil {
		t = l.t
	}
	args = append(ctxArgs, args...)
	if l.t != nil {
		l.t.Log(args...)
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2017 caicloud authors. All rights reserved.

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

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

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

package router

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	restful "github.com/emicklei/go-restful"
	log "github.com/golang/glog"
	"github.com/google/go-github/github"
	"github.com/xanzy/go-gitlab"

	"github.com/caicloud/cyclone/pkg/api"
	gitlabuitl "github.com/caicloud/cyclone/pkg/util/gitlab"
	httputil "github.com/caicloud/cyclone/pkg/util/http"
)

const (
	// branchRefTemplate represents reference template for branches.
	branchRefTemplate = "refs/heads/%s"

	// tagRefTemplate represents reference template for tags.
	tagRefTemplate = "refs/tags/%s"

	// githubPullRefTemplate represents reference template for Github pull request.
	githubPullRefTemplate = "refs/pull/%d/merge"

	// gitlabMergeRefTemplate represents reference template for Gitlab merge request and merge target branch
	gitlabMergeRefTemplate = "refs/merge-requests/%d/head:%s"

	// gitlabEventTypeHeader represents the Gitlab header key used to pass the event type.
	gitlabEventTypeHeader = "X-Gitlab-Event"
)

// handleGithubWebhook handles the webhook request from Github.
// 1. Parse the pipeline id from request path;
// 2. Get the pipeline by id;
// 3. Parse the payload from request header;
// 4. Parse the payload from request body;
// 5. First get the event type, and handle it according to its type.
func (router *router) handleGithubWebhook(request *restful.Request, response *restful.Response) {
	pipelineID := request.PathParameter(pipelineIDPathParameterName)
	pipeline, err := router.pipelineManager.GetPipelineByID(pipelineID)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}

	if pipeline.AutoTrigger == nil || pipeline.AutoTrigger.SCMTrigger == nil {
		httputil.ResponseWithError(response, fmt.Errorf("SCM auto trigger is not set"))
		return
	}
	scmTrigger := pipeline.AutoTrigger.SCMTrigger

	// TODO (robin) Validate the payload.
	// Ref: https://github.com/google/go-github/blob/df47db1628185875602e66d3356ae7337b52bba3/github/messages.go#L213-L233
	// github.ValidatePayload(request.Request, secretKey)
	payload, err := ioutil.ReadAll(request.Request.Body)
	if err != nil {
		httputil.ResponseWithError(response, fmt.Errorf("Fail to read the request body"))
		return
	}
	event, err := github.ParseWebHook(github.WebHookType(request.Request), payload)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}
	log.Infof("Github webhook event: %v", event)

	// Handle the event.
	var performParams *api.PipelinePerformParams
	switch event := event.(type) {
	case *github.ReleaseEvent:
		if scmTrigger.TagRelease == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Release trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Name:        *event.Release.TagName,
			Ref:         fmt.Sprintf(tagRefTemplate, *event.Release.TagName),
			Description: "Triggered by tag release",
			Stages:      scmTrigger.TagRelease.Stages,
		}

		log.Info("Triggered by Github release event")
	case *github.PullRequestEvent:
		// Only handle when the pull request are created.
		if *event.Action != "opened" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when pull request is created")
			return
		}

		if scmTrigger.PullRequest == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         fmt.Sprintf(githubPullRefTemplate, *event.PullRequest.Number),
			Description: "Triggered by pull request",
			Stages:      scmTrigger.PullRequest.Stages,
		}

		log.Info("Triggered by Github pull request event")
	case *github.IssueCommentEvent:
		if event.Issue.PullRequestLinks == nil {
			log.Infof("Only handle when issues type is pull request")
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when issues type is pull request")
			return
		}

		// Only handle when the pull request comments are created.
		if *event.Action != "created" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when pull request comment is created")
			return
		}

		if scmTrigger.PullRequestComment == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request comment trigger is not enabled")
			return
		}

		comment := event.Comment
		trigger := false
		if comment != nil {
			for _, c := range scmTrigger.PullRequestComment.Comments {
				if *comment.Body == c {
					trigger = true
					break
				}
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         fmt.Sprintf(githubPullRefTemplate, *event.Issue.Number),
				Description: "Triggered by pull request comments",
				Stages:      scmTrigger.PullRequestComment.Stages,
			}
			log.Info("Triggered by Github pull request review comment event")
		}
	case *github.PushEvent:
		if scmTrigger.Push == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Push trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         fmt.Sprintf(*event.Ref),
			Description: "Triggered by push",
			Stages:      scmTrigger.Push.Stages,
		}

		log.Info("Triggered by Github push event")
	default:
		log.Errorf("event type not support.")

	}

	if performParams != nil {
		pipelineRecord := &api.PipelineRecord{
			Name:          performParams.Name,
			PipelineID:    pipeline.ID,
			PerformParams: performParams,
			Trigger:       api.TriggerSCM,
		}
		if _, err = router.pipelineRecordManager.CreatePipelineRecord(pipelineRecord); err != nil {
			httputil.ResponseWithError(response, err)
			return
		}

		response.WriteHeaderAndEntity(http.StatusOK, "Successfully triggered")
	} else {
		response.WriteHeaderAndEntity(http.StatusOK, "Is ignored")
	}
}

// handleGitlabWebhook handles the webhook request from Gitlab.
// 1. Parse the pipeline id from request path;
// 2. Get the pipeline by id;
// 3. Parse the payload from request header;
// 4. Parse the payload from request body;
// 5. First get the event type, and handle it according to its type.
func (router *router) handleGitlabWebhook(request *restful.Request, response *restful.Response) {
	pipelineID := request.PathParameter(pipelineIDPathParameterName)
	pipeline, err := router.pipelineManager.GetPipelineByID(pipelineID)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}

	if pipeline.AutoTrigger == nil || pipeline.AutoTrigger.SCMTrigger == nil {
		httputil.ResponseWithError(response, fmt.Errorf("SCM auto trigger is not set"))
		return
	}
	scmTrigger := pipeline.AutoTrigger.SCMTrigger

	// TODO (robin) Validate the payload.
	// Ref: https://github.com/google/go-github/blob/df47db1628185875602e66d3356ae7337b52bba3/github/messages.go#L213-L233
	// github.ValidatePayload(request.Request, secretKey)
	event, err := gitlabuitl.ParseWebHook(request.Request)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}
	log.Infof("Gitlab webhook event: %v", event)

	// Handle the event.
	var performParams *api.PipelinePerformParams
	switch event := event.(type) {
	case *gitlab.TagEvent:
		if scmTrigger.TagRelease == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Release trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Name:        strings.Split(event.Ref, "/")[2],
			Ref:         event.Ref,
			Description: "Triggered by tag release",
			Stages:      scmTrigger.TagRelease.Stages,
		}

		log.Info("Triggered by Gitlab tag event")
	case *gitlab.MergeEvent:
		// Only handle when the pull request are created.
		objectAttributes := event.ObjectAttributes
		if objectAttributes.Action != "open" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when merge request is created")
			return
		}

		if scmTrigger.PullRequest == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         fmt.Sprintf(gitlabMergeRefTemplate, objectAttributes.Iid, objectAttributes.TargetBranch),
			Description: objectAttributes.Title,
			Stages:      scmTrigger.PullRequest.Stages,
		}

		log.Info("Triggered by Gitlab merge event")
	case *gitlabuitl.MergeCommentEvent:
		if event.MergeRequest == nil {
			log.Infof("Only handle comments on merge request")
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle comments on merge request")
			return
		}

		if scmTrigger.PullRequestComment == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request comment trigger is not enabled")
			return
		}
		objectAttributes := event.ObjectAttributes
		trigger := false
		if objectAttributes.Note != "" {
			for _, c := range scmTrigger.PullRequestComment.Comments {
				if objectAttributes.Note == c {
					trigger = true
					break
				}
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         fmt.Sprintf(gitlabMergeRefTemplate, event.MergeRequest.IID, event.MergeRequest.TargetBranch),
				Description: "Triggered by pull request comments",
				Stages:      scmTrigger.PullRequestComment.Stages,
			}
			log.Info("Triggered by Gitlab merge comment event")
		}
	case *gitlab.PushEvent:
		if scmTrigger.Push == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Push trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         event.Ref,
			Description: "Triggered by push",
			Stages:      scmTrigger.Push.Stages,
		}

		log.Info("Triggered by Gitlab push event")
	default:
		log.Errorf("event type not support.")
	}

	if performParams != nil {
		pipelineRecord := &api.PipelineRecord{
			Name:          performParams.Name,
			PipelineID:    pipeline.ID,
			PerformParams: performParams,
			Trigger:       api.TriggerSCM,
		}
		if _, err = router.pipelineRecordManager.CreatePipelineRecord(pipelineRecord); err != nil {
			httputil.ResponseWithError(response, err)
			return
		}

		response.WriteHeaderAndEntity(http.StatusOK, "Successfully triggered")
	} else {
		response.WriteHeaderAndEntity(http.StatusOK, "Is ignored")
	}
}
<commit_msg>feat:push event webhook support configuring target branches (#533)<commit_after>/*
Copyright 2017 caicloud authors. All rights reserved.

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

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

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

package router

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	restful "github.com/emicklei/go-restful"
	log "github.com/golang/glog"
	"github.com/google/go-github/github"
	"github.com/xanzy/go-gitlab"

	"github.com/caicloud/cyclone/pkg/api"
	gitlabuitl "github.com/caicloud/cyclone/pkg/util/gitlab"
	httputil "github.com/caicloud/cyclone/pkg/util/http"
)

const (
	// branchRefTemplate represents reference template for branches.
	branchRefTemplate = "refs/heads/%s"

	// tagRefTemplate represents reference template for tags.
	tagRefTemplate = "refs/tags/%s"

	// githubPullRefTemplate represents reference template for Github pull request.
	githubPullRefTemplate = "refs/pull/%d/merge"

	// gitlabMergeRefTemplate represents reference template for Gitlab merge request and merge target branch
	gitlabMergeRefTemplate = "refs/merge-requests/%d/head:%s"

	// gitlabEventTypeHeader represents the Gitlab header key used to pass the event type.
	gitlabEventTypeHeader = "X-Gitlab-Event"
)

// handleGithubWebhook handles the webhook request from Github.
// 1. Parse the pipeline id from request path;
// 2. Get the pipeline by id;
// 3. Parse the payload from request header;
// 4. Parse the payload from request body;
// 5. First get the event type, and handle it according to its type.
func (router *router) handleGithubWebhook(request *restful.Request, response *restful.Response) {
	pipelineID := request.PathParameter(pipelineIDPathParameterName)
	pipeline, err := router.pipelineManager.GetPipelineByID(pipelineID)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}

	if pipeline.AutoTrigger == nil || pipeline.AutoTrigger.SCMTrigger == nil {
		httputil.ResponseWithError(response, fmt.Errorf("SCM auto trigger is not set"))
		return
	}
	scmTrigger := pipeline.AutoTrigger.SCMTrigger

	// TODO (robin) Validate the payload.
	// Ref: https://github.com/google/go-github/blob/df47db1628185875602e66d3356ae7337b52bba3/github/messages.go#L213-L233
	// github.ValidatePayload(request.Request, secretKey)
	payload, err := ioutil.ReadAll(request.Request.Body)
	if err != nil {
		httputil.ResponseWithError(response, fmt.Errorf("Fail to read the request body"))
		return
	}
	event, err := github.ParseWebHook(github.WebHookType(request.Request), payload)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}
	log.Infof("Github webhook event: %v", event)

	// Handle the event.
	var performParams *api.PipelinePerformParams
	switch event := event.(type) {
	case *github.ReleaseEvent:
		if scmTrigger.TagRelease == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Release trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Name:        *event.Release.TagName,
			Ref:         fmt.Sprintf(tagRefTemplate, *event.Release.TagName),
			Description: "Triggered by tag release",
			Stages:      scmTrigger.TagRelease.Stages,
		}

		log.Info("Triggered by Github release event")
	case *github.PullRequestEvent:
		// Only handle when the pull request are created.
		if *event.Action != "opened" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when pull request is created")
			return
		}

		if scmTrigger.PullRequest == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         fmt.Sprintf(githubPullRefTemplate, *event.PullRequest.Number),
			Description: "Triggered by pull request",
			Stages:      scmTrigger.PullRequest.Stages,
		}

		log.Info("Triggered by Github pull request event")
	case *github.IssueCommentEvent:
		if event.Issue.PullRequestLinks == nil {
			log.Infof("Only handle when issues type is pull request")
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when issues type is pull request")
			return
		}

		// Only handle when the pull request comments are created.
		if *event.Action != "created" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when pull request comment is created")
			return
		}

		if scmTrigger.PullRequestComment == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request comment trigger is not enabled")
			return
		}

		comment := event.Comment
		trigger := false
		if comment != nil {
			for _, c := range scmTrigger.PullRequestComment.Comments {
				if *comment.Body == c {
					trigger = true
					break
				}
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         fmt.Sprintf(githubPullRefTemplate, *event.Issue.Number),
				Description: "Triggered by pull request comments",
				Stages:      scmTrigger.PullRequestComment.Stages,
			}
			log.Info("Triggered by Github pull request review comment event")
		}
	case *github.PushEvent:
		if scmTrigger.Push == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Push trigger is not enabled")
			return
		}

		ref := *event.Ref
		trigger := false
		for _, b := range scmTrigger.Push.Branches {
			if strings.HasSuffix(ref, b) {
				trigger = true
				break
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         ref,
				Description: "Triggered by push",
				Stages:      scmTrigger.Push.Stages,
			}

			log.Info("Triggered by Github push event")
		}
	default:
		log.Errorf("event type not support.")

	}

	if performParams != nil {
		pipelineRecord := &api.PipelineRecord{
			Name:          performParams.Name,
			PipelineID:    pipeline.ID,
			PerformParams: performParams,
			Trigger:       api.TriggerSCM,
		}
		if _, err = router.pipelineRecordManager.CreatePipelineRecord(pipelineRecord); err != nil {
			httputil.ResponseWithError(response, err)
			return
		}

		response.WriteHeaderAndEntity(http.StatusOK, "Successfully triggered")
	} else {
		response.WriteHeaderAndEntity(http.StatusOK, "Is ignored")
	}
}

// handleGitlabWebhook handles the webhook request from Gitlab.
// 1. Parse the pipeline id from request path;
// 2. Get the pipeline by id;
// 3. Parse the payload from request header;
// 4. Parse the payload from request body;
// 5. First get the event type, and handle it according to its type.
func (router *router) handleGitlabWebhook(request *restful.Request, response *restful.Response) {
	pipelineID := request.PathParameter(pipelineIDPathParameterName)
	pipeline, err := router.pipelineManager.GetPipelineByID(pipelineID)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}

	if pipeline.AutoTrigger == nil || pipeline.AutoTrigger.SCMTrigger == nil {
		httputil.ResponseWithError(response, fmt.Errorf("SCM auto trigger is not set"))
		return
	}
	scmTrigger := pipeline.AutoTrigger.SCMTrigger

	// TODO (robin) Validate the payload.
	// Ref: https://github.com/google/go-github/blob/df47db1628185875602e66d3356ae7337b52bba3/github/messages.go#L213-L233
	// github.ValidatePayload(request.Request, secretKey)
	event, err := gitlabuitl.ParseWebHook(request.Request)
	if err != nil {
		httputil.ResponseWithError(response, err)
		return
	}
	log.Infof("Gitlab webhook event: %v", event)

	// Handle the event.
	var performParams *api.PipelinePerformParams
	switch event := event.(type) {
	case *gitlab.TagEvent:
		if scmTrigger.TagRelease == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Release trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Name:        strings.Split(event.Ref, "/")[2],
			Ref:         event.Ref,
			Description: "Triggered by tag release",
			Stages:      scmTrigger.TagRelease.Stages,
		}

		log.Info("Triggered by Gitlab tag event")
	case *gitlab.MergeEvent:
		// Only handle when the pull request are created.
		objectAttributes := event.ObjectAttributes
		if objectAttributes.Action != "open" {
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle when merge request is created")
			return
		}

		if scmTrigger.PullRequest == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request trigger is not enabled")
			return
		}

		performParams = &api.PipelinePerformParams{
			Ref:         fmt.Sprintf(gitlabMergeRefTemplate, objectAttributes.Iid, objectAttributes.TargetBranch),
			Description: objectAttributes.Title,
			Stages:      scmTrigger.PullRequest.Stages,
		}

		log.Info("Triggered by Gitlab merge event")
	case *gitlabuitl.MergeCommentEvent:
		if event.MergeRequest == nil {
			log.Infof("Only handle comments on merge request")
			response.WriteHeaderAndEntity(http.StatusOK, "Only handle comments on merge request")
			return
		}

		if scmTrigger.PullRequestComment == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Pull request comment trigger is not enabled")
			return
		}
		objectAttributes := event.ObjectAttributes
		trigger := false
		if objectAttributes.Note != "" {
			for _, c := range scmTrigger.PullRequestComment.Comments {
				if objectAttributes.Note == c {
					trigger = true
					break
				}
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         fmt.Sprintf(gitlabMergeRefTemplate, event.MergeRequest.IID, event.MergeRequest.TargetBranch),
				Description: "Triggered by pull request comments",
				Stages:      scmTrigger.PullRequestComment.Stages,
			}
			log.Info("Triggered by Gitlab merge comment event")
		}
	case *gitlab.PushEvent:
		if scmTrigger.Push == nil {
			response.WriteHeaderAndEntity(http.StatusOK, "Push trigger is not enabled")
			return
		}

		ref := event.Ref
		trigger := false
		for _, b := range scmTrigger.Push.Branches {
			if strings.HasSuffix(ref, b) {
				trigger = true
				break
			}
		}

		if trigger {
			performParams = &api.PipelinePerformParams{
				Ref:         ref,
				Description: "Triggered by push",
				Stages:      scmTrigger.Push.Stages,
			}

			log.Info("Triggered by Gitlab push event")
		}
	default:
		log.Errorf("event type not support.")
	}

	if performParams != nil {
		pipelineRecord := &api.PipelineRecord{
			Name:          performParams.Name,
			PipelineID:    pipeline.ID,
			PerformParams: performParams,
			Trigger:       api.TriggerSCM,
		}
		if _, err = router.pipelineRecordManager.CreatePipelineRecord(pipelineRecord); err != nil {
			httputil.ResponseWithError(response, err)
			return
		}

		response.WriteHeaderAndEntity(http.StatusOK, "Successfully triggered")
	} else {
		response.WriteHeaderAndEntity(http.StatusOK, "Is ignored")
	}
}
<|endoftext|>