<commit_before>// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package pally

import (
	"context"
	"strconv"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/uber-go/tally"
	"github.com/uber-go/tally/m3"
	"go.uber.org/atomic"
)

func TestSimpleMetricDuplicates(t *testing.T) {
	r := NewRegistry()
	opts := Opts{
		Name: "foo",
		Help: "help",
	}
	_, err := r.NewCounter(opts)
	assert.NoError(t, err, "Unexpected error registering metadata for the first time.")

	t.Run("same type", func(t *testing.T) {
		// You can't reuse options with the same metric type.
		_, err := r.NewCounter(opts)
		assert.Error(t, err, "Unexpected success re-using metrics metadata.")
		assert.Panics(t, func() { r.MustCounter(opts) }, "Unexpected success re-using metrics metadata.")
	})

	t.Run("different type", func(t *testing.T) {
		// Even if you change the metric type, you still can't re-use metadata.
		_, err := r.NewGauge(opts)
		assert.Error(t, err, "Unexpected success re-using metrics metadata.")
		assert.Panics(t, func() { r.MustGauge(opts) }, "Unexpected success re-using metrics metadata.")
	})
}

func TestVectorMetricDuplicates(t *testing.T) {
	r := NewRegistry()
	opts := Opts{
		Name:           "foo",
		Help:           "help",
		VariableLabels: []string{"foo"},
	}
	_, err := r.NewCounterVector(opts)
	assert.NoError(t, err, "Unexpected error registering vector metadata for the first time.")

	t.Run("same type", func(t *testing.T) {
		// You can't reuse options with the same metric type.
		_, err := r.NewCounterVector(opts)
		assert.Error(t, err, "Unexpected success re-using vector metrics metadata.")
		assert.Panics(t, func() { r.MustCounterVector(opts) }, "Unexpected success re-using vector metrics metadata.")
	})

	t.Run("different type", func(t *testing.T) {
		// Even if you change the metric type, you still can't re-use metadata.
		_, err := r.NewGaugeVector(opts)
		assert.Error(t, err, "Unexpected success re-using vector metrics metadata.")
		assert.Panics(t, func() { r.MustGaugeVector(opts) }, "Unexpected success re-using vector metrics metadata.")
	})
}

func TestFederatedMetrics(t *testing.T) {
	prom := prometheus.NewRegistry()
	r := NewRegistry(Federated(prom))
	opts := Opts{
		Name: "foo",
		Help: "Some help.",
	}
	c, err := r.NewCounter(opts)
	assert.NoError(t, err, "Unexpected error registering vector metadata for the first time.")

	c.Inc()
	expected := "# HELP foo Some help.\n" +
		"# TYPE foo counter\n" +
		"foo 1"

	assertPrometheusText(t, promhttp.HandlerFor(prom, promhttp.HandlerOpts{}), expected)
}

func TestConstLabelValidation(t *testing.T) {
	r := NewRegistry(Labeled(Labels{
		"invalid-prometheus-name": "foo",
		"tally":                   "invalid.value",
		"ok":                      "yes",
	}))
	_, err := r.NewCounter(Opts{
		Name: "test",
		Help: "help",
	})
	require.NoError(t, err, "Unexpected error creating a counter.")
	assertPrometheusText(t, r, "# HELP test help\n"+
		"# TYPE test counter\n"+
		`test{ok="yes"} 0`)
}

func BenchmarkCreateNewMetrics(b *testing.B) {
	b.Run("create pally counter", func(b *testing.B) {
		r := NewRegistry()
		var count atomic.Int64
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				opts := Opts{
					Name:        "foo",
					Help:        "Some help.",
					ConstLabels: Labels{"iteration": strconv.FormatInt(count.Inc(), 10)},
				}
				r.NewCounter(opts)
			}
		})
	})
	b.Run("create tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		var count atomic.Int64
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				tags := map[string]string{"iteration": strconv.FormatInt(count.Inc(), 10)}
				scope.Tagged(tags).Counter("foo")
			}
		})
	})
	b.Run("create dynamic pally counter", func(b *testing.B) {
		vec := NewRegistry().MustCounterVector(Opts{
			Name:           "foo",
			Help:           "Some help.",
			VariableLabels: []string{"foo", "bar"},
		})
		var count atomic.Int64
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				foo := strconv.FormatInt(count.Inc(), 10)
				bar := strconv.FormatInt(count.Inc(), 10)
				vec.MustGet(foo, bar)
			}
		})
	})
	b.Run("create dynamic tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		var count atomic.Int64
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				foo := strconv.FormatInt(count.Inc(), 10)
				bar := strconv.FormatInt(count.Inc(), 10)
				scope.Tagged(map[string]string{"foo": foo, "bar": bar}).Counter("foo")
			}
		})
	})
	b.Run("increment pally counter", func(b *testing.B) {
		c := NewRegistry().MustCounter(Opts{
			Name: "foo",
			Help: "Some help.",
		})
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c.Inc()
			}
		})
	})
	b.Run("increment tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		c := scope.Counter("foo")
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c.Inc(1)
			}
		})
	})
	b.Run("increment dynamic pally counter", func(b *testing.B) {
		vec := NewRegistry().MustCounterVector(Opts{
			Name:           "foo",
			Help:           "Some help.",
			VariableLabels: []string{"foo", "bar"},
		})
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				vec.MustGet("one", "two").Inc()
			}
		})
	})
	b.Run("increment dynamic tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				scope.Tagged(map[string]string{"foo": "one", "bar": "two"}).Counter("foo").Inc(1)
			}
		})
	})
}

// Create a real, M3-backed Tally scope.
func newTallyScope(t testing.TB) (tally.Scope, context.CancelFunc) {
	reporter, err := m3.NewReporter(m3.Options{
		HostPorts: []string{"localhost:1234"},
		Service:   "benchmark",
		Env:       "production",
	})
	require.NoError(t, err, "Failed to construct an M3 reporter.")
	scope, close := tally.NewRootScope(
		tally.ScopeOptions{CachedReporter: reporter},
		time.Second,
	)
	return scope, func() { close.Close() }
}
<commit_msg>Add benchmarks against Prometheus<commit_after>// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package pally

import (
	"context"
	"strconv"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/uber-go/tally"
	"github.com/uber-go/tally/m3"
	"go.uber.org/atomic"
)

func TestSimpleMetricDuplicates(t *testing.T) {
	r := NewRegistry()
	opts := Opts{
		Name: "foo",
		Help: "help",
	}
	_, err := r.NewCounter(opts)
	assert.NoError(t, err, "Unexpected error registering metadata for the first time.")

	t.Run("same type", func(t *testing.T) {
		// You can't reuse options with the same metric type.
		_, err := r.NewCounter(opts)
		assert.Error(t, err, "Unexpected success re-using metrics metadata.")
		assert.Panics(t, func() { r.MustCounter(opts) }, "Unexpected success re-using metrics metadata.")
	})

	t.Run("different type", func(t *testing.T) {
		// Even if you change the metric type, you still can't re-use metadata.
		_, err := r.NewGauge(opts)
		assert.Error(t, err, "Unexpected success re-using metrics metadata.")
		assert.Panics(t, func() { r.MustGauge(opts) }, "Unexpected success re-using metrics metadata.")
	})
}

func TestVectorMetricDuplicates(t *testing.T) {
	r := NewRegistry()
	opts := Opts{
		Name:           "foo",
		Help:           "help",
		VariableLabels: []string{"foo"},
	}
	_, err := r.NewCounterVector(opts)
	assert.NoError(t, err, "Unexpected error registering vector metadata for the first time.")

	t.Run("same type", func(t *testing.T) {
		// You can't reuse options with the same metric type.
		_, err := r.NewCounterVector(opts)
		assert.Error(t, err, "Unexpected success re-using vector metrics metadata.")
		assert.Panics(t, func() { r.MustCounterVector(opts) }, "Unexpected success re-using vector metrics metadata.")
	})

	t.Run("different type", func(t *testing.T) {
		// Even if you change the metric type, you still can't re-use metadata.
		_, err := r.NewGaugeVector(opts)
		assert.Error(t, err, "Unexpected success re-using vector metrics metadata.")
		assert.Panics(t, func() { r.MustGaugeVector(opts) }, "Unexpected success re-using vector metrics metadata.")
	})
}

func TestFederatedMetrics(t *testing.T) {
	prom := prometheus.NewRegistry()
	r := NewRegistry(Federated(prom))
	opts := Opts{
		Name: "foo",
		Help: "Some help.",
	}
	c, err := r.NewCounter(opts)
	assert.NoError(t, err, "Unexpected error registering vector metadata for the first time.")

	c.Inc()
	expected := "# HELP foo Some help.\n" +
		"# TYPE foo counter\n" +
		"foo 1"

	assertPrometheusText(t, promhttp.HandlerFor(prom, promhttp.HandlerOpts{}), expected)
}

func TestConstLabelValidation(t *testing.T) {
	r := NewRegistry(Labeled(Labels{
		"invalid-prometheus-name": "foo",
		"tally":                   "invalid.value",
		"ok":                      "yes",
	}))
	_, err := r.NewCounter(Opts{
		Name: "test",
		Help: "help",
	})
	require.NoError(t, err, "Unexpected error creating a counter.")
	assertPrometheusText(t, r, "# HELP test help\n"+
		"# TYPE test counter\n"+
		`test{ok="yes"} 0`)
}

func BenchmarkCreateNewMetrics(b *testing.B) {
	b.Run("create Pally counter", func(b *testing.B) {
		r := NewRegistry()
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				opts := Opts{
					Name:        "foo",
					Help:        "Some help.",
					ConstLabels: Labels{"iteration": strconv.FormatInt(count.Inc(), 10)},
				}
				r.NewCounter(opts)
			}
		})
	})
	b.Run("create Prometheus counter", func(b *testing.B) {
		r := prometheus.NewRegistry()
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c := prometheus.NewCounter(prometheus.CounterOpts{
					Name:        "foo",
					Help:        "Some help.",
					ConstLabels: prometheus.Labels{"iteration": strconv.FormatInt(count.Inc(), 10)},
				})
				r.MustRegister(c)
			}
		})
	})
	b.Run("create Tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				tags := map[string]string{"iteration": strconv.FormatInt(count.Inc(), 10)}
				scope.Tagged(tags).Counter("foo")
			}
		})
	})
	b.Run("create dynamic Pally counter", func(b *testing.B) {
		vec := NewRegistry().MustCounterVector(Opts{
			Name:           "foo",
			Help:           "Some help.",
			VariableLabels: []string{"foo", "bar"},
		})
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				foo := strconv.FormatInt(count.Inc(), 10)
				bar := strconv.FormatInt(count.Inc(), 10)
				vec.MustGet(foo, bar)
			}
		})
	})
	b.Run("create dynamic Prometheus counter", func(b *testing.B) {
		r := prometheus.NewRegistry()
		vec := prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: "foo",
			Help: "Some help.",
		}, []string{"foo", "bar"})
		r.MustRegister(vec)
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				foo := strconv.FormatInt(count.Inc(), 10)
				bar := strconv.FormatInt(count.Inc(), 10)
				vec.WithLabelValues(foo, bar)
			}
		})
	})
	b.Run("create dynamic Tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		var count atomic.Int64
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				foo := strconv.FormatInt(count.Inc(), 10)
				bar := strconv.FormatInt(count.Inc(), 10)
				scope.Tagged(map[string]string{"foo": foo, "bar": bar}).Counter("foo")
			}
		})
	})
	b.Run("increment Pally counter", func(b *testing.B) {
		c := NewRegistry().MustCounter(Opts{
			Name: "foo",
			Help: "Some help.",
		})
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c.Inc()
			}
		})
	})
	b.Run("increment Prometheus counter", func(b *testing.B) {
		r := prometheus.NewRegistry()
		c := prometheus.NewCounter(prometheus.CounterOpts{
			Name: "foo",
			Help: "Some help.",
		})
		r.MustRegister(c)
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c.Inc()
			}
		})
	})
	b.Run("increment Tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		c := scope.Counter("foo")
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				c.Inc(1)
			}
		})
	})
	b.Run("increment dynamic Pally counter", func(b *testing.B) {
		vec := NewRegistry().MustCounterVector(Opts{
			Name:           "foo",
			Help:           "Some help.",
			VariableLabels: []string{"foo", "bar"},
		})
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				vec.MustGet("one", "two").Inc()
			}
		})
	})
	b.Run("increment dynamic Prometheus counter", func(b *testing.B) {
		r := prometheus.NewRegistry()
		vec := prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: "foo",
			Help: "Some help.",
		}, []string{"foo", "bar"})
		r.MustRegister(vec)
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				vec.WithLabelValues("one", "two").Inc()
			}
		})
	})
	b.Run("increment dynamic Tally counter", func(b *testing.B) {
		scope, close := newTallyScope(b)
		defer close()
		b.ResetTimer()
		b.RunParallel(func(pb *testing.PB) {
			for pb.Next() {
				scope.Tagged(map[string]string{"foo": "one", "bar": "two"}).Counter("foo").Inc(1)
			}
		})
	})
}

// Create a real, M3-backed Tally scope.
func newTallyScope(t testing.TB) (tally.Scope, context.CancelFunc) {
	reporter, err := m3.NewReporter(m3.Options{
		HostPorts: []string{"localhost:1234"},
		Service:   "benchmark",
		Env:       "production",
	})
	require.NoError(t, err, "Failed to construct an M3 reporter.")
	scope, close := tally.NewRootScope(
		tally.ScopeOptions{CachedReporter: reporter},
		time.Second,
	)
	return scope, func() { close.Close() }
}
<|endoftext|>


<commit_before>package interfaces

import (
	"fmt"
	"strconv"
	"time"

	"github.com/marcusolsson/goddd/application"
	"github.com/marcusolsson/goddd/domain/cargo"
	"github.com/marcusolsson/goddd/domain/location"
	"github.com/marcusolsson/goddd/domain/voyage"
)

type cargoDTO struct {
	TrackingID           string     `json:"trackingId"`
	StatusText           string     `json:"statusText"`
	Origin               string     `json:"origin"`
	Destination          string     `json:"destination"`
	ETA                  string     `json:"eta"`
	NextExpectedActivity string     `json:"nextExpectedActivity"`
	Misrouted            bool       `json:"misrouted"`
	Routed               bool       `json:"routed"`
	ArrivalDeadline      string     `json:"arrivalDeadline"`
	Events               []eventDTO `json:"events"`
	Legs                 []legDTO   `json:"legs,omitempty"`
}

type locationDTO struct {
	UNLocode string `json:"locode"`
	Name     string `json:"name"`
}

type RouteCandidateDTO struct {
	Legs []legDTO `json:"legs"`
}

type legDTO struct {
	VoyageNumber string    `json:"voyageNumber"`
	From         string    `json:"from"`
	To           string    `json:"to"`
	LoadTime     time.Time `json:"loadTime"`
	UnloadTime   time.Time `json:"unloadTime"`
}

type eventDTO struct {
	Description string `json:"description"`
	Expected    bool   `json:"expected"`
}

type BookingServiceFacade interface {
	BookNewCargo(origin, destination string, arrivalDeadline string) (string, error)
	LoadCargoForRouting(trackingID string) (cargoDTO, error)
	AssignCargoToRoute(trackingID string, candidate RouteCandidateDTO) error
	ChangeDestination(trackingID string, destinationUNLocode string) error
	RequestRoutesForCargo(trackingID string) []RouteCandidateDTO
	ListShippingLocations() []locationDTO
	ListAllCargos() []cargoDTO
}

type bookingServiceFacade struct {
	cargoRepository         cargo.Repository
	locationRepository      location.Repository
	handlingEventRepository cargo.HandlingEventRepository
	bookingService          application.BookingService
}

func (f *bookingServiceFacade) BookNewCargo(origin, destination string, arrivalDeadline string) (string, error) {
	millis, _ := strconv.ParseInt(fmt.Sprintf("%s", arrivalDeadline), 10, 64)
	trackingID, err := f.bookingService.BookNewCargo(location.UNLocode(origin), location.UNLocode(destination), time.Unix(millis/1000, 0))

	return string(trackingID), err
}

func (f *bookingServiceFacade) LoadCargoForRouting(trackingID string) (cargoDTO, error) {
	c, err := f.cargoRepository.Find(cargo.TrackingID(trackingID))

	if err != nil {
		return cargoDTO{}, err
	}

	return assemble(c, f.handlingEventRepository), nil
}

func (f *bookingServiceFacade) AssignCargoToRoute(trackingID string, candidate RouteCandidateDTO) error {
	var legs []cargo.Leg
	for _, l := range candidate.Legs {
		legs = append(legs, cargo.Leg{
			VoyageNumber:   voyage.Number(l.VoyageNumber),
			LoadLocation:   location.UNLocode(l.From),
			UnloadLocation: location.UNLocode(l.To),
		})
	}

	return f.bookingService.AssignCargoToRoute(cargo.Itinerary{Legs: legs}, cargo.TrackingID(trackingID))
}

func (f *bookingServiceFacade) ChangeDestination(trackingID string, destinationUNLocode string) error {
	return f.bookingService.ChangeDestination(cargo.TrackingID(trackingID), location.UNLocode(destinationUNLocode))
}

func (f *bookingServiceFacade) RequestRoutesForCargo(trackingID string) []RouteCandidateDTO {
	itineraries := f.bookingService.RequestPossibleRoutesForCargo(cargo.TrackingID(trackingID))

	var candidates []RouteCandidateDTO
	for _, itin := range itineraries {
		var legs []legDTO
		for _, leg := range itin.Legs {
			legs = append(legs, legDTO{
				VoyageNumber: string(leg.VoyageNumber),
				From:         string(leg.LoadLocation),
				To:           string(leg.UnloadLocation),
				LoadTime:     leg.LoadTime,
				UnloadTime:   leg.UnloadTime,
			})
		}
		candidates = append(candidates, RouteCandidateDTO{Legs: legs})
	}

	return candidates
}

func (f *bookingServiceFacade) ListShippingLocations() []locationDTO {
	locations := f.locationRepository.FindAll()

	dtos := make([]locationDTO, len(locations))
	for i, loc := range locations {
		dtos[i] = locationDTO{
			UNLocode: string(loc.UNLocode),
			Name:     loc.Name,
		}
	}

	return dtos
}

func (f *bookingServiceFacade) ListAllCargos() []cargoDTO {
	cargos := f.cargoRepository.FindAll()
	dtos := make([]cargoDTO, len(cargos))

	for i, c := range cargos {
		dtos[i] = assemble(c, f.handlingEventRepository)
	}

	return dtos
}

func NewBookingServiceFacade(cargoRepository cargo.Repository, locationRepository location.Repository, handlingEventRepository cargo.HandlingEventRepository, bookingService application.BookingService) BookingServiceFacade {
	return &bookingServiceFacade{cargoRepository, locationRepository, handlingEventRepository, bookingService}
}

func assemble(c cargo.Cargo, her cargo.HandlingEventRepository) cargoDTO {
	return cargoDTO{
		TrackingID:           string(c.TrackingID),
		Origin:               string(c.Origin),
		Destination:          string(c.RouteSpecification.Destination),
		ETA:                  c.Delivery.ETA.Format(time.RFC3339),
		NextExpectedActivity: "",
		Misrouted:            c.Delivery.RoutingStatus == cargo.Misrouted,
		Routed:               !c.Itinerary.IsEmpty(),
		ArrivalDeadline:      c.RouteSpecification.ArrivalDeadline.Format(time.RFC3339),
		StatusText:           assembleStatusText(c),
		Legs:                 assembleLegs(c),
		Events:               assembleEvents(c, her),
	}
}

func assembleStatusText(c cargo.Cargo) string {
	switch c.Delivery.TransportStatus {
	case cargo.NotReceived:
		return "Not received"
	case cargo.InPort:
		return fmt.Sprintf("In port %s", c.Delivery.LastKnownLocation)
	case cargo.OnboardCarrier:
		return fmt.Sprintf("Onboard voyage %s", c.Delivery.CurrentVoyage)
	case cargo.Claimed:
		return "Claimed"
	default:
		return "Unknown"
	}
}

func assembleLegs(c cargo.Cargo) []legDTO {
	var legs []legDTO
	for _, l := range c.Itinerary.Legs {
		legs = append(legs, legDTO{
			VoyageNumber: string(l.VoyageNumber),
			From:         string(l.LoadLocation),
			To:           string(l.UnloadLocation),
		})
	}
	return legs
}

func assembleEvents(c cargo.Cargo, r cargo.HandlingEventRepository) []eventDTO {
	h := r.QueryHandlingHistory(c.TrackingID)
	events := make([]eventDTO, len(h.HandlingEvents))
	for i, e := range h.HandlingEvents {
		var description string

		switch e.Activity.Type {
		case cargo.NotHandled:
			description = "Cargo has not yet been received."
		case cargo.Receive:
			description = fmt.Sprintf("Received in %s, at %s", e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Load:
			description = fmt.Sprintf("Loaded onto voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Unload:
			description = fmt.Sprintf("Unloaded off voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Claim:
			description = fmt.Sprintf("Claimed in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Customs:
			description = fmt.Sprintf("Cleared customs in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
		default:
			description = "[Unknown status]"
		}

		events[i] = eventDTO{Description: description, Expected: c.Itinerary.IsExpected(e)}
	}

	return events
}
<commit_msg>Handle time fields better.<commit_after>package interfaces

import (
	"fmt"
	"strconv"
	"time"

	"github.com/marcusolsson/goddd/application"
	"github.com/marcusolsson/goddd/domain/cargo"
	"github.com/marcusolsson/goddd/domain/location"
	"github.com/marcusolsson/goddd/domain/voyage"
)

type cargoDTO struct {
	TrackingID           string     `json:"trackingId"`
	StatusText           string     `json:"statusText"`
	Origin               string     `json:"origin"`
	Destination          string     `json:"destination"`
	ETA                  time.Time  `json:"eta"`
	NextExpectedActivity string     `json:"nextExpectedActivity"`
	Misrouted            bool       `json:"misrouted"`
	Routed               bool       `json:"routed"`
	ArrivalDeadline      time.Time  `json:"arrivalDeadline"`
	Events               []eventDTO `json:"events"`
	Legs                 []legDTO   `json:"legs,omitempty"`
}

type locationDTO struct {
	UNLocode string `json:"locode"`
	Name     string `json:"name"`
}

type RouteCandidateDTO struct {
	Legs []legDTO `json:"legs"`
}

type legDTO struct {
	VoyageNumber string    `json:"voyageNumber"`
	From         string    `json:"from"`
	To           string    `json:"to"`
	LoadTime     time.Time `json:"loadTime"`
	UnloadTime   time.Time `json:"unloadTime"`
}

type eventDTO struct {
	Description string `json:"description"`
	Expected    bool   `json:"expected"`
}

type BookingServiceFacade interface {
	BookNewCargo(origin, destination string, arrivalDeadline string) (string, error)
	LoadCargoForRouting(trackingID string) (cargoDTO, error)
	AssignCargoToRoute(trackingID string, candidate RouteCandidateDTO) error
	ChangeDestination(trackingID string, destinationUNLocode string) error
	RequestRoutesForCargo(trackingID string) []RouteCandidateDTO
	ListShippingLocations() []locationDTO
	ListAllCargos() []cargoDTO
}

type bookingServiceFacade struct {
	cargoRepository         cargo.Repository
	locationRepository      location.Repository
	handlingEventRepository cargo.HandlingEventRepository
	bookingService          application.BookingService
}

func (f *bookingServiceFacade) BookNewCargo(origin, destination string, arrivalDeadline string) (string, error) {
	millis, _ := strconv.ParseInt(fmt.Sprintf("%s", arrivalDeadline), 10, 64)
	trackingID, err := f.bookingService.BookNewCargo(location.UNLocode(origin), location.UNLocode(destination), time.Unix(millis/1000, 0))

	return string(trackingID), err
}

func (f *bookingServiceFacade) LoadCargoForRouting(trackingID string) (cargoDTO, error) {
	c, err := f.cargoRepository.Find(cargo.TrackingID(trackingID))

	if err != nil {
		return cargoDTO{}, err
	}

	return assemble(c, f.handlingEventRepository), nil
}

func (f *bookingServiceFacade) AssignCargoToRoute(trackingID string, candidate RouteCandidateDTO) error {
	var legs []cargo.Leg
	for _, l := range candidate.Legs {
		legs = append(legs, cargo.Leg{
			VoyageNumber:   voyage.Number(l.VoyageNumber),
			LoadLocation:   location.UNLocode(l.From),
			UnloadLocation: location.UNLocode(l.To),
		})
	}

	return f.bookingService.AssignCargoToRoute(cargo.Itinerary{Legs: legs}, cargo.TrackingID(trackingID))
}

func (f *bookingServiceFacade) ChangeDestination(trackingID string, destinationUNLocode string) error {
	return f.bookingService.ChangeDestination(cargo.TrackingID(trackingID), location.UNLocode(destinationUNLocode))
}

func (f *bookingServiceFacade) RequestRoutesForCargo(trackingID string) []RouteCandidateDTO {
	itineraries := f.bookingService.RequestPossibleRoutesForCargo(cargo.TrackingID(trackingID))

	var candidates []RouteCandidateDTO
	for _, itin := range itineraries {
		var legs []legDTO
		for _, leg := range itin.Legs {
			legs = append(legs, legDTO{
				VoyageNumber: string(leg.VoyageNumber),
				From:         string(leg.LoadLocation),
				To:           string(leg.UnloadLocation),
				LoadTime:     leg.LoadTime,
				UnloadTime:   leg.UnloadTime,
			})
		}
		candidates = append(candidates, RouteCandidateDTO{Legs: legs})
	}

	return candidates
}

func (f *bookingServiceFacade) ListShippingLocations() []locationDTO {
	locations := f.locationRepository.FindAll()

	dtos := make([]locationDTO, len(locations))
	for i, loc := range locations {
		dtos[i] = locationDTO{
			UNLocode: string(loc.UNLocode),
			Name:     loc.Name,
		}
	}

	return dtos
}

func (f *bookingServiceFacade) ListAllCargos() []cargoDTO {
	cargos := f.cargoRepository.FindAll()
	dtos := make([]cargoDTO, len(cargos))

	for i, c := range cargos {
		dtos[i] = assemble(c, f.handlingEventRepository)
	}

	return dtos
}

func NewBookingServiceFacade(cargoRepository cargo.Repository, locationRepository location.Repository, handlingEventRepository cargo.HandlingEventRepository, bookingService application.BookingService) BookingServiceFacade {
	return &bookingServiceFacade{cargoRepository, locationRepository, handlingEventRepository, bookingService}
}

func assemble(c cargo.Cargo, her cargo.HandlingEventRepository) cargoDTO {
	return cargoDTO{
		TrackingID:           string(c.TrackingID),
		Origin:               string(c.Origin),
		Destination:          string(c.RouteSpecification.Destination),
		ETA:                  c.Delivery.ETA,
		NextExpectedActivity: "",
		Misrouted:            c.Delivery.RoutingStatus == cargo.Misrouted,
		Routed:               !c.Itinerary.IsEmpty(),
		ArrivalDeadline:      c.RouteSpecification.ArrivalDeadline,
		StatusText:           assembleStatusText(c),
		Legs:                 assembleLegs(c),
		Events:               assembleEvents(c, her),
	}
}

func assembleStatusText(c cargo.Cargo) string {
	switch c.Delivery.TransportStatus {
	case cargo.NotReceived:
		return "Not received"
	case cargo.InPort:
		return fmt.Sprintf("In port %s", c.Delivery.LastKnownLocation)
	case cargo.OnboardCarrier:
		return fmt.Sprintf("Onboard voyage %s", c.Delivery.CurrentVoyage)
	case cargo.Claimed:
		return "Claimed"
	default:
		return "Unknown"
	}
}

func assembleLegs(c cargo.Cargo) []legDTO {
	var legs []legDTO
	for _, l := range c.Itinerary.Legs {
		legs = append(legs, legDTO{
			VoyageNumber: string(l.VoyageNumber),
			From:         string(l.LoadLocation),
			To:           string(l.UnloadLocation),
			LoadTime:     l.LoadTime,
			UnloadTime:   l.UnloadTime,
		})
	}
	return legs
}

func assembleEvents(c cargo.Cargo, r cargo.HandlingEventRepository) []eventDTO {
	h := r.QueryHandlingHistory(c.TrackingID)
	events := make([]eventDTO, len(h.HandlingEvents))
	for i, e := range h.HandlingEvents {
		var description string

		switch e.Activity.Type {
		case cargo.NotHandled:
			description = "Cargo has not yet been received."
		case cargo.Receive:
			description = fmt.Sprintf("Received in %s, at %s", e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Load:
			description = fmt.Sprintf("Loaded onto voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Unload:
			description = fmt.Sprintf("Unloaded off voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Claim:
			description = fmt.Sprintf("Claimed in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
		case cargo.Customs:
			description = fmt.Sprintf("Cleared customs in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
		default:
			description = "[Unknown status]"
		}

		events[i] = eventDTO{Description: description, Expected: c.Itinerary.IsExpected(e)}
	}

	return events
}
<|endoftext|>


<commit_before>// Copyright 2019 The Ebiten 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 driver

import (
	"errors"
	"image"
	"time"
)

// IsPlayground indicates whether the current environment is the Go Playground (play.golang.org) or not.
// The fixed time is explicitly defined. See "About the Playground" at play.golang.org.
var IsPlayground = time.Now().UnixNano() == 1257894000000000000

type UIContext interface {
	SetSize(width, height int, scale float64)
	Update(afterFrameUpdate func()) error
	SuspendAudio()
	ResumeAudio()
}

// RegularTermination represents a regular termination.
// Run can return this error, and if this error is received,
// the game loop should be terminated as soon as possible.
var RegularTermination = errors.New("regular termination")

type UI interface {
	DeviceScaleFactor() float64
	IsCursorVisible() bool
	IsFullscreen() bool
	IsRunnableInBackground() bool
	IsVsyncEnabled() bool
	IsWindowDecorated() bool
	IsWindowResizable() bool
	Run(width, height int, scale float64, title string, context UIContext, graphics Graphics) error
	RunWithoutMainLoop(width, height int, scale float64, title string, context UIContext, graphics Graphics) <-chan error
	ScreenPadding() (x0, y0, x1, y1 float64)
	ScreenScale() float64
	ScreenSizeInFullscreen() (int, int)
	SetCursorVisible(visible bool)
	SetFullscreen(fullscreen bool)
	SetRunnableInBackground(runnableInBackground bool)
	SetScreenScale(scale float64)
	SetScreenSize(width, height int)
	SetVsyncEnabled(enabled bool)
	SetWindowDecorated(decorated bool)
	SetWindowIcon(iconImages []image.Image)
	SetWindowResizable(resizable bool)
	SetWindowTitle(title string)
	Input() Input
}
<commit_msg>driver: Remove IsPlayground<commit_after>// Copyright 2019 The Ebiten 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 driver

import (
	"errors"
	"image"
	"time"
)

type UIContext interface {
	SetSize(width, height int, scale float64)
	Update(afterFrameUpdate func()) error
	SuspendAudio()
	ResumeAudio()
}

// RegularTermination represents a regular termination.
// Run can return this error, and if this error is received,
// the game loop should be terminated as soon as possible.
var RegularTermination = errors.New("regular termination")

type UI interface {
	DeviceScaleFactor() float64
	IsCursorVisible() bool
	IsFullscreen() bool
	IsRunnableInBackground() bool
	IsVsyncEnabled() bool
	IsWindowDecorated() bool
	IsWindowResizable() bool
	Run(width, height int, scale float64, title string, context UIContext, graphics Graphics) error
	RunWithoutMainLoop(width, height int, scale float64, title string, context UIContext, graphics Graphics) <-chan error
	ScreenPadding() (x0, y0, x1, y1 float64)
	ScreenScale() float64
	ScreenSizeInFullscreen() (int, int)
	SetCursorVisible(visible bool)
	SetFullscreen(fullscreen bool)
	SetRunnableInBackground(runnableInBackground bool)
	SetScreenScale(scale float64)
	SetScreenSize(width, height int)
	SetVsyncEnabled(enabled bool)
	SetWindowDecorated(decorated bool)
	SetWindowIcon(iconImages []image.Image)
	SetWindowResizable(resizable bool)
	SetWindowTitle(title string)
	Input() Input
}
<|endoftext|>


<commit_before>// Copyright 2012 Dorival de Moraes Pedroso. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package io

import (
	"os/exec"
	"testing"

	"github.com/cpmech/gosl/chk"
)

func Test_runcmd01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("runcmd01")

	ver := false
	Pforan("running 'ls -la'\n")
	out, err := RunCmd(ver, "ls", "-la")
	Pfblue2("\noutput:\n%v\n", out)
	if err != nil {
		tst.Errorf("error: %v\n", err)
		return
	}
}

func Test_pipe01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("runcmd01")

	Pforan("running pipe\n")

	// find $DIR -type f # Find all files
	dir := "."
	find := exec.Command("find", dir, "-type", "f")

	// | grep -v '/[._]' # Ignore hidden/temporary files
	egrep := exec.Command("egrep", "-v", `/[._]`)

	// | sort -t. -k2 # Sort by file extension
	sort := exec.Command("sort", "-t.", "-k2")

	output, stderr, err := Pipeline(find, egrep, sort)
	Pfblue2("\noutput:\n%v\n", string(output))
	Pfcyan("stderr:\n%v\n", string(stderr))
	if err != nil {
		tst.Errorf("error: %v\n", err)
		return
	}
}
<commit_msg>changed egrep to grep in test in order to make io pkg to work on windows<commit_after>// Copyright 2012 Dorival de Moraes Pedroso. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package io

import (
	"os/exec"
	"testing"

	"github.com/cpmech/gosl/chk"
)

func Test_runcmd01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("runcmd01")

	ver := false
	Pforan("running 'ls -la'\n")
	out, err := RunCmd(ver, "ls", "-la")
	Pfblue2("\noutput:\n%v\n", out)
	if err != nil {
		tst.Errorf("error: %v\n", err)
		return
	}
}

func Test_pipe01(tst *testing.T) {

	//verbose()
	chk.PrintTitle("runcmd01")

	Pforan("running pipe\n")

	// find $DIR -type f # Find all files
	dir := "."
	find := exec.Command("find", dir, "-type", "f")

	// | grep -v '/[._]' # Ignore hidden/temporary files
	egrep := exec.Command("grep", "-v", `/[._]`)

	// | sort -t. -k2 # Sort by file extension
	sort := exec.Command("sort", "-t.", "-k2")

	output, stderr, err := Pipeline(find, egrep, sort)
	Pfblue2("\noutput:\n%v\n", string(output))
	Pfcyan("stderr:\n%v\n", string(stderr))
	if err != nil {
		tst.Errorf("error: %v\n", err)
		return
	}
}
<|endoftext|>


<commit_before>// +build integration
// To turn on this test use -tags=integration in go test command

package credentials

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/jcmturner/gokrb5.v4/iana/nametype"
	"gopkg.in/jcmturner/gokrb5.v4/testdata"
	"gopkg.in/jcmturner/gokrb5.v4/types"
)

const (
	kinitCmd = "kinit"
	kvnoCmd  = "kvno"
	spn      = "HTTP/host.test.gokrb5"
)

func login() error {
	file, err := os.Create("/etc/krb5.conf")
	if err != nil {
		return fmt.Errorf("cannot open krb5.conf: %v", err)
	}
	defer file.Close()
	fmt.Fprintf(file, testdata.TEST_KRB5CONF)

	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")

	stdinR, stdinW := io.Pipe()
	stderrR, stderrW := io.Pipe()
	cmd.Stdin = stdinR
	cmd.Stderr = stderrW

	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
	}

	go func() {
		io.WriteString(stdinW, "passwordvalue")
		stdinW.Close()
	}()
	errBuf := new(bytes.Buffer)
	go func() {
		io.Copy(errBuf, stderrR)
		stderrR.Close()
	}()

	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v stderr: %s", kinitCmd, err, string(errBuf.Bytes()))
	}
	return nil
}

func getServiceTkt() error {
	cmd := exec.Command(kvnoCmd, spn)
	err := cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
	}
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
	}
	return nil
}

func loadCCache() (CCache, error) {
	usr, _ := user.Current()
	cpath := "/tmp/krb5cc_" + usr.Uid
	return LoadCCache(cpath)
}

func TestLoadCCache(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	c, err := loadCCache()
	pn := c.GetClientPrincipalName()
	assert.Equal(t, "testuser1", pn.GetPrincipalNameString(), "principal not as expected")
	assert.Equal(t, "TEST.GOKRB5", c.GetClientRealm(), "realm not as expected")
}

func TestCCacheEntries(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	err = getServiceTkt()
	if err != nil {
		t.Fatalf("error getting service ticket: %v", err)
	}
	c, err := loadCCache()
	creds := c.GetEntries()
	var found bool
	n := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, spn)
	for _, cred := range creds {
		if cred.Server.PrincipalName.Equal(n) {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("Entry for %s not found in CCache", spn)
	}
}
<commit_msg>test from ccache tgt<commit_after>// +build integration
// To turn on this test use -tags=integration in go test command

package credentials

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/jcmturner/gokrb5.v4/client"
	"gopkg.in/jcmturner/gokrb5.v4/config"
	"gopkg.in/jcmturner/gokrb5.v4/iana/nametype"
	"gopkg.in/jcmturner/gokrb5.v4/testdata"
	"gopkg.in/jcmturner/gokrb5.v4/types"
)

const (
	kinitCmd = "kinit"
	kvnoCmd  = "kvno"
	spn      = "HTTP/host.test.gokrb5"
)

func login() error {
	file, err := os.Create("/etc/krb5.conf")
	if err != nil {
		return fmt.Errorf("cannot open krb5.conf: %v", err)
	}
	defer file.Close()
	fmt.Fprintf(file, testdata.TEST_KRB5CONF)

	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")

	stdinR, stdinW := io.Pipe()
	stderrR, stderrW := io.Pipe()
	cmd.Stdin = stdinR
	cmd.Stderr = stderrW

	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
	}

	go func() {
		io.WriteString(stdinW, "passwordvalue")
		stdinW.Close()
	}()
	errBuf := new(bytes.Buffer)
	go func() {
		io.Copy(errBuf, stderrR)
		stderrR.Close()
	}()

	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v stderr: %s", kinitCmd, err, string(errBuf.Bytes()))
	}
	return nil
}

func getServiceTkt() error {
	cmd := exec.Command(kvnoCmd, spn)
	err := cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
	}
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
	}
	return nil
}

func loadCCache() (CCache, error) {
	usr, _ := user.Current()
	cpath := "/tmp/krb5cc_" + usr.Uid
	return LoadCCache(cpath)
}

func TestLoadCCache(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	c, err := loadCCache()
	if err != nil {
		t.Errorf("error loading CCache: %v", err)
	}
	pn := c.GetClientPrincipalName()
	assert.Equal(t, "testuser1", pn.GetPrincipalNameString(), "principal not as expected")
	assert.Equal(t, "TEST.GOKRB5", c.GetClientRealm(), "realm not as expected")
}

func TestCCacheEntries(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	err = getServiceTkt()
	if err != nil {
		t.Fatalf("error getting service ticket: %v", err)
	}
	c, err := loadCCache()
	if err != nil {
		t.Errorf("error loading CCache: %v", err)
	}
	creds := c.GetEntries()
	var found bool
	n := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, spn)
	for _, cred := range creds {
		if cred.Server.PrincipalName.Equal(n) {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("Entry for %s not found in CCache", spn)
	}
}

func TestGetServiceTicketFromCCacheTGT(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	c, err := loadCCache()
	if err != nil {
		t.Errorf("error loading CCache: %v", err)
	}
	cfg, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
	cl, err := client.NewClientFromCCache(c)
	if err != nil {
		t.Fatalf("error generating client from ccache: %v", err)
	}
	cl.WithConfig(cfg)
	url := os.Getenv("TEST_HTTP_URL")
	if url == "" {
		url = testdata.TEST_HTTP_URL
	}
	r, _ := http.NewRequest("GET", url, nil)
	err = cl.SetSPNEGOHeader(r, "HTTP/host.test.gokrb5")
	if err != nil {
		t.Fatalf("error setting client SPNEGO header: %v", err)
	}
	httpResp, err := http.DefaultClient.Do(r)
	if err != nil {
		t.Fatalf("request error: %v\n", err)
	}
	assert.Equal(t, http.StatusOK, httpResp.StatusCode, "status code in response to client SPNEGO request not as expected")
}
<|endoftext|>


<commit_before>package aks

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"sort"

	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-09-01/containerservice"
	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-07-01/network"
	"github.com/Azure/azure-sdk-for-go/services/subscription/mgmt/2020-09-01/subscription"
	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/autorest/adal"
	"github.com/Azure/go-autorest/autorest/to"
	"github.com/mcuadros/go-version"
)

type virtualNetworksResponseBody struct {
	Name          string   `json:"name"`
	ResourceGroup string   `json:"resourceGroup"`
	Subnets       []subnet `json:"subnets"`
}

type subnet struct {
	Name         string `json:"name"`
	AddressRange string `json:"addressRange"`
}

var matchResourceGroup = regexp.MustCompile("/resource[gG]roups/(.+?)/")

func NewClientAuthorizer(cap *Capabilities) (autorest.Authorizer, error) {
	oauthConfig, err := adal.NewOAuthConfig(cap.AuthBaseURL, cap.TenantID)
	if err != nil {
		return nil, err
	}

	spToken, err := adal.NewServicePrincipalToken(*oauthConfig, cap.ClientID, cap.ClientSecret, cap.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("couldn't authenticate to Azure cloud with error: %v", err)
	}

	return autorest.NewBearerAuthorizer(spToken), nil
}

func NewVirtualMachineClient(cap *Capabilities) (*compute.VirtualMachineSizesClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	virtualMachine := compute.NewVirtualMachineSizesClient(cap.SubscriptionID)
	virtualMachine.Authorizer = authorizer

	return &virtualMachine, nil
}

func NewContainerServiceClient(cap *Capabilities) (*containerservice.ContainerServicesClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	containerService := containerservice.NewContainerServicesClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	containerService.Authorizer = authorizer

	return &containerService, nil
}

func NewNetworkServiceClient(cap *Capabilities) (*network.VirtualNetworksClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	containerService := network.NewVirtualNetworksClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	containerService.Authorizer = authorizer

	return &containerService, nil
}

func NewClusterClient(cap *Capabilities) (*containerservice.ManagedClustersClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	client := containerservice.NewManagedClustersClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	client.Authorizer = authorizer

	return &client, nil
}

func NewSubscriptionServiceClient(cap *Capabilities) (*subscription.SubscriptionsClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	subscriptionService := subscription.NewSubscriptionsClient()
	subscriptionService.Authorizer = authorizer

	return &subscriptionService, nil
}

type sortableVersion []string

func (s sortableVersion) Len() int {
	return len(s)
}

func (s sortableVersion) Swap(a, b int) {
	s[a], s[b] = s[b], s[a]
}

func (s sortableVersion) Less(a, b int) bool {
	return version.Compare(s[a], s[b], "<")
}

func listKubernetesVersions(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	if cap.ResourceLocation == "" {
		return nil, http.StatusBadRequest, fmt.Errorf("region is required")
	}

	clientContainer, err := NewContainerServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	orchestrators, err := clientContainer.ListOrchestrators(ctx, cap.ResourceLocation, "managedClusters")
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get orchestrators: %v", err)
	}

	if orchestrators.Orchestrators == nil {
		return nil, http.StatusBadRequest, fmt.Errorf("no version profiles returned: %v", err)
	}

	var kubernetesVersions []string

	for _, profile := range *orchestrators.Orchestrators {
		if profile.OrchestratorType == nil || profile.OrchestratorVersion == nil {
			return nil, http.StatusInternalServerError, fmt.Errorf("unexpected nil orchestrator type or version")
		}

		if *profile.OrchestratorType == "Kubernetes" {
			kubernetesVersions = append(kubernetesVersions, *profile.OrchestratorVersion)
		}
	}

	sort.Sort(sortableVersion(kubernetesVersions))

	return encodeOutput(kubernetesVersions)
}

func listVirtualNetworks(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientNetwork, err := NewNetworkServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	networkList, err := clientNetwork.ListAll(ctx)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get networks: %v", err)
	}

	var networks []virtualNetworksResponseBody

	for networkList.NotDone() {
		var batch []virtualNetworksResponseBody

		for _, azureNetwork := range networkList.Values() {
			var subnets []subnet

			if azureNetwork.Subnets != nil {
				for _, azureSubnet := range *azureNetwork.Subnets {
					if azureSubnet.Name != nil {
						subnets = append(subnets, subnet{
							Name:         to.String(azureSubnet.Name),
							AddressRange: to.String(azureSubnet.AddressPrefix),
						})
					}
				}
			}

			if azureNetwork.ID == nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("no ID on virtual network")
			}

			match := matchResourceGroup.FindStringSubmatch(*azureNetwork.ID)

			if len(match) < 2 || match[1] == "" {
				return nil, http.StatusInternalServerError, fmt.Errorf("could not parse virtual network ID")
			}

			if azureNetwork.Name == nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("no name on virtual network")
			}

			batch = append(batch, virtualNetworksResponseBody{
				Name:          to.String(azureNetwork.Name),
				ResourceGroup: match[1],
				Subnets:       subnets,
			})
		}

		networks = append(networks, batch...)

		err = networkList.NextWithContext(ctx)
		if err != nil {
			return nil, http.StatusInternalServerError, err
		}
	}

	return encodeOutput(networks)
}

type clustersResponseBody struct {
	ResourceGroup string `json:"resourceGroup"`
	ClusterName   string `json:"clusterName"`
	RBACEnabled   bool   `json:"rbacEnabled"`
}

func listClusters(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientCluster, err := NewClusterClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	clusterList, err := clientCluster.List(ctx)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get cluster list: %v", err)
	}

	var clusters []clustersResponseBody

	for clusterList.NotDone() {
		for _, cluster := range clusterList.Values() {
			tmpCluster := clustersResponseBody{
				ClusterName: to.String(cluster.Name),
				RBACEnabled: to.Bool(cluster.EnableRBAC),
			}
			if cluster.ID != nil {
				match := matchResourceGroup.FindStringSubmatch(to.String(cluster.ID))
				if len(match) < 2 || match[1] == "" {
					return nil, http.StatusInternalServerError, fmt.Errorf("could not parse virtual network ID")
				}
				tmpCluster.ResourceGroup = match[1]
			}
			clusters = append(clusters, tmpCluster)
		}

		err = clusterList.NextWithContext(ctx)
		if err != nil {
			return nil, http.StatusInternalServerError, err
		}
	}

	return encodeOutput(clusters)
}

func listVMSizes(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	if cap.ResourceLocation == "" {
		return nil, http.StatusBadRequest, fmt.Errorf("region is required")
	}

	virtualMachine, err := NewVirtualMachineClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}
	vmMachineSizeList, err := virtualMachine.List(ctx, cap.ResourceLocation)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get VM sizes: %v", err)
	}

	vmSizes := make([]string, 0, len(*vmMachineSizeList.Value))

	for _, virtualMachineSize := range *vmMachineSizeList.Value {
		vmSizes = append(vmSizes, to.String(virtualMachineSize.Name))
	}

	return encodeOutput(vmSizes)
}

type locationsResponseBody struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
}

func listLocations(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientSubscription, err := NewSubscriptionServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}
	locationList, err := clientSubscription.ListLocations(ctx, cap.SubscriptionID)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get locations: %v", err)
	}

	var locations []locationsResponseBody

	for _, location := range *locationList.Value {
		locations = append(locations, locationsResponseBody{
			Name:        to.String(location.Name),
			DisplayName: to.String(location.DisplayName),
		})
	}

	return encodeOutput(locations)
}

func encodeOutput(result interface{}) ([]byte, int, error) {
	data, err := json.Marshal(&result)
	if err != nil {
		return data, http.StatusInternalServerError, err
	}

	return data, http.StatusOK, err
}
<commit_msg>Filter by region in /meta/aksVirtualNetworks<commit_after>package aks

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"sort"

	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute"
	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-09-01/containerservice"
	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-07-01/network"
	"github.com/Azure/azure-sdk-for-go/services/subscription/mgmt/2020-09-01/subscription"
	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/autorest/adal"
	"github.com/Azure/go-autorest/autorest/to"
	"github.com/mcuadros/go-version"
)

type virtualNetworksResponseBody struct {
	Name          string   `json:"name"`
	ResourceGroup string   `json:"resourceGroup"`
	Subnets       []subnet `json:"subnets"`
	Location      string   `json:"location"`
}

type subnet struct {
	Name         string `json:"name"`
	AddressRange string `json:"addressRange"`
}

var matchResourceGroup = regexp.MustCompile("/resource[gG]roups/(.+?)/")

func NewClientAuthorizer(cap *Capabilities) (autorest.Authorizer, error) {
	oauthConfig, err := adal.NewOAuthConfig(cap.AuthBaseURL, cap.TenantID)
	if err != nil {
		return nil, err
	}

	spToken, err := adal.NewServicePrincipalToken(*oauthConfig, cap.ClientID, cap.ClientSecret, cap.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("couldn't authenticate to Azure cloud with error: %v", err)
	}

	return autorest.NewBearerAuthorizer(spToken), nil
}

func NewVirtualMachineClient(cap *Capabilities) (*compute.VirtualMachineSizesClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	virtualMachine := compute.NewVirtualMachineSizesClient(cap.SubscriptionID)
	virtualMachine.Authorizer = authorizer

	return &virtualMachine, nil
}

func NewContainerServiceClient(cap *Capabilities) (*containerservice.ContainerServicesClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	containerService := containerservice.NewContainerServicesClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	containerService.Authorizer = authorizer

	return &containerService, nil
}

func NewNetworkServiceClient(cap *Capabilities) (*network.VirtualNetworksClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	containerService := network.NewVirtualNetworksClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	containerService.Authorizer = authorizer

	return &containerService, nil
}

func NewClusterClient(cap *Capabilities) (*containerservice.ManagedClustersClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	client := containerservice.NewManagedClustersClientWithBaseURI(cap.BaseURL, cap.SubscriptionID)
	client.Authorizer = authorizer

	return &client, nil
}

func NewSubscriptionServiceClient(cap *Capabilities) (*subscription.SubscriptionsClient, error) {
	authorizer, err := NewClientAuthorizer(cap)
	if err != nil {
		return nil, err
	}

	subscriptionService := subscription.NewSubscriptionsClient()
	subscriptionService.Authorizer = authorizer

	return &subscriptionService, nil
}

type sortableVersion []string

func (s sortableVersion) Len() int {
	return len(s)
}

func (s sortableVersion) Swap(a, b int) {
	s[a], s[b] = s[b], s[a]
}

func (s sortableVersion) Less(a, b int) bool {
	return version.Compare(s[a], s[b], "<")
}

func listKubernetesVersions(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	if cap.ResourceLocation == "" {
		return nil, http.StatusBadRequest, fmt.Errorf("region is required")
	}

	clientContainer, err := NewContainerServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	orchestrators, err := clientContainer.ListOrchestrators(ctx, cap.ResourceLocation, "managedClusters")
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get orchestrators: %v", err)
	}

	if orchestrators.Orchestrators == nil {
		return nil, http.StatusBadRequest, fmt.Errorf("no version profiles returned: %v", err)
	}

	var kubernetesVersions []string

	for _, profile := range *orchestrators.Orchestrators {
		if profile.OrchestratorType == nil || profile.OrchestratorVersion == nil {
			return nil, http.StatusInternalServerError, fmt.Errorf("unexpected nil orchestrator type or version")
		}

		if *profile.OrchestratorType == "Kubernetes" {
			kubernetesVersions = append(kubernetesVersions, *profile.OrchestratorVersion)
		}
	}

	sort.Sort(sortableVersion(kubernetesVersions))

	return encodeOutput(kubernetesVersions)
}

func listVirtualNetworks(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientNetwork, err := NewNetworkServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	networkList, err := clientNetwork.ListAll(ctx)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get networks: %v", err)
	}

	var networks []virtualNetworksResponseBody

	for networkList.NotDone() {
		var batch []virtualNetworksResponseBody

		for _, azureNetwork := range networkList.Values() {
			if cap.ResourceLocation != "" && to.String(azureNetwork.Location) != cap.ResourceLocation {
				continue
			}
			var subnets []subnet

			if azureNetwork.Subnets != nil {
				for _, azureSubnet := range *azureNetwork.Subnets {
					if azureSubnet.Name != nil {
						subnets = append(subnets, subnet{
							Name:         to.String(azureSubnet.Name),
							AddressRange: to.String(azureSubnet.AddressPrefix),
						})
					}
				}
			}

			if azureNetwork.ID == nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("no ID on virtual network")
			}

			match := matchResourceGroup.FindStringSubmatch(*azureNetwork.ID)

			if len(match) < 2 || match[1] == "" {
				return nil, http.StatusInternalServerError, fmt.Errorf("could not parse virtual network ID")
			}

			if azureNetwork.Name == nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("no name on virtual network")
			}

			batch = append(batch, virtualNetworksResponseBody{
				Name:          to.String(azureNetwork.Name),
				ResourceGroup: match[1],
				Subnets:       subnets,
				Location:      to.String(azureNetwork.Location),
			})
		}

		networks = append(networks, batch...)

		err = networkList.NextWithContext(ctx)
		if err != nil {
			return nil, http.StatusInternalServerError, err
		}
	}

	return encodeOutput(networks)
}

type clustersResponseBody struct {
	ResourceGroup string `json:"resourceGroup"`
	ClusterName   string `json:"clusterName"`
	RBACEnabled   bool   `json:"rbacEnabled"`
}

func listClusters(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientCluster, err := NewClusterClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}

	clusterList, err := clientCluster.List(ctx)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get cluster list: %v", err)
	}

	var clusters []clustersResponseBody

	for clusterList.NotDone() {
		for _, cluster := range clusterList.Values() {
			tmpCluster := clustersResponseBody{
				ClusterName: to.String(cluster.Name),
				RBACEnabled: to.Bool(cluster.EnableRBAC),
			}
			if cluster.ID != nil {
				match := matchResourceGroup.FindStringSubmatch(to.String(cluster.ID))
				if len(match) < 2 || match[1] == "" {
					return nil, http.StatusInternalServerError, fmt.Errorf("could not parse virtual network ID")
				}
				tmpCluster.ResourceGroup = match[1]
			}
			clusters = append(clusters, tmpCluster)
		}

		err = clusterList.NextWithContext(ctx)
		if err != nil {
			return nil, http.StatusInternalServerError, err
		}
	}

	return encodeOutput(clusters)
}

func listVMSizes(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	if cap.ResourceLocation == "" {
		return nil, http.StatusBadRequest, fmt.Errorf("region is required")
	}

	virtualMachine, err := NewVirtualMachineClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}
	vmMachineSizeList, err := virtualMachine.List(ctx, cap.ResourceLocation)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get VM sizes: %v", err)
	}

	vmSizes := make([]string, 0, len(*vmMachineSizeList.Value))

	for _, virtualMachineSize := range *vmMachineSizeList.Value {
		vmSizes = append(vmSizes, to.String(virtualMachineSize.Name))
	}

	return encodeOutput(vmSizes)
}

type locationsResponseBody struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
}

func listLocations(ctx context.Context, cap *Capabilities) ([]byte, int, error) {
	clientSubscription, err := NewSubscriptionServiceClient(cap)
	if err != nil {
		return nil, http.StatusInternalServerError, err
	}
	locationList, err := clientSubscription.ListLocations(ctx, cap.SubscriptionID)
	if err != nil {
		return nil, http.StatusBadRequest, fmt.Errorf("failed to get locations: %v", err)
	}

	var locations []locationsResponseBody

	for _, location := range *locationList.Value {
		locations = append(locations, locationsResponseBody{
			Name:        to.String(location.Name),
			DisplayName: to.String(location.DisplayName),
		})
	}

	return encodeOutput(locations)
}

func encodeOutput(result interface{}) ([]byte, int, error) {
	data, err := json.Marshal(&result)
	if err != nil {
		return data, http.StatusInternalServerError, err
	}

	return data, http.StatusOK, err
}
<|endoftext|>


<commit_before>package ldap

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/rancher/norman/httperror"
	v3 "github.com/rancher/types/apis/management.cattle.io/v3"
	"github.com/sirupsen/logrus"
	ldapv2 "gopkg.in/ldap.v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type ConfigAttributes struct {
	GroupMemberMappingAttribute string
	GroupNameAttribute          string
	GroupObjectClass            string
	GroupSearchAttribute        string
	ObjectClass                 string
	ProviderName                string
	UserLoginAttribute          string
	UserNameAttribute           string
	UserObjectClass             string
}

func Connect(config *v3.LdapConfig, caPool *x509.CertPool) (*ldapv2.Conn, error) {
	return NewLDAPConn(config.Servers, config.TLS, config.Port, config.ConnectionTimeout, caPool)
}

func NewLDAPConn(servers []string, TLS bool, port int64, connectionTimeout int64, caPool *x509.CertPool) (*ldapv2.Conn, error) {
	logrus.Debug("Now creating Ldap connection")
	var lConn *ldapv2.Conn
	var err error
	var tlsConfig *tls.Config
	ldapv2.DefaultTimeout = time.Duration(connectionTimeout) * time.Millisecond
	// TODO implment multi-server support
	if len(servers) != 1 {
		return nil, errors.New("invalid server config. only exactly 1 server is currently supported")
	}
	server := servers[0]
	if TLS {
		tlsConfig = &tls.Config{RootCAs: caPool, InsecureSkipVerify: false, ServerName: server}
		lConn, err = ldapv2.DialTLS("tcp", fmt.Sprintf("%s:%d", server, port), tlsConfig)
		if err != nil {
			return nil, fmt.Errorf("Error creating ssl connection: %v", err)
		}
	} else {
		lConn, err = ldapv2.Dial("tcp", fmt.Sprintf("%s:%d", server, port))
		if err != nil {
			return nil, fmt.Errorf("Error creating connection: %v", err)
		}
	}

	lConn.SetTimeout(time.Duration(connectionTimeout) * time.Millisecond)

	return lConn, nil
}

func GetUserExternalID(username string, loginDomain string) string {
	if strings.Contains(username, "\\") {
		return username
	} else if loginDomain != "" {
		return loginDomain + "\\" + username
	}
	return username
}

func HasPermission(attributes []*ldapv2.EntryAttribute, userObjectClass string, userEnabledAttribute string, userDisabledBitMask int64) bool {
	var permission int64
	if !IsType(attributes, userObjectClass) {
		return true
	}

	if userEnabledAttribute != "" {
		for _, attr := range attributes {
			if attr.Name == userEnabledAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					intAttr, err := strconv.ParseInt(attr.Values[0], 10, 64)
					if err != nil {
						logrus.Errorf("Failed to get USER_ENABLED_ATTRIBUTE, error: %v", err)
						return false
					}
					permission = intAttr
				}
			}
		}
	} else {
		return true
	}
	permission = permission & userDisabledBitMask
	return permission != userDisabledBitMask
}

func IsType(search []*ldapv2.EntryAttribute, varType string) bool {
	for _, attrib := range search {
		if attrib.Name == "objectClass" {
			for _, val := range attrib.Values {
				if strings.EqualFold(val, varType) {
					logrus.Debugf("ldap IsType found object of type %s", varType)
					return true
				}
			}
		}
	}
	logrus.Debugf("ldap IsType failed to determine if object is type: %s", varType)
	return false
}

func GetAttributeValuesByName(search []*ldapv2.EntryAttribute, attributeName string) []string {
	for _, attrib := range search {
		if attrib.Name == attributeName {
			return attrib.Values
		}
	}
	return []string{}
}

func GetUserSearchAttributes(memberOfAttribute, ObjectClass string, config *v3.ActiveDirectoryConfig) []string {
	userSearchAttributes := []string{memberOfAttribute,
		ObjectClass,
		config.UserObjectClass,
		config.UserLoginAttribute,
		config.UserNameAttribute,
		config.UserEnabledAttribute}
	return userSearchAttributes
}

func GetGroupSearchAttributes(memberOfAttribute, ObjectClass string, config *v3.ActiveDirectoryConfig) []string {
	groupSeachAttributes := []string{memberOfAttribute,
		ObjectClass,
		config.GroupObjectClass,
		config.UserLoginAttribute,
		config.GroupNameAttribute,
		config.GroupSearchAttribute}
	return groupSeachAttributes
}

func GetUserSearchAttributesForLDAP(ObjectClass string, config *v3.LdapConfig) []string {
	userSearchAttributes := []string{"dn", config.UserMemberAttribute,
		ObjectClass,
		config.UserObjectClass,
		config.UserLoginAttribute,
		config.UserNameAttribute,
		config.UserEnabledAttribute}
	return userSearchAttributes
}

func GetGroupSearchAttributesForLDAP(ObjectClass string, config *v3.LdapConfig) []string {
	groupSeachAttributes := []string{config.GroupMemberUserAttribute,
		config.GroupMemberMappingAttribute,
		ObjectClass,
		config.GroupObjectClass,
		config.UserLoginAttribute,
		config.GroupNameAttribute,
		config.GroupSearchAttribute}
	return groupSeachAttributes
}

func AuthenticateServiceAccountUser(serviceAccountPassword string, serviceAccountUsername string, defaultLoginDomain string, lConn *ldapv2.Conn) error {
	logrus.Debug("Binding service account username password")
	if serviceAccountPassword == "" {
		return httperror.NewAPIError(httperror.MissingRequired, "service account password not provided")
	}
	sausername := GetUserExternalID(serviceAccountUsername, defaultLoginDomain)
	err := lConn.Bind(sausername, serviceAccountPassword)
	if err != nil {
		if ldapv2.IsErrorWithCode(err, ldapv2.LDAPResultInvalidCredentials) {
			return httperror.WrapAPIError(err, httperror.Unauthorized, "authentication failed")
		}
		return httperror.WrapAPIError(err, httperror.ServerError, "server error while authenticating")
	}

	return nil
}

func AttributesToPrincipal(attribs []*ldapv2.EntryAttribute, dnStr, scope, providerName, userObjectClass, userNameAttribute, userLoginAttribute, groupObjectClass, groupNameAttribute string) (*v3.Principal, error) {
	var externalIDType, accountName, externalID, login, kind string
	externalID = dnStr
	externalIDType = scope

	if IsType(attribs, userObjectClass) {
		for _, attr := range attribs {
			if attr.Name == userNameAttribute {
				if len(attr.Values) != 0 {
					accountName = attr.Values[0]
				} else {
					accountName = externalID
				}
			}
			if attr.Name == userLoginAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					login = attr.Values[0]
				}
			}
		}
		if login == "" {
			login = accountName
		}
		kind = "user"
	} else if IsType(attribs, groupObjectClass) {
		for _, attr := range attribs {
			if attr.Name == groupNameAttribute {
				if len(attr.Values) != 0 {
					accountName = attr.Values[0]
				} else {
					accountName = externalID
				}
			}
			if attr.Name == userLoginAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					login = attr.Values[0]
				}
			}
		}
		if login == "" {
			login = accountName
		}
		kind = "group"
	} else {
		return nil, fmt.Errorf("Failed to get attributes for %s", dnStr)
	}

	principal := &v3.Principal{
		ObjectMeta:    metav1.ObjectMeta{Name: externalIDType + "://" + externalID},
		DisplayName:   accountName,
		LoginName:     login,
		PrincipalType: kind,
		Me:            true,
		Provider:      providerName,
	}
	return principal, nil
}

func GatherParentGroups(groupPrincipal v3.Principal, searchDomain string, groupScope string, config *ConfigAttributes, lConn *ldapv2.Conn,
	groupMap map[string]bool, nestedGroupPrincipals *[]v3.Principal, searchAttributes []string) error {
	groupMap[groupPrincipal.ObjectMeta.Name] = true
	principals := []v3.Principal{}
	//var searchAttributes []string
	parts := strings.SplitN(groupPrincipal.ObjectMeta.Name, ":", 2)
	if len(parts) != 2 {
		return errors.Errorf("invalid id %v", groupPrincipal.ObjectMeta.Name)
	}
	groupDN := strings.TrimPrefix(parts[1], "//")

	searchGroup := ldapv2.NewSearchRequest(searchDomain,
		ldapv2.ScopeWholeSubtree, ldapv2.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(%v=%v)(%v=%v))", config.GroupMemberMappingAttribute, ldapv2.EscapeFilter(groupDN), config.ObjectClass, config.GroupObjectClass),
		searchAttributes, nil)
	resultGroups, err := lConn.SearchWithPaging(searchGroup, 1000)
	if err != nil {
		return err
	}

	for i := 0; i < len(resultGroups.Entries); i++ {
		entry := resultGroups.Entries[i]
		principal, err := AttributesToPrincipal(entry.Attributes, entry.DN, groupScope, config.ProviderName, config.UserObjectClass, config.UserNameAttribute, config.UserLoginAttribute, config.GroupObjectClass, config.GroupNameAttribute)
		if err != nil {
			logrus.Errorf("Error translating group result: %v", err)
			continue
		}
		principals = append(principals, *principal)
	}

	for _, gp := range principals {
		if _, ok := groupMap[gp.ObjectMeta.Name]; ok {
			continue
		} else {
			*nestedGroupPrincipals = append(*nestedGroupPrincipals, gp)
			err = GatherParentGroups(gp, searchDomain, groupScope, config, lConn, groupMap, nestedGroupPrincipals, searchAttributes)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func FindNonDuplicateBetweenGroupPrincipals(newGroupPrincipals []v3.Principal, groupPrincipals []v3.Principal, nonDupGroupPrincipals []v3.Principal) []v3.Principal {
	for _, gp := range newGroupPrincipals {
		counter := 0
		for _, usermembergp := range groupPrincipals {
			// check the groups ObjectMeta.Name and name fields value are the same, then they are the same group
			if gp.ObjectMeta.Name == usermembergp.ObjectMeta.Name && gp.DisplayName == usermembergp.DisplayName {
				break
			} else {
				counter++
			}
		}
		if counter == len(groupPrincipals) {
			nonDupGroupPrincipals = append(nonDupGroupPrincipals, gp)
		}
	}
	return nonDupGroupPrincipals
}

func Min(a int, b int) int {
	if a < b {
		return a
	}
	return b
}

func NewCAPool(cert string) (*x509.CertPool, error) {
	pool, err := x509.SystemCertPool()
	if err != nil {
		return nil, err
	}
	pool.AppendCertsFromPEM([]byte(cert))
	return pool, nil
}

func ValidateLdapConfig(ldapConfig *v3.LdapConfig, certpool *x509.CertPool) (bool, error) {
	if len(ldapConfig.Servers) != 1 {
		return false, nil
	}

	lConn, err := Connect(ldapConfig, certpool)
	if err != nil {
		return false, err
	}
	defer lConn.Close()

	logrus.Debugf("validated ldap configuration: %s", ldapConfig.Servers[0])
	return true, nil
}
<commit_msg>Remove the member attribute of the group query, a group could have thousands of members and this makes the group query really slow on huge setups. When we pull data for groups, we do not need the members unless it is for nested group functionality. For nested groups the query remains same and is not changed.<commit_after>package ldap

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/rancher/norman/httperror"
	v3 "github.com/rancher/types/apis/management.cattle.io/v3"
	"github.com/sirupsen/logrus"
	ldapv2 "gopkg.in/ldap.v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type ConfigAttributes struct {
	GroupMemberMappingAttribute string
	GroupNameAttribute          string
	GroupObjectClass            string
	GroupSearchAttribute        string
	ObjectClass                 string
	ProviderName                string
	UserLoginAttribute          string
	UserNameAttribute           string
	UserObjectClass             string
}

func Connect(config *v3.LdapConfig, caPool *x509.CertPool) (*ldapv2.Conn, error) {
	return NewLDAPConn(config.Servers, config.TLS, config.Port, config.ConnectionTimeout, caPool)
}

func NewLDAPConn(servers []string, TLS bool, port int64, connectionTimeout int64, caPool *x509.CertPool) (*ldapv2.Conn, error) {
	logrus.Debug("Now creating Ldap connection")
	var lConn *ldapv2.Conn
	var err error
	var tlsConfig *tls.Config
	ldapv2.DefaultTimeout = time.Duration(connectionTimeout) * time.Millisecond
	// TODO implment multi-server support
	if len(servers) != 1 {
		return nil, errors.New("invalid server config. only exactly 1 server is currently supported")
	}
	server := servers[0]
	if TLS {
		tlsConfig = &tls.Config{RootCAs: caPool, InsecureSkipVerify: false, ServerName: server}
		lConn, err = ldapv2.DialTLS("tcp", fmt.Sprintf("%s:%d", server, port), tlsConfig)
		if err != nil {
			return nil, fmt.Errorf("Error creating ssl connection: %v", err)
		}
	} else {
		lConn, err = ldapv2.Dial("tcp", fmt.Sprintf("%s:%d", server, port))
		if err != nil {
			return nil, fmt.Errorf("Error creating connection: %v", err)
		}
	}

	lConn.SetTimeout(time.Duration(connectionTimeout) * time.Millisecond)

	return lConn, nil
}

func GetUserExternalID(username string, loginDomain string) string {
	if strings.Contains(username, "\\") {
		return username
	} else if loginDomain != "" {
		return loginDomain + "\\" + username
	}
	return username
}

func HasPermission(attributes []*ldapv2.EntryAttribute, userObjectClass string, userEnabledAttribute string, userDisabledBitMask int64) bool {
	var permission int64
	if !IsType(attributes, userObjectClass) {
		return true
	}

	if userEnabledAttribute != "" {
		for _, attr := range attributes {
			if attr.Name == userEnabledAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					intAttr, err := strconv.ParseInt(attr.Values[0], 10, 64)
					if err != nil {
						logrus.Errorf("Failed to get USER_ENABLED_ATTRIBUTE, error: %v", err)
						return false
					}
					permission = intAttr
				}
			}
		}
	} else {
		return true
	}
	permission = permission & userDisabledBitMask
	return permission != userDisabledBitMask
}

func IsType(search []*ldapv2.EntryAttribute, varType string) bool {
	for _, attrib := range search {
		if attrib.Name == "objectClass" {
			for _, val := range attrib.Values {
				if strings.EqualFold(val, varType) {
					logrus.Debugf("ldap IsType found object of type %s", varType)
					return true
				}
			}
		}
	}
	logrus.Debugf("ldap IsType failed to determine if object is type: %s", varType)
	return false
}

func GetAttributeValuesByName(search []*ldapv2.EntryAttribute, attributeName string) []string {
	for _, attrib := range search {
		if attrib.Name == attributeName {
			return attrib.Values
		}
	}
	return []string{}
}

func GetUserSearchAttributes(memberOfAttribute, ObjectClass string, config *v3.ActiveDirectoryConfig) []string {
	userSearchAttributes := []string{memberOfAttribute,
		ObjectClass,
		config.UserObjectClass,
		config.UserLoginAttribute,
		config.UserNameAttribute,
		config.UserEnabledAttribute}
	return userSearchAttributes
}

func GetGroupSearchAttributes(memberOfAttribute, ObjectClass string, config *v3.ActiveDirectoryConfig) []string {
	groupSeachAttributes := []string{memberOfAttribute,
		ObjectClass,
		config.GroupObjectClass,
		config.UserLoginAttribute,
		config.GroupNameAttribute,
		config.GroupSearchAttribute}
	return groupSeachAttributes
}

func GetUserSearchAttributesForLDAP(ObjectClass string, config *v3.LdapConfig) []string {
	userSearchAttributes := []string{"dn", config.UserMemberAttribute,
		ObjectClass,
		config.UserObjectClass,
		config.UserLoginAttribute,
		config.UserNameAttribute,
		config.UserEnabledAttribute}
	return userSearchAttributes
}

func GetGroupSearchAttributesForLDAP(ObjectClass string, config *v3.LdapConfig) []string {
	groupSeachAttributes := []string{config.GroupMemberUserAttribute,
		ObjectClass,
		config.GroupObjectClass,
		config.UserLoginAttribute,
		config.GroupNameAttribute,
		config.GroupSearchAttribute}
	return groupSeachAttributes
}

func AuthenticateServiceAccountUser(serviceAccountPassword string, serviceAccountUsername string, defaultLoginDomain string, lConn *ldapv2.Conn) error {
	logrus.Debug("Binding service account username password")
	if serviceAccountPassword == "" {
		return httperror.NewAPIError(httperror.MissingRequired, "service account password not provided")
	}
	sausername := GetUserExternalID(serviceAccountUsername, defaultLoginDomain)
	err := lConn.Bind(sausername, serviceAccountPassword)
	if err != nil {
		if ldapv2.IsErrorWithCode(err, ldapv2.LDAPResultInvalidCredentials) {
			return httperror.WrapAPIError(err, httperror.Unauthorized, "authentication failed")
		}
		return httperror.WrapAPIError(err, httperror.ServerError, "server error while authenticating")
	}

	return nil
}

func AttributesToPrincipal(attribs []*ldapv2.EntryAttribute, dnStr, scope, providerName, userObjectClass, userNameAttribute, userLoginAttribute, groupObjectClass, groupNameAttribute string) (*v3.Principal, error) {
	var externalIDType, accountName, externalID, login, kind string
	externalID = dnStr
	externalIDType = scope

	if IsType(attribs, userObjectClass) {
		for _, attr := range attribs {
			if attr.Name == userNameAttribute {
				if len(attr.Values) != 0 {
					accountName = attr.Values[0]
				} else {
					accountName = externalID
				}
			}
			if attr.Name == userLoginAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					login = attr.Values[0]
				}
			}
		}
		if login == "" {
			login = accountName
		}
		kind = "user"
	} else if IsType(attribs, groupObjectClass) {
		for _, attr := range attribs {
			if attr.Name == groupNameAttribute {
				if len(attr.Values) != 0 {
					accountName = attr.Values[0]
				} else {
					accountName = externalID
				}
			}
			if attr.Name == userLoginAttribute {
				if len(attr.Values) > 0 && attr.Values[0] != "" {
					login = attr.Values[0]
				}
			}
		}
		if login == "" {
			login = accountName
		}
		kind = "group"
	} else {
		return nil, fmt.Errorf("Failed to get attributes for %s", dnStr)
	}

	principal := &v3.Principal{
		ObjectMeta:    metav1.ObjectMeta{Name: externalIDType + "://" + externalID},
		DisplayName:   accountName,
		LoginName:     login,
		PrincipalType: kind,
		Me:            true,
		Provider:      providerName,
	}
	return principal, nil
}

func GatherParentGroups(groupPrincipal v3.Principal, searchDomain string, groupScope string, config *ConfigAttributes, lConn *ldapv2.Conn,
	groupMap map[string]bool, nestedGroupPrincipals *[]v3.Principal, searchAttributes []string) error {
	groupMap[groupPrincipal.ObjectMeta.Name] = true
	principals := []v3.Principal{}
	//var searchAttributes []string
	parts := strings.SplitN(groupPrincipal.ObjectMeta.Name, ":", 2)
	if len(parts) != 2 {
		return errors.Errorf("invalid id %v", groupPrincipal.ObjectMeta.Name)
	}
	groupDN := strings.TrimPrefix(parts[1], "//")

	searchGroup := ldapv2.NewSearchRequest(searchDomain,
		ldapv2.ScopeWholeSubtree, ldapv2.NeverDerefAliases, 0, 0, false,
		fmt.Sprintf("(&(%v=%v)(%v=%v))", config.GroupMemberMappingAttribute, ldapv2.EscapeFilter(groupDN), config.ObjectClass, config.GroupObjectClass),
		searchAttributes, nil)
	resultGroups, err := lConn.SearchWithPaging(searchGroup, 1000)
	if err != nil {
		return err
	}

	for i := 0; i < len(resultGroups.Entries); i++ {
		entry := resultGroups.Entries[i]
		principal, err := AttributesToPrincipal(entry.Attributes, entry.DN, groupScope, config.ProviderName, config.UserObjectClass, config.UserNameAttribute, config.UserLoginAttribute, config.GroupObjectClass, config.GroupNameAttribute)
		if err != nil {
			logrus.Errorf("Error translating group result: %v", err)
			continue
		}
		principals = append(principals, *principal)
	}

	for _, gp := range principals {
		if _, ok := groupMap[gp.ObjectMeta.Name]; ok {
			continue
		} else {
			*nestedGroupPrincipals = append(*nestedGroupPrincipals, gp)
			err = GatherParentGroups(gp, searchDomain, groupScope, config, lConn, groupMap, nestedGroupPrincipals, searchAttributes)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func FindNonDuplicateBetweenGroupPrincipals(newGroupPrincipals []v3.Principal, groupPrincipals []v3.Principal, nonDupGroupPrincipals []v3.Principal) []v3.Principal {
	for _, gp := range newGroupPrincipals {
		counter := 0
		for _, usermembergp := range groupPrincipals {
			// check the groups ObjectMeta.Name and name fields value are the same, then they are the same group
			if gp.ObjectMeta.Name == usermembergp.ObjectMeta.Name && gp.DisplayName == usermembergp.DisplayName {
				break
			} else {
				counter++
			}
		}
		if counter == len(groupPrincipals) {
			nonDupGroupPrincipals = append(nonDupGroupPrincipals, gp)
		}
	}
	return nonDupGroupPrincipals
}

func Min(a int, b int) int {
	if a < b {
		return a
	}
	return b
}

func NewCAPool(cert string) (*x509.CertPool, error) {
	pool, err := x509.SystemCertPool()
	if err != nil {
		return nil, err
	}
	pool.AppendCertsFromPEM([]byte(cert))
	return pool, nil
}

func ValidateLdapConfig(ldapConfig *v3.LdapConfig, certpool *x509.CertPool) (bool, error) {
	if len(ldapConfig.Servers) != 1 {
		return false, nil
	}

	lConn, err := Connect(ldapConfig, certpool)
	if err != nil {
		return false, err
	}
	defer lConn.Close()

	logrus.Debugf("validated ldap configuration: %s", ldapConfig.Servers[0])
	return true, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package factory

import (
	"context"
	"fmt"
	"path"
	"sync"
	"sync/atomic"
	"time"

	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/pkg/transport"
	grpcprom "github.com/grpc-ecosystem/go-grpc-prometheus"
	"google.golang.org/grpc"

	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/etcd3"
	"k8s.io/apiserver/pkg/storage/storagebackend"
	"k8s.io/apiserver/pkg/storage/value"
	"k8s.io/component-base/metrics/legacyregistry"
)

// The short keepalive timeout and interval have been chosen to aggressively
// detect a failed etcd server without introducing much overhead.
const keepaliveTime = 30 * time.Second
const keepaliveTimeout = 10 * time.Second

// dialTimeout is the timeout for failing to establish a connection.
// It is set to 20 seconds as times shorter than that will cause TLS connections to fail
// on heavily loaded arm64 CPUs (issue #64649)
const dialTimeout = 20 * time.Second

func init() {
	// grpcprom auto-registers (via an init function) their client metrics, since we are opting out of
	// using the global prometheus registry and using our own wrapped global registry,
	// we need to explicitly register these metrics to our global registry here.
	// For reference: https://github.com/kubernetes/kubernetes/pull/81387
	legacyregistry.RawMustRegister(grpcprom.DefaultClientMetrics)
}

func newETCD3HealthCheck(c storagebackend.Config) (func() error, error) {
	// constructing the etcd v3 client blocks and times out if etcd is not available.
	// retry in a loop in the background until we successfully create the client, storing the client or error encountered

	clientValue := &atomic.Value{}

	clientErrMsg := &atomic.Value{}
	clientErrMsg.Store("etcd client connection not yet established")

	go wait.PollUntil(time.Second, func() (bool, error) {
		client, err := newETCD3Client(c.Transport)
		if err != nil {
			clientErrMsg.Store(err.Error())
			return false, nil
		}
		clientValue.Store(client)
		clientErrMsg.Store("")
		return true, nil
	}, wait.NeverStop)

	return func() error {
		if errMsg := clientErrMsg.Load().(string); len(errMsg) > 0 {
			return fmt.Errorf(errMsg)
		}
		client := clientValue.Load().(*clientv3.Client)
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		// See https://github.com/etcd-io/etcd/blob/master/etcdctl/ctlv3/command/ep_command.go#L118
		_, err := client.Get(ctx, path.Join(c.Prefix, "health"))
		if err == nil {
			return nil
		}
		return fmt.Errorf("error getting data from etcd: %v", err)
	}, nil
}

func newETCD3Client(c storagebackend.TransportConfig) (*clientv3.Client, error) {
	tlsInfo := transport.TLSInfo{
		CertFile: c.CertFile,
		KeyFile:  c.KeyFile,
		CAFile:   c.CAFile,
	}
	tlsConfig, err := tlsInfo.ClientConfig()
	if err != nil {
		return nil, err
	}
	// NOTE: Client relies on nil tlsConfig
	// for non-secure connections, update the implicit variable
	if len(c.CertFile) == 0 && len(c.KeyFile) == 0 && len(c.CAFile) == 0 {
		tlsConfig = nil
	}
	cfg := clientv3.Config{
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
		DialOptions: []grpc.DialOption{
			grpc.WithUnaryInterceptor(grpcprom.UnaryClientInterceptor),
			grpc.WithStreamInterceptor(grpcprom.StreamClientInterceptor),
		},
		Endpoints: c.ServerList,
		TLS:       tlsConfig,
	}

	return clientv3.New(cfg)
}

type runningCompactor struct {
	interval time.Duration
	cancel   context.CancelFunc
	client   *clientv3.Client
	refs     int
}

var (
	lock       sync.Mutex
	compactors = map[string]*runningCompactor{}
)

// startCompactorOnce start one compactor per transport. If the interval get smaller on repeated calls, the
// compactor is replaced. A destroy func is returned. If all destroy funcs with the same transport are called,
// the compactor is stopped.
func startCompactorOnce(c storagebackend.TransportConfig, interval time.Duration) (func(), error) {
	lock.Lock()
	defer lock.Unlock()

	key := fmt.Sprintf("%v", c) // gives: {[server1 server2] keyFile certFile caFile}
	if compactor, foundBefore := compactors[key]; !foundBefore || compactor.interval > interval {
		compactorClient, err := newETCD3Client(c)
		if err != nil {
			return nil, err
		}

		if foundBefore {
			// replace compactor
			compactor.cancel()
			compactor.client.Close()
		} else {
			// start new compactor
			compactor = &runningCompactor{}
			compactors[key] = compactor
		}

		ctx, cancel := context.WithCancel(context.Background())

		compactor.interval = interval
		compactor.cancel = cancel
		compactor.client = compactorClient

		etcd3.StartCompactor(ctx, compactorClient, interval)
	}

	compactors[key].refs++

	return func() {
		lock.Lock()
		defer lock.Unlock()

		compactor := compactors[key]
		compactor.refs--
		if compactor.refs == 0 {
			compactor.cancel()
			compactor.client.Close()
			delete(compactors, key)
		}
	}, nil
}

func newETCD3Storage(c storagebackend.Config) (storage.Interface, DestroyFunc, error) {
	stopCompactor, err := startCompactorOnce(c.Transport, c.CompactionInterval)
	if err != nil {
		return nil, nil, err
	}

	client, err := newETCD3Client(c.Transport)
	if err != nil {
		stopCompactor()
		return nil, nil, err
	}

	var once sync.Once
	destroyFunc := func() {
		// we know that storage destroy funcs are called multiple times (due to reuse in subresources).
		// Hence, we only destroy once.
		// TODO: fix duplicated storage destroy calls higher level
		once.Do(func() {
			stopCompactor()
			client.Close()
		})
	}
	transformer := c.Transformer
	if transformer == nil {
		transformer = value.IdentityTransformer
	}
	return etcd3.New(client, c.Codec, c.Prefix, transformer, c.Paging), destroyFunc, nil
}
<commit_msg>k8s/apiextensions-apiserver/test/integration: block etcd client creation until connection is up<commit_after>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package factory

import (
	"context"
	"fmt"
	"path"
	"sync"
	"sync/atomic"
	"time"

	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/pkg/transport"
	grpcprom "github.com/grpc-ecosystem/go-grpc-prometheus"
	"google.golang.org/grpc"

	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/etcd3"
	"k8s.io/apiserver/pkg/storage/storagebackend"
	"k8s.io/apiserver/pkg/storage/value"
	"k8s.io/component-base/metrics/legacyregistry"
)

// The short keepalive timeout and interval have been chosen to aggressively
// detect a failed etcd server without introducing much overhead.
const keepaliveTime = 30 * time.Second
const keepaliveTimeout = 10 * time.Second

// dialTimeout is the timeout for failing to establish a connection.
// It is set to 20 seconds as times shorter than that will cause TLS connections to fail
// on heavily loaded arm64 CPUs (issue #64649)
const dialTimeout = 20 * time.Second

func init() {
	// grpcprom auto-registers (via an init function) their client metrics, since we are opting out of
	// using the global prometheus registry and using our own wrapped global registry,
	// we need to explicitly register these metrics to our global registry here.
	// For reference: https://github.com/kubernetes/kubernetes/pull/81387
	legacyregistry.RawMustRegister(grpcprom.DefaultClientMetrics)
}

func newETCD3HealthCheck(c storagebackend.Config) (func() error, error) {
	// constructing the etcd v3 client blocks and times out if etcd is not available.
	// retry in a loop in the background until we successfully create the client, storing the client or error encountered

	clientValue := &atomic.Value{}

	clientErrMsg := &atomic.Value{}
	clientErrMsg.Store("etcd client connection not yet established")

	go wait.PollUntil(time.Second, func() (bool, error) {
		client, err := newETCD3Client(c.Transport)
		if err != nil {
			clientErrMsg.Store(err.Error())
			return false, nil
		}
		clientValue.Store(client)
		clientErrMsg.Store("")
		return true, nil
	}, wait.NeverStop)

	return func() error {
		if errMsg := clientErrMsg.Load().(string); len(errMsg) > 0 {
			return fmt.Errorf(errMsg)
		}
		client := clientValue.Load().(*clientv3.Client)
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		// See https://github.com/etcd-io/etcd/blob/master/etcdctl/ctlv3/command/ep_command.go#L118
		_, err := client.Get(ctx, path.Join(c.Prefix, "health"))
		if err == nil {
			return nil
		}
		return fmt.Errorf("error getting data from etcd: %v", err)
	}, nil
}

func newETCD3Client(c storagebackend.TransportConfig) (*clientv3.Client, error) {
	tlsInfo := transport.TLSInfo{
		CertFile: c.CertFile,
		KeyFile:  c.KeyFile,
		CAFile:   c.CAFile,
	}
	tlsConfig, err := tlsInfo.ClientConfig()
	if err != nil {
		return nil, err
	}
	// NOTE: Client relies on nil tlsConfig
	// for non-secure connections, update the implicit variable
	if len(c.CertFile) == 0 && len(c.KeyFile) == 0 && len(c.CAFile) == 0 {
		tlsConfig = nil
	}
	cfg := clientv3.Config{
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
		DialOptions: []grpc.DialOption{
			grpc.WithBlock(), // block until the underlying connection is up
			grpc.WithUnaryInterceptor(grpcprom.UnaryClientInterceptor),
			grpc.WithStreamInterceptor(grpcprom.StreamClientInterceptor),
		},
		Endpoints: c.ServerList,
		TLS:       tlsConfig,
	}

	return clientv3.New(cfg)
}

type runningCompactor struct {
	interval time.Duration
	cancel   context.CancelFunc
	client   *clientv3.Client
	refs     int
}

var (
	lock       sync.Mutex
	compactors = map[string]*runningCompactor{}
)

// startCompactorOnce start one compactor per transport. If the interval get smaller on repeated calls, the
// compactor is replaced. A destroy func is returned. If all destroy funcs with the same transport are called,
// the compactor is stopped.
func startCompactorOnce(c storagebackend.TransportConfig, interval time.Duration) (func(), error) {
	lock.Lock()
	defer lock.Unlock()

	key := fmt.Sprintf("%v", c) // gives: {[server1 server2] keyFile certFile caFile}
	if compactor, foundBefore := compactors[key]; !foundBefore || compactor.interval > interval {
		compactorClient, err := newETCD3Client(c)
		if err != nil {
			return nil, err
		}

		if foundBefore {
			// replace compactor
			compactor.cancel()
			compactor.client.Close()
		} else {
			// start new compactor
			compactor = &runningCompactor{}
			compactors[key] = compactor
		}

		ctx, cancel := context.WithCancel(context.Background())

		compactor.interval = interval
		compactor.cancel = cancel
		compactor.client = compactorClient

		etcd3.StartCompactor(ctx, compactorClient, interval)
	}

	compactors[key].refs++

	return func() {
		lock.Lock()
		defer lock.Unlock()

		compactor := compactors[key]
		compactor.refs--
		if compactor.refs == 0 {
			compactor.cancel()
			compactor.client.Close()
			delete(compactors, key)
		}
	}, nil
}

func newETCD3Storage(c storagebackend.Config) (storage.Interface, DestroyFunc, error) {
	stopCompactor, err := startCompactorOnce(c.Transport, c.CompactionInterval)
	if err != nil {
		return nil, nil, err
	}

	client, err := newETCD3Client(c.Transport)
	if err != nil {
		stopCompactor()
		return nil, nil, err
	}

	var once sync.Once
	destroyFunc := func() {
		// we know that storage destroy funcs are called multiple times (due to reuse in subresources).
		// Hence, we only destroy once.
		// TODO: fix duplicated storage destroy calls higher level
		once.Do(func() {
			stopCompactor()
			client.Close()
		})
	}
	transformer := c.Transformer
	if transformer == nil {
		transformer = value.IdentityTransformer
	}
	return etcd3.New(client, c.Codec, c.Prefix, transformer, c.Paging), destroyFunc, nil
}
<|endoftext|>