<commit_before>package stick

import (
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/256dpi/xo"
	"github.com/asaskevich/govalidator"
)

// Validatable represents a type that can be validated.
type Validatable interface {
	Validate() error
}

// NoValidation can be embedded in a struct to provide a no-op validation method.
type NoValidation struct{}

// Validate will perform no validation.
func (*NoValidation) Validate() error {
	return nil
}

// ValidationError describes a validation error.
type ValidationError struct {
	Reports []ValidationReport
}

// ValidationReport is a single validation error.
type ValidationReport struct {
	Path  []string
	Error error
}

// Error implements the error interface.
func (e ValidationError) Error() string {
	// prepare errors
	var errs []string
	for _, report := range e.Reports {
		errs = append(errs, fmt.Sprintf("%s: %s", strings.Join(report.Path, "."), report.Error.Error()))
	}

	return strings.Join(errs, "; ")
}

// Validator is used to validate an object.
type Validator struct {
	obj     Accessible
	path    []string
	reports []ValidationReport
}

// Validate will validate the provided accessible using the specified validator
// function.
func Validate(obj Accessible, fn func(v *Validator)) error {
	// prepare validator
	val := &Validator{obj: obj}

	// run validator
	fn(val)

	return val.Error()
}

// Nest nest validation under the specified field.
func (v *Validator) Nest(field string, fn func()) {
	// push
	v.path = append(v.path, field)

	// yield
	fn()

	// pop
	v.path = v.path[:len(v.path)-1]
}

// Value will validate the value at the named field using the provided rules.
// If the value is optional it will be skipped if nil or unwrapped if present.
func (v *Validator) Value(name string, optional bool, rules ...Rule) {
	// get value
	value := MustGet(v.obj, name)

	// prepare context
	ctx := RuleContext{
		IValue: value,
		RValue: reflect.ValueOf(value),
	}

	// handle optionals
	if optional {
		// skip if nil
		if ctx.IsNil() {
			return
		}

		// otherwise unwrap pointer once
		ctx.RValue = ctx.RValue.Elem()
		ctx.IValue = ctx.RValue.Interface()
	}

	// execute rules
	for _, rule := range rules {
		err := rule(ctx)
		if err != nil {
			v.Report(name, err)
		}
	}
}

// Items will validate each item of the slice at the named field using the
// provided rules.
func (v *Validator) Items(name string, rules ...Rule) {
	// get slice
	slice := reflect.ValueOf(MustGet(v.obj, name))

	// execute rules for each item
	v.Nest(name, func() {
		for i := 0; i < slice.Len(); i++ {
			// get item
			item := slice.Index(i)

			// prepare context
			ctx := RuleContext{
				IValue: item.Interface(),
				RValue: item,
			}

			// execute rules
			for _, rule := range rules {
				err := rule(ctx)
				if err != nil {
					v.Report(strconv.Itoa(i), err)
				}
			}
		}
	})
}

// Report will report a validation error.
func (v *Validator) Report(name string, err error) {
	// copy path
	path := append([]string{}, v.path...)
	path = append(path, name)

	// add report
	v.reports = append(v.reports, ValidationReport{
		Path:  path,
		Error: err,
	})
}

// Error will return the accumulated error or nil of no errors have yet been
// reported.
func (v *Validator) Error() error {
	// check errors
	if len(v.reports) > 0 {
		return ValidationError{Reports: v.reports}
	}

	return nil
}

// RuleContext carries the to be checked value.
type RuleContext struct {
	IValue interface{}
	RValue reflect.Value
}

// IsNil returns whether the value is nil.
func (c *RuleContext) IsNil() bool {
	// check plain nil
	if c.IValue == nil {
		return true
	}

	// check typed nils
	switch c.RValue.Kind() {
	case reflect.Ptr, reflect.Slice, reflect.Map:
		return c.RValue.IsNil()
	}

	return false
}

// Unwrap will unwrap all pointers.
func (c *RuleContext) Unwrap() bool {
	// unwrap pointers
	var unwrapped bool
	for c.RValue.Kind() == reflect.Ptr {
		c.RValue = c.RValue.Elem()
		unwrapped = true
	}
	if unwrapped {
		c.IValue = c.RValue.Interface()
	}

	return unwrapped
}

// Guard will run the provided function if a concrete value can be unwrapped.
func (c *RuleContext) Guard(fn func() error) error {
	// check nil
	if c.IsNil() {
		return nil
	}

	// unwrap
	c.Unwrap()

	return fn()
}

// Rule is a single validation rule.
type Rule func(ctx RuleContext) error

// IsOK will run the provided validation function.
func IsOK(fn func() error) Rule {
	return func(RuleContext) error {
		return fn()
	}
}

func isZero(ctx RuleContext) bool {
	// check nil
	if ctx.IsNil() {
		return true
	}

	// check using IsZero method
	type isZero interface {
		IsZero() bool
	}
	if v, ok := ctx.IValue.(isZero); ok {
		// check zeroness
		if !v.IsZero() {
			return false
		}

		return true
	}

	// check using Zero method
	type zero interface {
		Zero() bool
	}
	if v, ok := ctx.IValue.(zero); ok {
		// check zeroness
		if !v.Zero() {
			return false
		}

		return true
	}

	// unwrap pointer
	ctx.Unwrap()

	// check nil again
	if ctx.IsNil() {
		return true
	}

	// check zeroness
	if !ctx.RValue.IsZero() {
		return false
	}

	return true
}

// IsZero will check if the provided value is zero. It will determine zeroness
// using IsZero() or Zero() if implemented and default back to reflect. A nil
// pointer, slice, array or map is also considered as zero.
func IsZero(ctx RuleContext) error {
	// check zeroness
	if !isZero(ctx) {
		return xo.SF("not zero")
	}

	return nil
}

// IsNotZero will check if the provided value is not zero. It will determine
// zeroness using IsZero() or Zero() if implemented and default back to reflect.
// A nil pointer, slice, array or map is also considered as zero.
func IsNotZero(ctx RuleContext) error {
	// check zeroness
	if isZero(ctx) {
		return xo.SF("zero")
	}

	return nil
}

func isEmpty(ctx RuleContext) bool {
	// check nil
	if ctx.IsNil() {
		return true
	}

	// unwrap
	ctx.Unwrap()

	// check array, slice, map and string length
	switch ctx.RValue.Kind() {
	case reflect.Slice, reflect.Map:
		return ctx.RValue.Len() == 0
	}

	panic(fmt.Sprintf("stick: cannot check length of %T", ctx.IValue))
}

// IsEmpty will check if the provided value is empty. Emptiness can only be
// checked for slices and maps.
func IsEmpty(ctx RuleContext) error {
	// check emptiness
	if !isEmpty(ctx) {
		return xo.SF("not empty")
	}

	return nil
}

// IsNotEmpty will check if the provided value is not empty. Emptiness can only
// be checked for slices and maps.
func IsNotEmpty(ctx RuleContext) error {
	// check emptiness
	if isEmpty(ctx) {
		return xo.SF("empty")
	}

	return nil
}

// IsValid will check if the value is valid by calling Validate(), IsValid() or
// Valid().
func IsValid(ctx RuleContext) error {
	// check using Validate() method
	if v, ok := ctx.IValue.(Validatable); ok {
		return v.Validate()
	}

	// check using IsValid() method
	type isValid interface {
		IsValid() bool
	}
	if v, ok := ctx.IValue.(isValid); ok {
		// check validity
		if !v.IsValid() {
			return xo.SF("invalid")
		}

		return nil
	}

	// check using Valid() method
	type valid interface {
		Valid() bool
	}
	if v, ok := ctx.IValue.(valid); ok {
		// check validity
		if !v.Valid() {
			return xo.SF("invalid")
		}

		return nil
	}

	panic(fmt.Sprintf("stick: cannot check validity of %T", ctx.IValue))
}

// IsMinLen checks whether the value has at least the specified length.
func IsMinLen(min int) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check length
			if ctx.RValue.Len() < min {
				return xo.SF("too short")
			}

			return nil
		})
	}
}

// IsMaxLen checks whether the value does not exceed the specified length.
func IsMaxLen(max int) Rule {
	return func(ctx RuleContext) error {
		// check length
		if ctx.RValue.Len() > max {
			return xo.SF("too long")
		}

		return nil
	}
}

// IsMinInt checks whether the value satisfies the provided minimum.
func IsMinInt(min int64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			default:
				panic("stick: expected int value")
			}

			// check min
			if ctx.RValue.Int() < min {
				return xo.SF("too small")
			}

			return nil
		})
	}
}

// IsMaxInt checks whether the value satisfies the provided maximum.
func IsMaxInt(max int64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			default:
				panic("stick: expected int value")
			}

			// check min
			if ctx.RValue.Int() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsMinUint checks whether the value satisfies the provided minimum.
func IsMinUint(min uint64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			default:
				panic("stick: expected uint value")
			}

			// check range
			if ctx.RValue.Uint() < min {
				return xo.SF("too small")
			}

			return nil
		})
	}
}

// IsMaxUint checks whether the value satisfies the provided maximum.
func IsMaxUint(max uint64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			default:
				panic("stick: expected uint value")
			}

			// check max
			if ctx.RValue.Uint() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsMinFloat checks whether the value satisfies the provided minimum.
func IsMinFloat(min float64) Rule {
	return func(ctx RuleContext) error {
		// check value
		switch ctx.RValue.Kind() {
		case reflect.Float32, reflect.Float64:
		default:
			panic("stick: expected float value")
		}

		// check min
		if ctx.RValue.Float() < min {
			return xo.SF("too small")
		}

		return nil
	}
}

// IsMaxFloat checks whether the value satisfies the provided maximum.
func IsMaxFloat(max float64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Float32, reflect.Float64:
			default:
				panic("stick: expected float value")
			}

			// check max
			if ctx.RValue.Float() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsFormat will check of the value corresponds to the format determined by the
// provided string format checker.
func IsFormat(fns ...func(string) bool) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			if ctx.RValue.Kind() != reflect.String {
				panic("stick: expected string value")
			}

			// get string
			str := ctx.RValue.String()

			// check zero
			if str == "" {
				return nil
			}

			// check validity
			for _, fn := range fns {
				if !fn(str) {
					return xo.SF("invalid format")
				}
			}

			return nil
		})
	}
}

// IsRegexMatch will check if a string matches a regular expression.
func IsRegexMatch(reg *regexp.Regexp) Rule {
	return IsFormat(reg.MatchString)
}

// IsPatternMatch will check if a string matches a regular expression pattern.
func IsPatternMatch(pattern string) Rule {
	return IsRegexMatch(regexp.MustCompile(pattern))
}

// IsEmail will check if a string is a valid email.
var IsEmail = IsFormat(govalidator.IsEmail)

// IsURL will check if a string is a valid URL.
var IsURL = IsFormat(govalidator.IsURL)

// IsHost will check if a string is a valid host.
var IsHost = IsFormat(govalidator.IsHost)

// IsDNSName will check if a string is a valid DNS name.
var IsDNSName = IsFormat(govalidator.IsDNSName)

// IsIPAddress will check if a string is a valid IP address.
var IsIPAddress = IsFormat(govalidator.IsIP)

// IsNumeric will check if a string is numeric.
var IsNumeric = IsFormat(govalidator.IsNumeric)

// IsValidUTF8 will check if a string is valid utf8.
var IsValidUTF8 = IsFormat(utf8.ValidString)

// IsVisible will check if a string is visible.
var IsVisible = IsFormat(utf8.ValidString, func(s string) bool {
	// count characters and whitespace
	c := 0
	w := 0
	for _, r := range s {
		c++
		if unicode.IsSpace(r) {
			w++
		}
	}

	return w < c
})
<commit_msg>stick: only check nil if unwrapped<commit_after>package stick

import (
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/256dpi/xo"
	"github.com/asaskevich/govalidator"
)

// Validatable represents a type that can be validated.
type Validatable interface {
	Validate() error
}

// NoValidation can be embedded in a struct to provide a no-op validation method.
type NoValidation struct{}

// Validate will perform no validation.
func (*NoValidation) Validate() error {
	return nil
}

// ValidationError describes a validation error.
type ValidationError struct {
	Reports []ValidationReport
}

// ValidationReport is a single validation error.
type ValidationReport struct {
	Path  []string
	Error error
}

// Error implements the error interface.
func (e ValidationError) Error() string {
	// prepare errors
	var errs []string
	for _, report := range e.Reports {
		errs = append(errs, fmt.Sprintf("%s: %s", strings.Join(report.Path, "."), report.Error.Error()))
	}

	return strings.Join(errs, "; ")
}

// Validator is used to validate an object.
type Validator struct {
	obj     Accessible
	path    []string
	reports []ValidationReport
}

// Validate will validate the provided accessible using the specified validator
// function.
func Validate(obj Accessible, fn func(v *Validator)) error {
	// prepare validator
	val := &Validator{obj: obj}

	// run validator
	fn(val)

	return val.Error()
}

// Nest nest validation under the specified field.
func (v *Validator) Nest(field string, fn func()) {
	// push
	v.path = append(v.path, field)

	// yield
	fn()

	// pop
	v.path = v.path[:len(v.path)-1]
}

// Value will validate the value at the named field using the provided rules.
// If the value is optional it will be skipped if nil or unwrapped if present.
func (v *Validator) Value(name string, optional bool, rules ...Rule) {
	// get value
	value := MustGet(v.obj, name)

	// prepare context
	ctx := RuleContext{
		IValue: value,
		RValue: reflect.ValueOf(value),
	}

	// handle optionals
	if optional {
		// skip if nil
		if ctx.IsNil() {
			return
		}

		// otherwise unwrap pointer once
		ctx.RValue = ctx.RValue.Elem()
		ctx.IValue = ctx.RValue.Interface()
	}

	// execute rules
	for _, rule := range rules {
		err := rule(ctx)
		if err != nil {
			v.Report(name, err)
		}
	}
}

// Items will validate each item of the slice at the named field using the
// provided rules.
func (v *Validator) Items(name string, rules ...Rule) {
	// get slice
	slice := reflect.ValueOf(MustGet(v.obj, name))

	// execute rules for each item
	v.Nest(name, func() {
		for i := 0; i < slice.Len(); i++ {
			// get item
			item := slice.Index(i)

			// prepare context
			ctx := RuleContext{
				IValue: item.Interface(),
				RValue: item,
			}

			// execute rules
			for _, rule := range rules {
				err := rule(ctx)
				if err != nil {
					v.Report(strconv.Itoa(i), err)
				}
			}
		}
	})
}

// Report will report a validation error.
func (v *Validator) Report(name string, err error) {
	// copy path
	path := append([]string{}, v.path...)
	path = append(path, name)

	// add report
	v.reports = append(v.reports, ValidationReport{
		Path:  path,
		Error: err,
	})
}

// Error will return the accumulated error or nil of no errors have yet been
// reported.
func (v *Validator) Error() error {
	// check errors
	if len(v.reports) > 0 {
		return ValidationError{Reports: v.reports}
	}

	return nil
}

// RuleContext carries the to be checked value.
type RuleContext struct {
	IValue interface{}
	RValue reflect.Value
}

// IsNil returns whether the value is nil.
func (c *RuleContext) IsNil() bool {
	// check plain nil
	if c.IValue == nil {
		return true
	}

	// check typed nils
	switch c.RValue.Kind() {
	case reflect.Ptr, reflect.Slice, reflect.Map:
		return c.RValue.IsNil()
	}

	return false
}

// Unwrap will unwrap all pointers.
func (c *RuleContext) Unwrap() bool {
	// unwrap pointers
	var unwrapped bool
	for c.RValue.Kind() == reflect.Ptr {
		c.RValue = c.RValue.Elem()
		unwrapped = true
	}
	if unwrapped {
		c.IValue = c.RValue.Interface()
	}

	return unwrapped
}

// Guard will run the provided function if a concrete value can be unwrapped.
func (c *RuleContext) Guard(fn func() error) error {
	// check nil
	if c.IsNil() {
		return nil
	}

	// unwrap
	c.Unwrap()

	return fn()
}

// Rule is a single validation rule.
type Rule func(ctx RuleContext) error

// IsOK will run the provided validation function.
func IsOK(fn func() error) Rule {
	return func(RuleContext) error {
		return fn()
	}
}

func isZero(ctx RuleContext) bool {
	// check nil
	if ctx.IsNil() {
		return true
	}

	// check using IsZero method
	type isZero interface {
		IsZero() bool
	}
	if v, ok := ctx.IValue.(isZero); ok {
		// check zeroness
		if !v.IsZero() {
			return false
		}

		return true
	}

	// check using Zero method
	type zero interface {
		Zero() bool
	}
	if v, ok := ctx.IValue.(zero); ok {
		// check zeroness
		if !v.Zero() {
			return false
		}

		return true
	}

	// unwrap pointer and check nil again if unwrapped
	if ctx.Unwrap() && ctx.IsNil() {
		return true
	}

	// check zeroness
	if !ctx.RValue.IsZero() {
		return false
	}

	return true
}

// IsZero will check if the provided value is zero. It will determine zeroness
// using IsZero() or Zero() if implemented and default back to reflect. A nil
// pointer, slice, array or map is also considered as zero.
func IsZero(ctx RuleContext) error {
	// check zeroness
	if !isZero(ctx) {
		return xo.SF("not zero")
	}

	return nil
}

// IsNotZero will check if the provided value is not zero. It will determine
// zeroness using IsZero() or Zero() if implemented and default back to reflect.
// A nil pointer, slice, array or map is also considered as zero.
func IsNotZero(ctx RuleContext) error {
	// check zeroness
	if isZero(ctx) {
		return xo.SF("zero")
	}

	return nil
}

func isEmpty(ctx RuleContext) bool {
	// check nil
	if ctx.IsNil() {
		return true
	}

	// unwrap
	ctx.Unwrap()

	// check array, slice, map and string length
	switch ctx.RValue.Kind() {
	case reflect.Slice, reflect.Map:
		return ctx.RValue.Len() == 0
	}

	panic(fmt.Sprintf("stick: cannot check length of %T", ctx.IValue))
}

// IsEmpty will check if the provided value is empty. Emptiness can only be
// checked for slices and maps.
func IsEmpty(ctx RuleContext) error {
	// check emptiness
	if !isEmpty(ctx) {
		return xo.SF("not empty")
	}

	return nil
}

// IsNotEmpty will check if the provided value is not empty. Emptiness can only
// be checked for slices and maps.
func IsNotEmpty(ctx RuleContext) error {
	// check emptiness
	if isEmpty(ctx) {
		return xo.SF("empty")
	}

	return nil
}

// IsValid will check if the value is valid by calling Validate(), IsValid() or
// Valid().
func IsValid(ctx RuleContext) error {
	// check using Validate() method
	if v, ok := ctx.IValue.(Validatable); ok {
		return v.Validate()
	}

	// check using IsValid() method
	type isValid interface {
		IsValid() bool
	}
	if v, ok := ctx.IValue.(isValid); ok {
		// check validity
		if !v.IsValid() {
			return xo.SF("invalid")
		}

		return nil
	}

	// check using Valid() method
	type valid interface {
		Valid() bool
	}
	if v, ok := ctx.IValue.(valid); ok {
		// check validity
		if !v.Valid() {
			return xo.SF("invalid")
		}

		return nil
	}

	panic(fmt.Sprintf("stick: cannot check validity of %T", ctx.IValue))
}

// IsMinLen checks whether the value has at least the specified length.
func IsMinLen(min int) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check length
			if ctx.RValue.Len() < min {
				return xo.SF("too short")
			}

			return nil
		})
	}
}

// IsMaxLen checks whether the value does not exceed the specified length.
func IsMaxLen(max int) Rule {
	return func(ctx RuleContext) error {
		// check length
		if ctx.RValue.Len() > max {
			return xo.SF("too long")
		}

		return nil
	}
}

// IsMinInt checks whether the value satisfies the provided minimum.
func IsMinInt(min int64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			default:
				panic("stick: expected int value")
			}

			// check min
			if ctx.RValue.Int() < min {
				return xo.SF("too small")
			}

			return nil
		})
	}
}

// IsMaxInt checks whether the value satisfies the provided maximum.
func IsMaxInt(max int64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			default:
				panic("stick: expected int value")
			}

			// check min
			if ctx.RValue.Int() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsMinUint checks whether the value satisfies the provided minimum.
func IsMinUint(min uint64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			default:
				panic("stick: expected uint value")
			}

			// check range
			if ctx.RValue.Uint() < min {
				return xo.SF("too small")
			}

			return nil
		})
	}
}

// IsMaxUint checks whether the value satisfies the provided maximum.
func IsMaxUint(max uint64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			default:
				panic("stick: expected uint value")
			}

			// check max
			if ctx.RValue.Uint() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsMinFloat checks whether the value satisfies the provided minimum.
func IsMinFloat(min float64) Rule {
	return func(ctx RuleContext) error {
		// check value
		switch ctx.RValue.Kind() {
		case reflect.Float32, reflect.Float64:
		default:
			panic("stick: expected float value")
		}

		// check min
		if ctx.RValue.Float() < min {
			return xo.SF("too small")
		}

		return nil
	}
}

// IsMaxFloat checks whether the value satisfies the provided maximum.
func IsMaxFloat(max float64) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			switch ctx.RValue.Kind() {
			case reflect.Float32, reflect.Float64:
			default:
				panic("stick: expected float value")
			}

			// check max
			if ctx.RValue.Float() > max {
				return xo.SF("too big")
			}

			return nil
		})
	}
}

// IsFormat will check of the value corresponds to the format determined by the
// provided string format checker.
func IsFormat(fns ...func(string) bool) Rule {
	return func(ctx RuleContext) error {
		return ctx.Guard(func() error {
			// check value
			if ctx.RValue.Kind() != reflect.String {
				panic("stick: expected string value")
			}

			// get string
			str := ctx.RValue.String()

			// check zero
			if str == "" {
				return nil
			}

			// check validity
			for _, fn := range fns {
				if !fn(str) {
					return xo.SF("invalid format")
				}
			}

			return nil
		})
	}
}

// IsRegexMatch will check if a string matches a regular expression.
func IsRegexMatch(reg *regexp.Regexp) Rule {
	return IsFormat(reg.MatchString)
}

// IsPatternMatch will check if a string matches a regular expression pattern.
func IsPatternMatch(pattern string) Rule {
	return IsRegexMatch(regexp.MustCompile(pattern))
}

// IsEmail will check if a string is a valid email.
var IsEmail = IsFormat(govalidator.IsEmail)

// IsURL will check if a string is a valid URL.
var IsURL = IsFormat(govalidator.IsURL)

// IsHost will check if a string is a valid host.
var IsHost = IsFormat(govalidator.IsHost)

// IsDNSName will check if a string is a valid DNS name.
var IsDNSName = IsFormat(govalidator.IsDNSName)

// IsIPAddress will check if a string is a valid IP address.
var IsIPAddress = IsFormat(govalidator.IsIP)

// IsNumeric will check if a string is numeric.
var IsNumeric = IsFormat(govalidator.IsNumeric)

// IsValidUTF8 will check if a string is valid utf8.
var IsValidUTF8 = IsFormat(utf8.ValidString)

// IsVisible will check if a string is visible.
var IsVisible = IsFormat(utf8.ValidString, func(s string) bool {
	// count characters and whitespace
	c := 0
	w := 0
	for _, r := range s {
		c++
		if unicode.IsSpace(r) {
			w++
		}
	}

	return w < c
})
<|endoftext|>


<commit_before>package widgets

import (
	"fmt"

	"github.com/ambientsound/pms/api"
	"github.com/ambientsound/pms/console"
	"github.com/ambientsound/pms/input/parser"
	"github.com/ambientsound/pms/message"
	"github.com/ambientsound/pms/style"

	"github.com/gdamore/tcell"
	"github.com/gdamore/tcell/views"
)

// history represents a text history that can be navigated through.
type history struct {
	items   []string
	current string
	index   int
}

// MultibarWidget receives keyboard events, displays status messages, and the position readout.
type MultibarWidget struct {
	api       api.API
	cursor    int
	events    chan parser.KeyEvent
	inputMode int
	msg       message.Message
	runes     []rune
	textStyle tcell.Style

	// Three histories, one for each input mode
	history [3]history

	views.TextBar
	style.Styled
}

// Different input modes are handled in different ways. Check
// MultibarWidget.inputMode against these constants.
const (
	MultibarModeNormal = iota
	MultibarModeInput
	MultibarModeSearch
)

// Add adds to the input history.
func (h *history) Add(s string) {
	if len(s) > 0 {
		hl := len(h.items)
		if hl == 0 || h.items[hl-1] != s {
			h.items = append(h.items, s)
		}
	}
	h.Reset(s)
}

// Reset resets the cursor offset to the last position.
func (h *history) Reset(s string) {
	h.index = len(h.items)
	h.current = s
}

// Current returns the current history item.
func (h *history) Current() string {
	if len(h.items) == 0 || h.index >= len(h.items) {
		console.Log("Want index %d, returning current string '%s'", h.index, h.current)
		h.index = len(h.items)
		return h.current
	}
	h.validateIndex()
	console.Log("History returning index %d", h.index)
	return h.items[h.index]
}

// Navigate navigates the history and returns that history item.
func (h *history) Navigate(offset int) string {
	h.index += offset
	return h.Current()
}

// validateIndex ensures that the item index stays within the valid range.
func (h *history) validateIndex() {
	if h.index >= len(h.items) {
		h.index = len(h.items) - 1
	}
	if h.index < 0 {
		h.index = 0
	}
}

func NewMultibarWidget(a api.API, events chan parser.KeyEvent) *MultibarWidget {
	return &MultibarWidget{
		api:    a,
		runes:  make([]rune, 0),
		events: events,
		history: [3]history{
			{items: make([]string, 0)},
			{items: make([]string, 0)},
			{items: make([]string, 0)},
		},
	}
}

// History returns the input history of the current input mode.
func (m *MultibarWidget) History() *history {
	return &m.history[m.inputMode]
}

func (m *MultibarWidget) SetMessage(msg message.Message) {
	switch {
	case msg.Type == message.SequenceText:
		m.textStyle = m.Style("sequenceText")
	case msg.Severity == message.Info:
		m.textStyle = m.Style("statusbar")
	case msg.Severity == message.Error:
		m.textStyle = m.Style("errorText")
	default:
		return
	}
	m.msg = msg
	m.DrawStatusbar()
}

func (m *MultibarWidget) SetMode(mode int) error {
	switch mode {
	case MultibarModeNormal:
	case MultibarModeInput:
	case MultibarModeSearch:
	default:
		return fmt.Errorf("Mode not supported")
	}
	console.Log("Switching input mode from %d to %d", m.inputMode, mode)
	m.inputMode = mode
	m.setRunes(make([]rune, 0))
	m.History().Reset("")
	PostEventInputChanged(m)
	return nil
}

func (m *MultibarWidget) Mode() int {
	return m.inputMode
}

func (m *MultibarWidget) setRunes(r []rune) {
	m.runes = r
	m.validateCursor()
	m.DrawStatusbar()
}

// Draw the statusbar part of the Multibar.
func (m *MultibarWidget) DrawStatusbar() {
	var st tcell.Style
	var s string

	switch m.inputMode {
	case MultibarModeInput:
		s = ":" + m.RuneString()
		st = m.Style("commandText")
	case MultibarModeSearch:
		s = "/" + m.RuneString()
		st = m.Style("searchText")
	default:
		if len(m.msg.Text) == 0 && m.api.Songlist().HasVisualSelection() {
			s = "-- VISUAL --"
			st = m.Style("visualText")
		} else {
			s = m.msg.Text
			st = m.textStyle
		}
	}

	m.SetLeft(s, st)
}

func (m *MultibarWidget) RuneString() string {
	return string(m.runes)
}

func (m *MultibarWidget) RuneLen() int {
	return len(m.runes)
}

func (m *MultibarWidget) Cursor() int {
	return m.cursor
}

func (m *MultibarWidget) handleTruncate() {
	m.setRunes(make([]rune, 0))
	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

// handleTextRune inserts a literal rune at the cursor position.
func (m *MultibarWidget) handleTextRune(r rune) {
	runes := make([]rune, len(m.runes)+1)
	copy(runes, m.runes[:m.cursor])
	copy(runes[m.cursor+1:], m.runes[m.cursor:])
	runes[m.cursor] = r
	m.setRunes(runes)

	m.cursor++
	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

// handleBackspace deletes a literal rune behind the cursor position.
func (m *MultibarWidget) handleBackspace() {

	// Backspace on an empty string returns to normal mode.
	if len(m.runes) == 0 {
		m.SetMode(MultibarModeNormal)
		return
	}

	// Can't delete at start of string
	if m.cursor == 0 {
		return
	}

	// Copy all runes except the deleted rune
	runes := make([]rune, len(m.runes)-1)
	copy(runes, m.runes[:m.cursor-1])
	copy(runes[index:], m.runes[m.cursor:])

	m.cursor--
	m.setRunes(runes)

	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

func (m *MultibarWidget) handleFinished() {
	m.History().Add(m.RuneString())
	PostEventInputFinished(m)
}

func (m *MultibarWidget) handleAbort() {
	m.History().Add(m.RuneString())
	m.History().Reset("")
	m.setRunes(make([]rune, 0))
	m.SetMode(MultibarModeNormal)
}

func (m *MultibarWidget) handleHistory(offset int) {
	s := m.History().Navigate(offset)
	m.setRunes([]rune(s))
	m.cursor = len(m.runes)
	PostEventInputChanged(m)
}

func (m *MultibarWidget) handleCursor(offset int) {
	m.cursor += offset
	m.validateCursor()
	PostEventInputChanged(m) // FIXME: this triggers a search query; disable that
}

// validateCursor makes sure the cursor stays within boundaries.
func (m *MultibarWidget) validateCursor() {
	if m.cursor > len(m.runes) {
		m.cursor = len(m.runes)
	}
	if m.cursor < 0 {
		m.cursor = 0
	}
}

// handleTextInputEvent is called when an input event is received during any of the text input modes.
func (m *MultibarWidget) handleTextInputEvent(ev *tcell.EventKey) bool {
	switch ev.Key() {

	case tcell.KeyRune:
		m.handleTextRune(ev.Rune())
	case tcell.KeyCtrlU:
		m.handleTruncate()
	case tcell.KeyEnter:
		m.handleFinished()
	case tcell.KeyLeft:
		m.handleCursor(-1)
	case tcell.KeyRight:
		m.handleCursor(1)
	case tcell.KeyUp:
		m.handleHistory(-1)
	case tcell.KeyDown:
		m.handleHistory(1)
	case tcell.KeyCtrlG, tcell.KeyCtrlC:
		m.handleAbort()
	case tcell.KeyBS, tcell.KeyDEL:
		m.handleBackspace()

	default:
		console.Log("Unhandled text input event in Multibar: %s", ev.Key())
		return false
	}

	return true
}

// handleNormalEvent is called when an input event is received during command mode.
func (m *MultibarWidget) handleNormalEvent(ev *tcell.EventKey) bool {
	ke := parser.KeyEvent{Key: ev.Key(), Rune: ev.Rune()}
	//console.Log("Input event in command mode: %s %s", ke.Key, string(ke.Rune))
	m.events <- ke
	return true
}

func (m *MultibarWidget) HandleEvent(ev tcell.Event) bool {
	switch ev := ev.(type) {
	case *tcell.EventKey:
		switch m.inputMode {
		case MultibarModeNormal:
			return m.handleNormalEvent(ev)
		case MultibarModeInput:
			return m.handleTextInputEvent(ev)
		case MultibarModeSearch:
			return m.handleTextInputEvent(ev)
		}
	}
	return false
}

func (w *MultibarWidget) Resize() {
}
<commit_msg>Fix broken build<commit_after>package widgets

import (
	"fmt"

	"github.com/ambientsound/pms/api"
	"github.com/ambientsound/pms/console"
	"github.com/ambientsound/pms/input/parser"
	"github.com/ambientsound/pms/message"
	"github.com/ambientsound/pms/style"

	"github.com/gdamore/tcell"
	"github.com/gdamore/tcell/views"
)

// history represents a text history that can be navigated through.
type history struct {
	items   []string
	current string
	index   int
}

// MultibarWidget receives keyboard events, displays status messages, and the position readout.
type MultibarWidget struct {
	api       api.API
	cursor    int
	events    chan parser.KeyEvent
	inputMode int
	msg       message.Message
	runes     []rune
	textStyle tcell.Style

	// Three histories, one for each input mode
	history [3]history

	views.TextBar
	style.Styled
}

// Different input modes are handled in different ways. Check
// MultibarWidget.inputMode against these constants.
const (
	MultibarModeNormal = iota
	MultibarModeInput
	MultibarModeSearch
)

// Add adds to the input history.
func (h *history) Add(s string) {
	if len(s) > 0 {
		hl := len(h.items)
		if hl == 0 || h.items[hl-1] != s {
			h.items = append(h.items, s)
		}
	}
	h.Reset(s)
}

// Reset resets the cursor offset to the last position.
func (h *history) Reset(s string) {
	h.index = len(h.items)
	h.current = s
}

// Current returns the current history item.
func (h *history) Current() string {
	if len(h.items) == 0 || h.index >= len(h.items) {
		console.Log("Want index %d, returning current string '%s'", h.index, h.current)
		h.index = len(h.items)
		return h.current
	}
	h.validateIndex()
	console.Log("History returning index %d", h.index)
	return h.items[h.index]
}

// Navigate navigates the history and returns that history item.
func (h *history) Navigate(offset int) string {
	h.index += offset
	return h.Current()
}

// validateIndex ensures that the item index stays within the valid range.
func (h *history) validateIndex() {
	if h.index >= len(h.items) {
		h.index = len(h.items) - 1
	}
	if h.index < 0 {
		h.index = 0
	}
}

func NewMultibarWidget(a api.API, events chan parser.KeyEvent) *MultibarWidget {
	return &MultibarWidget{
		api:    a,
		runes:  make([]rune, 0),
		events: events,
		history: [3]history{
			{items: make([]string, 0)},
			{items: make([]string, 0)},
			{items: make([]string, 0)},
		},
	}
}

// History returns the input history of the current input mode.
func (m *MultibarWidget) History() *history {
	return &m.history[m.inputMode]
}

func (m *MultibarWidget) SetMessage(msg message.Message) {
	switch {
	case msg.Type == message.SequenceText:
		m.textStyle = m.Style("sequenceText")
	case msg.Severity == message.Info:
		m.textStyle = m.Style("statusbar")
	case msg.Severity == message.Error:
		m.textStyle = m.Style("errorText")
	default:
		return
	}
	m.msg = msg
	m.DrawStatusbar()
}

func (m *MultibarWidget) SetMode(mode int) error {
	switch mode {
	case MultibarModeNormal:
	case MultibarModeInput:
	case MultibarModeSearch:
	default:
		return fmt.Errorf("Mode not supported")
	}
	console.Log("Switching input mode from %d to %d", m.inputMode, mode)
	m.inputMode = mode
	m.setRunes(make([]rune, 0))
	m.History().Reset("")
	PostEventInputChanged(m)
	return nil
}

func (m *MultibarWidget) Mode() int {
	return m.inputMode
}

func (m *MultibarWidget) setRunes(r []rune) {
	m.runes = r
	m.validateCursor()
	m.DrawStatusbar()
}

// Draw the statusbar part of the Multibar.
func (m *MultibarWidget) DrawStatusbar() {
	var st tcell.Style
	var s string

	switch m.inputMode {
	case MultibarModeInput:
		s = ":" + m.RuneString()
		st = m.Style("commandText")
	case MultibarModeSearch:
		s = "/" + m.RuneString()
		st = m.Style("searchText")
	default:
		if len(m.msg.Text) == 0 && m.api.Songlist().HasVisualSelection() {
			s = "-- VISUAL --"
			st = m.Style("visualText")
		} else {
			s = m.msg.Text
			st = m.textStyle
		}
	}

	m.SetLeft(s, st)
}

func (m *MultibarWidget) RuneString() string {
	return string(m.runes)
}

func (m *MultibarWidget) RuneLen() int {
	return len(m.runes)
}

func (m *MultibarWidget) Cursor() int {
	return m.cursor
}

func (m *MultibarWidget) handleTruncate() {
	m.setRunes(make([]rune, 0))
	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

// handleTextRune inserts a literal rune at the cursor position.
func (m *MultibarWidget) handleTextRune(r rune) {
	runes := make([]rune, len(m.runes)+1)
	copy(runes, m.runes[:m.cursor])
	copy(runes[m.cursor+1:], m.runes[m.cursor:])
	runes[m.cursor] = r
	m.setRunes(runes)

	m.cursor++
	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

// handleBackspace deletes a literal rune behind the cursor position.
func (m *MultibarWidget) handleBackspace() {

	// Backspace on an empty string returns to normal mode.
	if len(m.runes) == 0 {
		m.SetMode(MultibarModeNormal)
		return
	}

	// Can't delete at start of string
	if m.cursor == 0 {
		return
	}

	// Copy all runes except the deleted rune
	runes := make([]rune, len(m.runes)-1)
	index := copy(runes, m.runes[:m.cursor-1])
	copy(runes[index:], m.runes[m.cursor:])

	m.cursor--
	m.setRunes(runes)

	m.History().Reset(m.RuneString())
	PostEventInputChanged(m)
}

func (m *MultibarWidget) handleFinished() {
	m.History().Add(m.RuneString())
	PostEventInputFinished(m)
}

func (m *MultibarWidget) handleAbort() {
	m.History().Add(m.RuneString())
	m.History().Reset("")
	m.setRunes(make([]rune, 0))
	m.SetMode(MultibarModeNormal)
}

func (m *MultibarWidget) handleHistory(offset int) {
	s := m.History().Navigate(offset)
	m.setRunes([]rune(s))
	m.cursor = len(m.runes)
	PostEventInputChanged(m)
}

func (m *MultibarWidget) handleCursor(offset int) {
	m.cursor += offset
	m.validateCursor()
	PostEventInputChanged(m) // FIXME: this triggers a search query; disable that
}

// validateCursor makes sure the cursor stays within boundaries.
func (m *MultibarWidget) validateCursor() {
	if m.cursor > len(m.runes) {
		m.cursor = len(m.runes)
	}
	if m.cursor < 0 {
		m.cursor = 0
	}
}

// handleTextInputEvent is called when an input event is received during any of the text input modes.
func (m *MultibarWidget) handleTextInputEvent(ev *tcell.EventKey) bool {
	switch ev.Key() {

	case tcell.KeyRune:
		m.handleTextRune(ev.Rune())
	case tcell.KeyCtrlU:
		m.handleTruncate()
	case tcell.KeyEnter:
		m.handleFinished()
	case tcell.KeyLeft:
		m.handleCursor(-1)
	case tcell.KeyRight:
		m.handleCursor(1)
	case tcell.KeyUp:
		m.handleHistory(-1)
	case tcell.KeyDown:
		m.handleHistory(1)
	case tcell.KeyCtrlG, tcell.KeyCtrlC:
		m.handleAbort()
	case tcell.KeyBS, tcell.KeyDEL:
		m.handleBackspace()

	default:
		console.Log("Unhandled text input event in Multibar: %s", ev.Key())
		return false
	}

	return true
}

// handleNormalEvent is called when an input event is received during command mode.
func (m *MultibarWidget) handleNormalEvent(ev *tcell.EventKey) bool {
	ke := parser.KeyEvent{Key: ev.Key(), Rune: ev.Rune()}
	//console.Log("Input event in command mode: %s %s", ke.Key, string(ke.Rune))
	m.events <- ke
	return true
}

func (m *MultibarWidget) HandleEvent(ev tcell.Event) bool {
	switch ev := ev.(type) {
	case *tcell.EventKey:
		switch m.inputMode {
		case MultibarModeNormal:
			return m.handleNormalEvent(ev)
		case MultibarModeInput:
			return m.handleTextInputEvent(ev)
		case MultibarModeSearch:
			return m.handleTextInputEvent(ev)
		}
	}
	return false
}

func (w *MultibarWidget) Resize() {
}
<|endoftext|>


<commit_before>// Copyright 2019 Istio 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 converter

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	coreV1 "k8s.io/api/core/v1"

	"istio.io/api/annotation"
	networking "istio.io/api/networking/v1alpha3"
	"istio.io/pkg/log"

	"istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod"
	"istio.io/istio/galley/pkg/config/resource"
	"istio.io/istio/pkg/config/constants"
	configKube "istio.io/istio/pkg/config/kube"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/config/protocol"
)

// Instance of the converter.
type Instance struct {
	domain string
	pods   pod.Cache
}

// New creates a new instance of the converter.
func New(domain string, pods pod.Cache) *Instance {
	return &Instance{
		domain: domain,
		pods:   pods,
	}
}

// Convert applies the conversion function from k8s Service and Endpoints to ServiceEntry. The
// ServiceEntry is passed as an argument (out) in order to enable object reuse in the future.
func (i *Instance) Convert(service *resource.Entry, endpoints *resource.Entry, outMeta *resource.Metadata,
	out *networking.ServiceEntry) error {
	// we want to build the endpoints and then services
	// as availability of endpoints can impact determining
	// service resolution e.g STATIC service resolution
	// must have endpoints
	i.convertEndpoints(endpoints, outMeta, out)
	i.convertService(service, outMeta, out)
	return nil
}

// convertService applies the k8s Service to the output.
func (i *Instance) convertService(service *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) {
	if service == nil {
		// For testing only. Production code will always provide a non-nil service.
		return
	}

	spec := service.Item.(*coreV1.ServiceSpec)
	location := networking.ServiceEntry_MESH_INTERNAL
	endpoints := out.Endpoints
	if len(endpoints) == 0 {
		endpoints = convertExternalServiceEndpoints(spec, service.Metadata)
	}

	var resolution networking.ServiceEntry_Resolution
	// Resolution STATIC must have endpoints
	if len(endpoints) != 0 {
		resolution = networking.ServiceEntry_STATIC
	}

	// Check for an external service
	externalName := ""
	if spec.Type == coreV1.ServiceTypeExternalName && spec.ExternalName != "" {
		externalName = spec.ExternalName
		resolution = networking.ServiceEntry_DNS
		location = networking.ServiceEntry_MESH_EXTERNAL
	}

	// Check for unspecified Cluster IP
	addr := constants.UnspecifiedIP
	if spec.ClusterIP != "" && spec.ClusterIP != coreV1.ClusterIPNone {
		addr = spec.ClusterIP
	}
	if addr == constants.UnspecifiedIP && externalName == "" {
		// Headless services should not be load balanced
		resolution = networking.ServiceEntry_NONE
	}
	// Resolution NONE must not have endpoints
	if resolution == networking.ServiceEntry_NONE && len(endpoints) != 0 {
		resolution = networking.ServiceEntry_STATIC
	}

	ports := make([]*networking.Port, 0, len(spec.Ports))
	for _, port := range spec.Ports {
		p, err := convertPort(port)
		if err != nil {
			log.Errorf("convertService: %s", err)
			continue
		}
		ports = append(ports, p)
	}

	host := serviceHostname(service.Metadata.Name, i.domain)

	// Store everything in the ServiceEntry.
	out.Hosts = []string{host}
	// CIDR addrs are only allowed for NONE/STATIC resolution types
	if resolution != networking.ServiceEntry_DNS {
		out.Addresses = []string{addr}
	} else {
		out.Addresses = []string{constants.UnspecifiedIP}
	}
	out.Resolution = resolution
	out.Location = location
	out.Ports = ports
	out.Endpoints = endpoints
	out.ExportTo = convertExportTo(service.Metadata.Annotations)

	// Convert Metadata
	outMeta.Name = service.Metadata.Name
	outMeta.Labels = service.Metadata.Labels.Clone()

	// Convert the creation time.
	outMeta.CreateTime = service.Metadata.CreateTime

	// Update the annotations.
	outMeta.Annotations = outMeta.Annotations.CloneOrCreate()
	for k, v := range service.Metadata.Annotations {
		outMeta.Annotations[k] = v
	}

	// Add an annotation for the version of the service resource.
	outMeta.Annotations[annotation.AlphaNetworkingServiceVersion.Name] = string(service.Metadata.Version)
}

func convertExportTo(annotations resource.StringMap) []string {
	var exportTo map[string]struct{}
	if annotations[annotation.NetworkingExportTo.Name] != "" {
		exportTo = make(map[string]struct{})
		for _, e := range strings.Split(annotations[annotation.NetworkingExportTo.Name], ",") {
			exportTo[strings.TrimSpace(e)] = struct{}{}
		}
	}
	if exportTo == nil {
		return nil
	}

	out := make([]string, 0, len(exportTo))
	for k := range exportTo {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

// convertEndpoints applies the k8s Endpoints to the output.
func (i *Instance) convertEndpoints(endpoints *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) {
	if endpoints == nil {
		return
	}

	spec := endpoints.Item.(*coreV1.Endpoints)
	// Store the subject alternate names in a set to avoid duplicates.
	subjectAltNameSet := make(map[string]struct{})
	eps := make([]*networking.ServiceEntry_Endpoint, 0)

	// TODO: Add pooling support for strings.Builder and put it in pkg/pool

	// A builder for the annotation for not-ready addresses. The ServiceEntry does not support not ready addresses,
	// so we send them as an annotation instead.
	var notReadyBuilder strings.Builder

	for _, subset := range spec.Subsets {
		// Convert the ports for this subset. They will be re-used for each endpoint in the same subset.
		ports := make(map[string]uint32)
		for _, port := range subset.Ports {
			ports[port.Name] = uint32(port.Port)

			// Process the not ready addresses.
			portString := strconv.Itoa(int(port.Port))
			for _, address := range subset.NotReadyAddresses {
				if notReadyBuilder.Len() > 0 {
					// Add a separator between the addresses.
					notReadyBuilder.WriteByte(',')
				}
				notReadyBuilder.WriteString(address.IP)
				notReadyBuilder.WriteByte(':')
				notReadyBuilder.WriteString(portString)
			}
		}

		// Convert the endpoints in this subset.
		for _, address := range subset.Addresses {
			locality := ""
			var labels map[string]string

			ip := address.IP
			p, hasPod := i.pods.GetPodByIP(ip)
			if hasPod {
				labels = p.Labels
				locality = p.Locality
				if p.ServiceAccountName != "" {
					subjectAltNameSet[p.ServiceAccountName] = struct{}{}
				}
			}

			ep := &networking.ServiceEntry_Endpoint{
				Labels:   labels,
				Address:  ip,
				Ports:    ports,
				Locality: locality,
				// TODO(nmittler): Network: "",
			}
			eps = append(eps, ep)
		}
	}

	// Convert the subject alternate names to an array.
	subjectAltNames := make([]string, 0, len(subjectAltNameSet))
	for k := range subjectAltNameSet {
		subjectAltNames = append(subjectAltNames, k)
	}
	sort.Strings(subjectAltNames)

	out.Endpoints = eps
	out.SubjectAltNames = subjectAltNames

	// Add an annotation for the version of the Endpoints resource.
	outMeta.Annotations = outMeta.Annotations.CloneOrCreate()
	outMeta.Annotations[annotation.AlphaNetworkingEndpointsVersion.Name] = string(endpoints.Metadata.Version)

	// Add an annotation for any "not ready" endpoints.
	if notReadyBuilder.Len() > 0 {
		outMeta.Annotations[annotation.AlphaNetworkingNotReadyEndpoints.Name] = notReadyBuilder.String()
	}
}

func convertExternalServiceEndpoints(
	svc *coreV1.ServiceSpec,
	serviceMeta resource.Metadata) []*networking.ServiceEntry_Endpoint {

	endpoints := make([]*networking.ServiceEntry_Endpoint, 0)
	if svc.Type == coreV1.ServiceTypeExternalName && svc.ExternalName != "" {
		// Generate endpoints for the external service.
		ports := make(map[string]uint32)
		for _, port := range svc.Ports {
			ports[port.Name] = uint32(port.Port)
		}
		addr := svc.ExternalName
		endpoints = append(endpoints, &networking.ServiceEntry_Endpoint{
			Address: addr,
			Ports:   ports,
			Labels:  serviceMeta.Labels,
		})
	}
	return endpoints
}

// serviceHostname produces FQDN for a k8s service
func serviceHostname(fullName resource.Name, domainSuffix string) string {
	namespace, name := fullName.InterpretAsNamespaceAndName()
	if namespace == "" {
		namespace = coreV1.NamespaceDefault
	}
	return name + "." + namespace + ".svc." + domainSuffix
}

func convertPort(port coreV1.ServicePort) (*networking.Port, error) {
	if err := validatePortName(port.Name); err != nil {
		return nil, err
	}
	if err := validateProtocol(string(port.Protocol)); err != nil {
		return nil, err
	}
	if err := validatePort(port.Port); err != nil {
		return nil, err
	}

	return &networking.Port{
		Name:     port.Name,
		Number:   uint32(port.Port),
		Protocol: string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol)),
	}, nil
}

func validatePortName(name string) error {
	if !labels.IsDNS1123Label(name) {
		return fmt.Errorf("invalid port name: %s", name)
	}
	return nil
}

func validateProtocol(protocolStr string) error {
	// Empty string is used for protocol sniffing.
	if protocolStr != "" && protocol.Parse(protocolStr) == protocol.Unsupported {
		return fmt.Errorf("unsupported protocol: %s", protocolStr)
	}
	return nil
}

// ValidatePort checks that the network port is in range
func validatePort(port int32) error {
	if 1 <= port && port <= 65535 {
		return nil
	}
	return fmt.Errorf("port number %d must be in the range 1..65535", port)
}
<commit_msg>clean up logging for service port conversion (#19479)<commit_after>// Copyright 2019 Istio 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 converter

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	coreV1 "k8s.io/api/core/v1"

	"istio.io/api/annotation"
	networking "istio.io/api/networking/v1alpha3"

	"istio.io/istio/galley/pkg/config/processor/transforms/serviceentry/pod"
	"istio.io/istio/galley/pkg/config/resource"
	"istio.io/istio/galley/pkg/config/scope"
	"istio.io/istio/pkg/config/constants"
	configKube "istio.io/istio/pkg/config/kube"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/config/protocol"
)

// Instance of the converter.
type Instance struct {
	domain string
	pods   pod.Cache
}

// New creates a new instance of the converter.
func New(domain string, pods pod.Cache) *Instance {
	return &Instance{
		domain: domain,
		pods:   pods,
	}
}

// Convert applies the conversion function from k8s Service and Endpoints to ServiceEntry. The
// ServiceEntry is passed as an argument (out) in order to enable object reuse in the future.
func (i *Instance) Convert(service *resource.Entry, endpoints *resource.Entry, outMeta *resource.Metadata,
	out *networking.ServiceEntry) error {
	// we want to build the endpoints and then services
	// as availability of endpoints can impact determining
	// service resolution e.g STATIC service resolution
	// must have endpoints
	i.convertEndpoints(endpoints, outMeta, out)
	i.convertService(service, outMeta, out)
	return nil
}

// convertService applies the k8s Service to the output.
func (i *Instance) convertService(service *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) {
	if service == nil {
		// For testing only. Production code will always provide a non-nil service.
		return
	}

	spec := service.Item.(*coreV1.ServiceSpec)
	location := networking.ServiceEntry_MESH_INTERNAL
	endpoints := out.Endpoints
	if len(endpoints) == 0 {
		endpoints = convertExternalServiceEndpoints(spec, service.Metadata)
	}

	var resolution networking.ServiceEntry_Resolution
	// Resolution STATIC must have endpoints
	if len(endpoints) != 0 {
		resolution = networking.ServiceEntry_STATIC
	}

	// Check for an external service
	externalName := ""
	if spec.Type == coreV1.ServiceTypeExternalName && spec.ExternalName != "" {
		externalName = spec.ExternalName
		resolution = networking.ServiceEntry_DNS
		location = networking.ServiceEntry_MESH_EXTERNAL
	}

	// Check for unspecified Cluster IP
	addr := constants.UnspecifiedIP
	if spec.ClusterIP != "" && spec.ClusterIP != coreV1.ClusterIPNone {
		addr = spec.ClusterIP
	}
	if addr == constants.UnspecifiedIP && externalName == "" {
		// Headless services should not be load balanced
		resolution = networking.ServiceEntry_NONE
	}
	// Resolution NONE must not have endpoints
	if resolution == networking.ServiceEntry_NONE && len(endpoints) != 0 {
		resolution = networking.ServiceEntry_STATIC
	}

	ports := make([]*networking.Port, 0, len(spec.Ports))
	for _, port := range spec.Ports {
		p, err := convertPort(port)
		if err != nil {
			scope.Processing.Warnf("convertService: failed to convert port %v for service %s (skipping): %v", port, service.Metadata.Name, err)
			continue
		}
		ports = append(ports, p)
	}

	host := serviceHostname(service.Metadata.Name, i.domain)

	// Store everything in the ServiceEntry.
	out.Hosts = []string{host}
	// CIDR addrs are only allowed for NONE/STATIC resolution types
	if resolution != networking.ServiceEntry_DNS {
		out.Addresses = []string{addr}
	} else {
		out.Addresses = []string{constants.UnspecifiedIP}
	}
	out.Resolution = resolution
	out.Location = location
	out.Ports = ports
	out.Endpoints = endpoints
	out.ExportTo = convertExportTo(service.Metadata.Annotations)

	// Convert Metadata
	outMeta.Name = service.Metadata.Name
	outMeta.Labels = service.Metadata.Labels.Clone()

	// Convert the creation time.
	outMeta.CreateTime = service.Metadata.CreateTime

	// Update the annotations.
	outMeta.Annotations = outMeta.Annotations.CloneOrCreate()
	for k, v := range service.Metadata.Annotations {
		outMeta.Annotations[k] = v
	}

	// Add an annotation for the version of the service resource.
	outMeta.Annotations[annotation.AlphaNetworkingServiceVersion.Name] = string(service.Metadata.Version)
}

func convertExportTo(annotations resource.StringMap) []string {
	var exportTo map[string]struct{}
	if annotations[annotation.NetworkingExportTo.Name] != "" {
		exportTo = make(map[string]struct{})
		for _, e := range strings.Split(annotations[annotation.NetworkingExportTo.Name], ",") {
			exportTo[strings.TrimSpace(e)] = struct{}{}
		}
	}
	if exportTo == nil {
		return nil
	}

	out := make([]string, 0, len(exportTo))
	for k := range exportTo {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

// convertEndpoints applies the k8s Endpoints to the output.
func (i *Instance) convertEndpoints(endpoints *resource.Entry, outMeta *resource.Metadata, out *networking.ServiceEntry) {
	if endpoints == nil {
		return
	}

	spec := endpoints.Item.(*coreV1.Endpoints)
	// Store the subject alternate names in a set to avoid duplicates.
	subjectAltNameSet := make(map[string]struct{})
	eps := make([]*networking.ServiceEntry_Endpoint, 0)

	// TODO: Add pooling support for strings.Builder and put it in pkg/pool

	// A builder for the annotation for not-ready addresses. The ServiceEntry does not support not ready addresses,
	// so we send them as an annotation instead.
	var notReadyBuilder strings.Builder

	for _, subset := range spec.Subsets {
		// Convert the ports for this subset. They will be re-used for each endpoint in the same subset.
		ports := make(map[string]uint32)
		for _, port := range subset.Ports {
			ports[port.Name] = uint32(port.Port)

			// Process the not ready addresses.
			portString := strconv.Itoa(int(port.Port))
			for _, address := range subset.NotReadyAddresses {
				if notReadyBuilder.Len() > 0 {
					// Add a separator between the addresses.
					notReadyBuilder.WriteByte(',')
				}
				notReadyBuilder.WriteString(address.IP)
				notReadyBuilder.WriteByte(':')
				notReadyBuilder.WriteString(portString)
			}
		}

		// Convert the endpoints in this subset.
		for _, address := range subset.Addresses {
			locality := ""
			var labels map[string]string

			ip := address.IP
			p, hasPod := i.pods.GetPodByIP(ip)
			if hasPod {
				labels = p.Labels
				locality = p.Locality
				if p.ServiceAccountName != "" {
					subjectAltNameSet[p.ServiceAccountName] = struct{}{}
				}
			}

			ep := &networking.ServiceEntry_Endpoint{
				Labels:   labels,
				Address:  ip,
				Ports:    ports,
				Locality: locality,
				// TODO(nmittler): Network: "",
			}
			eps = append(eps, ep)
		}
	}

	// Convert the subject alternate names to an array.
	subjectAltNames := make([]string, 0, len(subjectAltNameSet))
	for k := range subjectAltNameSet {
		subjectAltNames = append(subjectAltNames, k)
	}
	sort.Strings(subjectAltNames)

	out.Endpoints = eps
	out.SubjectAltNames = subjectAltNames

	// Add an annotation for the version of the Endpoints resource.
	outMeta.Annotations = outMeta.Annotations.CloneOrCreate()
	outMeta.Annotations[annotation.AlphaNetworkingEndpointsVersion.Name] = string(endpoints.Metadata.Version)

	// Add an annotation for any "not ready" endpoints.
	if notReadyBuilder.Len() > 0 {
		outMeta.Annotations[annotation.AlphaNetworkingNotReadyEndpoints.Name] = notReadyBuilder.String()
	}
}

func convertExternalServiceEndpoints(
	svc *coreV1.ServiceSpec,
	serviceMeta resource.Metadata) []*networking.ServiceEntry_Endpoint {

	endpoints := make([]*networking.ServiceEntry_Endpoint, 0)
	if svc.Type == coreV1.ServiceTypeExternalName && svc.ExternalName != "" {
		// Generate endpoints for the external service.
		ports := make(map[string]uint32)
		for _, port := range svc.Ports {
			ports[port.Name] = uint32(port.Port)
		}
		addr := svc.ExternalName
		endpoints = append(endpoints, &networking.ServiceEntry_Endpoint{
			Address: addr,
			Ports:   ports,
			Labels:  serviceMeta.Labels,
		})
	}
	return endpoints
}

// serviceHostname produces FQDN for a k8s service
func serviceHostname(fullName resource.Name, domainSuffix string) string {
	namespace, name := fullName.InterpretAsNamespaceAndName()
	if namespace == "" {
		namespace = coreV1.NamespaceDefault
	}
	return name + "." + namespace + ".svc." + domainSuffix
}

func convertPort(port coreV1.ServicePort) (*networking.Port, error) {
	if err := validatePortName(port.Name); err != nil {
		return nil, err
	}
	if err := validateProtocol(string(port.Protocol)); err != nil {
		return nil, err
	}
	if err := validatePort(port.Port); err != nil {
		return nil, err
	}

	return &networking.Port{
		Name:     port.Name,
		Number:   uint32(port.Port),
		Protocol: string(configKube.ConvertProtocol(port.Port, port.Name, port.Protocol)),
	}, nil
}

func validatePortName(name string) error {
	if !labels.IsDNS1123Label(name) {
		return fmt.Errorf("invalid port name: %q", name)
	}
	return nil
}

func validateProtocol(protocolStr string) error {
	// Empty string is used for protocol sniffing.
	if protocolStr != "" && protocol.Parse(protocolStr) == protocol.Unsupported {
		return fmt.Errorf("unsupported protocol: %q", protocolStr)
	}
	return nil
}

// ValidatePort checks that the network port is in range
func validatePort(port int32) error {
	if 1 <= port && port <= 65535 {
		return nil
	}
	return fmt.Errorf("port number %d must be in the range 1..65535", port)
}
<|endoftext|>


<commit_before>package tests

import (
	"context"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"google.golang.org/grpc"
	"sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client"
)

type tcpLB struct {
	t        *testing.T
	mu       sync.RWMutex
	backends []string
}

func copy(wc io.WriteCloser, r io.Reader) {
	defer wc.Close()
	io.Copy(wc, r)
}

func (lb *tcpLB) handleConnection(in net.Conn, backend string) {
	out, err := net.Dial("tcp", backend)
	if err != nil {
		lb.t.Log(err)
		return
	}
	go copy(out, in)
	go copy(in, out)
}

func (lb *tcpLB) serve(stopCh chan struct{}) {
	ln, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		log.Fatalf("failed to bind: %s", err)
	}
	for {
		select {
		case <-stopCh:
			return
		default:
		}
		conn, err := ln.Accept()
		if err != nil {
			log.Printf("failed to accept: %s", err)
			continue
		}
		// go lb.handleConnection(conn, lb.randomBackend())
		back := lb.randomBackend()
		go lb.handleConnection(conn, back)
	}
}

func (lb *tcpLB) addBackend(backend string) {
	lb.mu.Lock()
	defer lb.mu.Unlock()
	lb.backends = append(lb.backends, backend)
}

func (lb *tcpLB) removeBackend(backend string) {
	lb.mu.Lock()
	defer lb.mu.Unlock()
	for i := range lb.backends {
		if lb.backends[i] == backend {
			lb.backends = append(lb.backends[:i], lb.backends[i+1:]...)
			return
		}
	}
}

func (lb *tcpLB) randomBackend() string {
	lb.mu.RLock()
	defer lb.mu.RUnlock()
	i := rand.Intn(len(lb.backends)) /* #nosec G404 */
	return lb.backends[i]
}

const haServerCount = 3

func setupHAProxyServer(t *testing.T) ([]proxy, []func()) {
	proxy1, _, cleanup1, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}

	proxy2, _, cleanup2, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}

	proxy3, _, cleanup3, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}
	return []proxy{proxy1, proxy2, proxy3}, []func(){cleanup1, cleanup2, cleanup3}
}

func TestBasicHAProxyServer_GRPC(t *testing.T) {
	server := httptest.NewServer(newEchoServer("hello"))
	defer server.Close()

	stopCh := make(chan struct{})
	defer close(stopCh)

	proxy, cleanups := setupHAProxyServer(t)

	lb := tcpLB{
		backends: []string{
			proxy[0].agent,
			proxy[1].agent,
			proxy[2].agent,
		},
		t: t,
	}
	go lb.serve(stopCh)

	clientset := runAgent(":8000", stopCh)

	var ready bool
	var hc, cc int
	for i := 0; i < 3; i++ {
		time.Sleep(1 * time.Second)
		hc, cc = clientset.HealthyClientsCount(), clientset.ClientsCount()
		t.Logf("got %d clients, %d of them are healthy", hc, cc)
		if hc == 3 && cc == 3 {
			ready = true
			break
		}
	}
	if !ready {
		t.Fatalf("expected to get 3 clients, got %d clients, %d healthy clients", hc, cc)
	}

	// run test client
	testProxyServer(t, proxy[0].front, server.URL)
	testProxyServer(t, proxy[1].front, server.URL)
	testProxyServer(t, proxy[2].front, server.URL)

	t.Logf("basic HA proxy server test passed")

	// interrupt the HA server
	lb.removeBackend(proxy[0].agent)
	cleanups[0]()

	proxy4, _, cleanup4, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}
	lb.addBackend(proxy4.agent)
	defer func() {
		cleanups[1]()
		cleanups[2]()
		cleanup4()
	}()
	// give the agent some time to detect the disconnection
	time.Sleep(1 * time.Second)

	ready = false
	for i := 0; i < 3; i++ {
		time.Sleep(1 * time.Second)
		hc, cc = clientset.HealthyClientsCount(), clientset.ClientsCount()
		t.Logf("got %d clients, %d of them are healthy", hc, cc)
		if hc == 3 && (cc == 3 || cc == 4) {
			ready = true
			break
		}
	}
	if !ready {
		t.Fatalf("expected to get 3 clients, got %d clients, %d healthy clients", hc, cc)
	}
	// run test client
	testProxyServer(t, proxy[1].front, server.URL)
	testProxyServer(t, proxy[2].front, server.URL)
	testProxyServer(t, proxy4.front, server.URL)
}

func testProxyServer(t *testing.T, front string, target string) {
	ctx := context.Background()
	tunnel, err := client.CreateSingleUseGrpcTunnel(ctx, front, grpc.WithInsecure())
	if err != nil {
		t.Fatal(err)
	}

	c := &http.Client{
		Transport: &http.Transport{
			DialContext: tunnel.DialContext,
		},
		Timeout: 1 * time.Second,
	}

	r, err := c.Get(target)
	if err != nil {
		t.Fatal(err)
	}

	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		t.Error(err)
	}

	if string(data) != "hello" {
		t.Errorf("expect %v; got %v", "hello", string(data))
	}
}
<commit_msg>Dynamically assign port for HA test<commit_after>package tests

import (
	"context"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"google.golang.org/grpc"
	"sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client"
)

type tcpLB struct {
	t        *testing.T
	mu       sync.RWMutex
	backends []string
}

func copy(wc io.WriteCloser, r io.Reader) {
	defer wc.Close()
	io.Copy(wc, r)
}

func (lb *tcpLB) handleConnection(in net.Conn, backend string) {
	out, err := net.Dial("tcp", backend)
	if err != nil {
		lb.t.Log(err)
		return
	}
	go copy(out, in)
	go copy(in, out)
}

func (lb *tcpLB) serve(stopCh chan struct{}) string {
	ln, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		log.Fatalf("failed to bind: %s", err)
	}

	go func() {
		for {
			select {
			case <-stopCh:
				return
			default:
			}
			conn, err := ln.Accept()
			if err != nil {
				log.Printf("failed to accept: %s", err)
				continue
			}
			// go lb.handleConnection(conn, lb.randomBackend())
			back := lb.randomBackend()
			go lb.handleConnection(conn, back)
		}
	}()

	return ln.Addr().String()
}

func (lb *tcpLB) addBackend(backend string) {
	lb.mu.Lock()
	defer lb.mu.Unlock()
	lb.backends = append(lb.backends, backend)
}

func (lb *tcpLB) removeBackend(backend string) {
	lb.mu.Lock()
	defer lb.mu.Unlock()
	for i := range lb.backends {
		if lb.backends[i] == backend {
			lb.backends = append(lb.backends[:i], lb.backends[i+1:]...)
			return
		}
	}
}

func (lb *tcpLB) randomBackend() string {
	lb.mu.RLock()
	defer lb.mu.RUnlock()
	i := rand.Intn(len(lb.backends)) /* #nosec G404 */
	return lb.backends[i]
}

const haServerCount = 3

func setupHAProxyServer(t *testing.T) ([]proxy, []func()) {
	proxy1, _, cleanup1, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}

	proxy2, _, cleanup2, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}

	proxy3, _, cleanup3, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}
	return []proxy{proxy1, proxy2, proxy3}, []func(){cleanup1, cleanup2, cleanup3}
}

func TestBasicHAProxyServer_GRPC(t *testing.T) {
	server := httptest.NewServer(newEchoServer("hello"))
	defer server.Close()

	stopCh := make(chan struct{})
	defer close(stopCh)

	proxy, cleanups := setupHAProxyServer(t)

	lb := tcpLB{
		backends: []string{
			proxy[0].agent,
			proxy[1].agent,
			proxy[2].agent,
		},
		t: t,
	}
	lbAddr := lb.serve(stopCh)

	clientset := runAgent(lbAddr, stopCh)

	var ready bool
	var hc, cc int
	for i := 0; i < 3; i++ {
		time.Sleep(1 * time.Second)
		hc, cc = clientset.HealthyClientsCount(), clientset.ClientsCount()
		t.Logf("got %d clients, %d of them are healthy", hc, cc)
		if hc == 3 && cc == 3 {
			ready = true
			break
		}
	}
	if !ready {
		t.Fatalf("expected to get 3 clients, got %d clients, %d healthy clients", hc, cc)
	}

	// run test client
	testProxyServer(t, proxy[0].front, server.URL)
	testProxyServer(t, proxy[1].front, server.URL)
	testProxyServer(t, proxy[2].front, server.URL)

	t.Logf("basic HA proxy server test passed")

	// interrupt the HA server
	lb.removeBackend(proxy[0].agent)
	cleanups[0]()

	proxy4, _, cleanup4, err := runGRPCProxyServerWithServerCount(haServerCount)
	if err != nil {
		t.Fatal(err)
	}
	lb.addBackend(proxy4.agent)
	defer func() {
		cleanups[1]()
		cleanups[2]()
		cleanup4()
	}()
	// give the agent some time to detect the disconnection
	time.Sleep(1 * time.Second)

	ready = false
	for i := 0; i < 3; i++ {
		time.Sleep(1 * time.Second)
		hc, cc = clientset.HealthyClientsCount(), clientset.ClientsCount()
		t.Logf("got %d clients, %d of them are healthy", hc, cc)
		if hc == 3 && (cc == 3 || cc == 4) {
			ready = true
			break
		}
	}
	if !ready {
		t.Fatalf("expected to get 3 clients, got %d clients, %d healthy clients", hc, cc)
	}
	// run test client
	testProxyServer(t, proxy[1].front, server.URL)
	testProxyServer(t, proxy[2].front, server.URL)
	testProxyServer(t, proxy4.front, server.URL)
}

func testProxyServer(t *testing.T, front string, target string) {
	ctx := context.Background()
	tunnel, err := client.CreateSingleUseGrpcTunnel(ctx, front, grpc.WithInsecure())
	if err != nil {
		t.Fatal(err)
	}

	c := &http.Client{
		Transport: &http.Transport{
			DialContext: tunnel.DialContext,
		},
		Timeout: 1 * time.Second,
	}

	r, err := c.Get(target)
	if err != nil {
		t.Fatal(err)
	}

	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		t.Error(err)
	}

	if string(data) != "hello" {
		t.Errorf("expect %v; got %v", "hello", string(data))
	}
}
<|endoftext|>


<commit_before>//
// Copyright (c) 2016 The heketi 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 sshexec

import (
	"strings"
	"testing"

	"github.com/heketi/heketi/executors"
	"github.com/heketi/heketi/pkg/utils"
	"github.com/heketi/tests"
)

func TestSshExecBrickCreate(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)
		tests.Assert(t, len(commands) == 6)

		for i, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch i {
			case 0:
				tests.Assert(t,
					cmd == "mkdir -p /var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 1:
				tests.Assert(t,
					cmd == "lvcreate --poolmetadatasize 5K "+
						"-c 256K -L 100K -T vg_xvgid/tp_id -V 10K -n brick_id", cmd)

			case 2:
				tests.Assert(t,
					cmd == "mkfs.xfs -i size=512 "+
						"-n size=8192 /dev/vg_xvgid/brick_id", cmd)

			case 3:
				tests.Assert(t,
					cmd == "echo \"/dev/vg_xvgid/brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id "+
						"xfs rw,inode64,noatime,nouuid 1 2\" | "+
						"tee -a /my/fstab > /dev/null", cmd)

			case 4:
				tests.Assert(t,
					cmd == "mount -o rw,inode64,noatime,nouuid "+
						"/dev/vg_xvgid/brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 5:
				tests.Assert(t,
					cmd == "mkdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id/brick", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	_, err = s.BrickCreate("myhost", b)
	tests.Assert(t, err == nil, err)

}

func TestSshExecBrickCreateSudo(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
			Sudo:  true,
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)
		tests.Assert(t, len(commands) == 6)
		tests.Assert(t, useSudo == true)

		for i, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch i {
			case 0:
				tests.Assert(t,
					cmd == "mkdir -p /var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 1:
				tests.Assert(t,
					cmd == "lvcreate --poolmetadatasize 5K "+
						"-c 256K -L 100K -T vg_xvgid/tp_id -V 10K -n brick_id", cmd)

			case 2:
				tests.Assert(t,
					cmd == "mkfs.xfs -i size=512 "+
						"-n size=8192 /dev/vg_xvgid/brick_id", cmd)

			case 3:
				tests.Assert(t,
					cmd == "echo \"/dev/vg_xvgid/brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id "+
						"xfs rw,inode64,noatime,nouuid 1 2\" | "+
						"tee -a /my/fstab > /dev/null", cmd)

			case 4:
				tests.Assert(t,
					cmd == "mount -o rw,inode64,noatime,nouuid "+
						"/dev/vg_xvgid/brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 5:
				tests.Assert(t,
					cmd == "mkdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id/brick", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	_, err = s.BrickCreate("myhost", b)
	tests.Assert(t, err == nil, err)

}

func TestSshExecBrickDestroy(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)

		for _, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch {
			case strings.Contains(cmd, "umount"):
				tests.Assert(t,
					cmd == "umount "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case strings.Contains(cmd, "lvremove"):
				tests.Assert(t,
					cmd == "lvremove -f vg_xvgid/tp_id", cmd)

			case strings.Contains(cmd, "rmdir"):
				tests.Assert(t,
					cmd == "rmdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case strings.Contains(cmd, "sed"):
				tests.Assert(t,
					cmd == "sed -i.save "+
						"\"/brick_id/d\" /my/fstab", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	err = s.BrickDestroy("myhost", b)
	tests.Assert(t, err == nil, err)
}
<commit_msg>Update unit tests with new /dev/mapper devnode<commit_after>//
// Copyright (c) 2016 The heketi 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 sshexec

import (
	"strings"
	"testing"

	"github.com/heketi/heketi/executors"
	"github.com/heketi/heketi/pkg/utils"
	"github.com/heketi/tests"
)

func TestSshExecBrickCreate(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)
		tests.Assert(t, len(commands) == 6)

		for i, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch i {
			case 0:
				tests.Assert(t,
					cmd == "mkdir -p /var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 1:
				tests.Assert(t,
					cmd == "lvcreate --poolmetadatasize 5K "+
						"-c 256K -L 100K -T vg_xvgid/tp_id -V 10K -n brick_id", cmd)

			case 2:
				tests.Assert(t,
					cmd == "mkfs.xfs -i size=512 "+
						"-n size=8192 /dev/mapper/vg_xvgid-brick_id", cmd)

			case 3:
				tests.Assert(t,
					cmd == "echo \"/dev/mapper/vg_xvgid-brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id "+
						"xfs rw,inode64,noatime,nouuid 1 2\" | "+
						"tee -a /my/fstab > /dev/null", cmd)

			case 4:
				tests.Assert(t,
					cmd == "mount -o rw,inode64,noatime,nouuid "+
						"/dev/mapper/vg_xvgid-brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 5:
				tests.Assert(t,
					cmd == "mkdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id/brick", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	_, err = s.BrickCreate("myhost", b)
	tests.Assert(t, err == nil, err)

}

func TestSshExecBrickCreateSudo(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
			Sudo:  true,
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)
		tests.Assert(t, len(commands) == 6)
		tests.Assert(t, useSudo == true)

		for i, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch i {
			case 0:
				tests.Assert(t,
					cmd == "mkdir -p /var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 1:
				tests.Assert(t,
					cmd == "lvcreate --poolmetadatasize 5K "+
						"-c 256K -L 100K -T vg_xvgid/tp_id -V 10K -n brick_id", cmd)

			case 2:
				tests.Assert(t,
					cmd == "mkfs.xfs -i size=512 "+
						"-n size=8192 /dev/mapper/vg_xvgid-brick_id", cmd)

			case 3:
				tests.Assert(t,
					cmd == "echo \"/dev/mapper/vg_xvgid-brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id "+
						"xfs rw,inode64,noatime,nouuid 1 2\" | "+
						"tee -a /my/fstab > /dev/null", cmd)

			case 4:
				tests.Assert(t,
					cmd == "mount -o rw,inode64,noatime,nouuid "+
						"/dev/mapper/vg_xvgid-brick_id "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case 5:
				tests.Assert(t,
					cmd == "mkdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id/brick", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	_, err = s.BrickCreate("myhost", b)
	tests.Assert(t, err == nil, err)

}

func TestSshExecBrickDestroy(t *testing.T) {

	f := NewFakeSsh()
	defer tests.Patch(&sshNew,
		func(logger *utils.Logger, user string, file string) (Ssher, error) {
			return f, nil
		}).Restore()

	config := &SshConfig{
		PrivateKeyFile: "xkeyfile",
		User:           "xuser",
		Port:           "100",
		CLICommandConfig: CLICommandConfig{
			Fstab: "/my/fstab",
		},
	}

	s, err := NewSshExecutor(config)
	tests.Assert(t, err == nil)
	tests.Assert(t, s != nil)

	// Create a Brick
	b := &executors.BrickRequest{
		VgId:             "xvgid",
		Name:             "id",
		TpSize:           100,
		Size:             10,
		PoolMetadataSize: 5,
	}

	// Mock ssh function
	f.FakeConnectAndExec = func(host string,
		commands []string,
		timeoutMinutes int,
		useSudo bool) ([]string, error) {

		tests.Assert(t, host == "myhost:100", host)

		for _, cmd := range commands {
			cmd = strings.Trim(cmd, " ")
			switch {
			case strings.Contains(cmd, "umount"):
				tests.Assert(t,
					cmd == "umount "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case strings.Contains(cmd, "lvremove"):
				tests.Assert(t,
					cmd == "lvremove -f vg_xvgid/tp_id", cmd)

			case strings.Contains(cmd, "rmdir"):
				tests.Assert(t,
					cmd == "rmdir "+
						"/var/lib/heketi/mounts/vg_xvgid/brick_id", cmd)

			case strings.Contains(cmd, "sed"):
				tests.Assert(t,
					cmd == "sed -i.save "+
						"\"/brick_id/d\" /my/fstab", cmd)
			}
		}

		return nil, nil
	}

	// Create Brick
	err = s.BrickDestroy("myhost", b)
	tests.Assert(t, err == nil, err)
}
<|endoftext|>


<commit_before>/*
 * This file is part of the kubevirt project
 *
 * 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.
 *
 * Copyright 2021 Red Hat, Inc.
 *
 */

package tests_test

import (
	"context"
	"fmt"
	"time"

	k8sv1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubevirtv1 "kubevirt.io/api/core/v1"

	cd "kubevirt.io/kubevirt/tests/containerdisk"
	"kubevirt.io/kubevirt/tests/libvmi"

	"kubevirt.io/kubevirt/tests/util"

	"kubevirt.io/client-go/kubecli"

	"kubevirt.io/kubevirt/tests"
	"kubevirt.io/kubevirt/tools/vms-generator/utils"
)

var _ = Describe("[sig-compute]VMI with external kernel boot", func() {

	var virtClient kubecli.KubevirtClient
	var err error

	BeforeEach(func() {
		virtClient, err = kubecli.GetKubevirtClient()
		Expect(err).ToNot(HaveOccurred())
	})

	Context("with external alpine-based kernel & initrd images", func() {
		It("[test_id:7748][QUARANTINE]ensure successful boot", func() {
			vmi := utils.GetVMIKernelBoot()
			obj, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(obj)
		})

		It("ensure successful boot and deletion when VMI has a disk defined", func() {
			By("Creating VMI with disk and kernel boot")
			vmi := tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskAlpine))
			vmi.Spec.Domain.Resources.Requests[k8sv1.ResourceMemory] = resource.MustParse("1Gi")
			utils.AddKernelBootToVMI(vmi)

			Expect(vmi.Spec.Volumes).ToNot(BeEmpty())
			Expect(vmi.Spec.Domain.Devices.Disks).ToNot(BeEmpty())

			By("Ensuring VMI can boot")
			vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(vmi)

			By("Fetching virt-launcher pod")
			virtLauncherPod := tests.GetRunningPodByVirtualMachineInstance(vmi, vmi.Namespace)

			By("Ensuring VMI is deleted")
			err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Delete(vmi.Name, &v1.DeleteOptions{})
			Expect(err).ToNot(HaveOccurred())
			Eventually(func() (isVmiDeleted bool) {
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Get(vmi.Name, &v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), "VMI Should be successfully deleted")

			By("Ensuring virt-launcher is deleted")
			Eventually(func() (isVmiDeleted bool) {
				_, err = virtClient.CoreV1().Pods(virtLauncherPod.Namespace).Get(context.Background(), virtLauncherPod.Name, v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), fmt.Sprintf("virt-launcher pod (%s) Should be successfully deleted", virtLauncherPod.Name))
		})
	})

	Context("with illegal definition ensure rejection of", func() {

		It("[test_id:7750]VMI defined without an image", func() {
			vmi := utils.GetVMIKernelBoot()
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.Image = ""
			_, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("denied the request: spec.domain.firmware.kernelBoot.container must be defined with an image"))
		})

		It("[test_id:7751]VMI defined with image but without initrd & kernel paths", func() {
			vmi := utils.GetVMIKernelBoot()
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.KernelPath = ""
			kernelBoot.Container.InitrdPath = ""
			_, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("denied the request: spec.domain.firmware.kernelBoot.container must be defined with at least one of the following: kernelPath, initrdPath"))
		})
	})

	Context("with external alpine-based kernel only (without initrd)", func() {
		getVMIKernelBoot := func() *kubevirtv1.VirtualMachineInstance {
			vmi := utils.GetVMIKernelBoot()
			// Remove initrd path from vmi spec
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.InitrdPath = ""
			return vmi
		}

		It("[QUARANTINE] ensure successful boot", func() {
			vmi := getVMIKernelBoot()

			obj, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(obj)
		})

		It("ensure successful boot and deletion when VMI has a disk defined", func() {
			By("Creating VMI with disk and kernel boot")
			vmi := libvmi.NewAlpine(libvmi.WithResourceMemory("1Gi"))

			utils.AddKernelBootToVMI(vmi)
			// Remove initrd path from vmi spec
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.InitrdPath = ""

			Expect(vmi.Spec.Volumes).ToNot(BeEmpty())
			Expect(vmi.Spec.Domain.Devices.Disks).ToNot(BeEmpty())

			By("Ensuring VMI can boot")
			vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(vmi)

			By("Fetching virt-launcher pod")
			virtLauncherPod := tests.GetRunningPodByVirtualMachineInstance(vmi, vmi.Namespace)

			By("Ensuring VMI is deleted")
			err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Delete(vmi.Name, &v1.DeleteOptions{})
			Expect(err).ToNot(HaveOccurred())
			Eventually(func() (isVmiDeleted bool) {
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Get(vmi.Name, &v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), "VMI Should be successfully deleted")

			By("Ensuring virt-launcher is deleted")
			Eventually(func() (isVmiDeleted bool) {
				_, err = virtClient.CoreV1().Pods(virtLauncherPod.Namespace).Get(context.Background(), virtLauncherPod.Name, v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), fmt.Sprintf("virt-launcher pod (%s) Should be successfully deleted", virtLauncherPod.Name))
		})
	})
})
<commit_msg>de-quarantine kernel boot tests<commit_after>/*
 * This file is part of the kubevirt project
 *
 * 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.
 *
 * Copyright 2021 Red Hat, Inc.
 *
 */

package tests_test

import (
	"context"
	"fmt"
	"time"

	k8sv1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubevirtv1 "kubevirt.io/api/core/v1"

	cd "kubevirt.io/kubevirt/tests/containerdisk"
	"kubevirt.io/kubevirt/tests/libvmi"

	"kubevirt.io/kubevirt/tests/util"

	"kubevirt.io/client-go/kubecli"

	"kubevirt.io/kubevirt/tests"
	"kubevirt.io/kubevirt/tools/vms-generator/utils"
)

var _ = Describe("[sig-compute]VMI with external kernel boot", func() {

	var virtClient kubecli.KubevirtClient
	var err error

	BeforeEach(func() {
		virtClient, err = kubecli.GetKubevirtClient()
		Expect(err).ToNot(HaveOccurred())
	})

	Context("with external alpine-based kernel & initrd images", func() {
		It("[test_id:7748]ensure successful boot", func() {
			vmi := utils.GetVMIKernelBoot()
			obj, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(obj)
		})

		It("ensure successful boot and deletion when VMI has a disk defined", func() {
			By("Creating VMI with disk and kernel boot")
			vmi := tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskAlpine))
			vmi.Spec.Domain.Resources.Requests[k8sv1.ResourceMemory] = resource.MustParse("1Gi")
			utils.AddKernelBootToVMI(vmi)

			Expect(vmi.Spec.Volumes).ToNot(BeEmpty())
			Expect(vmi.Spec.Domain.Devices.Disks).ToNot(BeEmpty())

			By("Ensuring VMI can boot")
			vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(vmi)

			By("Fetching virt-launcher pod")
			virtLauncherPod := tests.GetRunningPodByVirtualMachineInstance(vmi, vmi.Namespace)

			By("Ensuring VMI is deleted")
			err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Delete(vmi.Name, &v1.DeleteOptions{})
			Expect(err).ToNot(HaveOccurred())
			Eventually(func() (isVmiDeleted bool) {
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Get(vmi.Name, &v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), "VMI Should be successfully deleted")

			By("Ensuring virt-launcher is deleted")
			Eventually(func() (isVmiDeleted bool) {
				_, err = virtClient.CoreV1().Pods(virtLauncherPod.Namespace).Get(context.Background(), virtLauncherPod.Name, v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), fmt.Sprintf("virt-launcher pod (%s) Should be successfully deleted", virtLauncherPod.Name))
		})
	})

	Context("with illegal definition ensure rejection of", func() {

		It("[test_id:7750]VMI defined without an image", func() {
			vmi := utils.GetVMIKernelBoot()
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.Image = ""
			_, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("denied the request: spec.domain.firmware.kernelBoot.container must be defined with an image"))
		})

		It("[test_id:7751]VMI defined with image but without initrd & kernel paths", func() {
			vmi := utils.GetVMIKernelBoot()
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.KernelPath = ""
			kernelBoot.Container.InitrdPath = ""
			_, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("denied the request: spec.domain.firmware.kernelBoot.container must be defined with at least one of the following: kernelPath, initrdPath"))
		})
	})

	Context("with external alpine-based kernel only (without initrd)", func() {
		getVMIKernelBoot := func() *kubevirtv1.VirtualMachineInstance {
			vmi := utils.GetVMIKernelBoot()
			// Remove initrd path from vmi spec
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.InitrdPath = ""
			return vmi
		}

		It("ensure successful boot", func() {
			vmi := getVMIKernelBoot()

			obj, err := virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(obj)
		})

		It("ensure successful boot and deletion when VMI has a disk defined", func() {
			By("Creating VMI with disk and kernel boot")
			vmi := libvmi.NewAlpine(libvmi.WithResourceMemory("1Gi"))

			utils.AddKernelBootToVMI(vmi)
			// Remove initrd path from vmi spec
			kernelBoot := vmi.Spec.Domain.Firmware.KernelBoot
			kernelBoot.Container.InitrdPath = ""

			Expect(vmi.Spec.Volumes).ToNot(BeEmpty())
			Expect(vmi.Spec.Domain.Devices.Disks).ToNot(BeEmpty())

			By("Ensuring VMI can boot")
			vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Create(vmi)
			Expect(err).ToNot(HaveOccurred())
			tests.WaitForSuccessfulVMIStart(vmi)

			By("Fetching virt-launcher pod")
			virtLauncherPod := tests.GetRunningPodByVirtualMachineInstance(vmi, vmi.Namespace)

			By("Ensuring VMI is deleted")
			err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Delete(vmi.Name, &v1.DeleteOptions{})
			Expect(err).ToNot(HaveOccurred())
			Eventually(func() (isVmiDeleted bool) {
				vmi, err = virtClient.VirtualMachineInstance(util.NamespaceTestDefault).Get(vmi.Name, &v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), "VMI Should be successfully deleted")

			By("Ensuring virt-launcher is deleted")
			Eventually(func() (isVmiDeleted bool) {
				_, err = virtClient.CoreV1().Pods(virtLauncherPod.Namespace).Get(context.Background(), virtLauncherPod.Name, v1.GetOptions{})
				if errors.IsNotFound(err) {
					return true
				}
				Expect(err).ToNot(HaveOccurred())
				return false
			}, 60*time.Second, 3*time.Second).Should(BeTrue(), fmt.Sprintf("virt-launcher pod (%s) Should be successfully deleted", virtLauncherPod.Name))
		})
	})
})
<|endoftext|>