<commit_before>package services

import (
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path"
	"runtime"
	"strconv"

	"github.com/pkg/errors"
	"github.com/yext/edward/common"
	"github.com/yext/edward/warmup"
	"github.com/yext/errgo"
)

var _ ServiceOrGroup = &ServiceConfig{}

// ServiceConfig represents a service that can be managed by Edward
type ServiceConfig struct {
	// Service name, used to identify in commands
	Name string `json:"name"`
	// Alternative names for this service
	Aliases []string `json:"aliases,omitempty"`
	// Service description
	Description string `json:"description,omitempty"`
	// Optional path to service. If nil, uses cwd
	Path *string `json:"path,omitempty"`

	// Does this service require sudo privileges?
	RequiresSudo bool `json:"requiresSudo,omitempty"`

	// Env holds environment variables for a service, for example: GOPATH=~/gocode/
	// These will be added to the vars in the environment under which the Edward command was run
	Env []string `json:"env,omitempty"`

	Platform string `json:"platform,omitempty"`

	// Path to watch for updates, relative to config file. If specified, will enable hot reloading.
	WatchJSON json.RawMessage `json:"watch,omitempty"`

	// Action for warming up this service
	Warmup *warmup.Warmup `json:"warmup,omitempty"`

	// Path to config file from which this service was loaded
	// This may be the file that imported the config containing the service definition.
	ConfigFile string `json:"-"`

	// Logger for actions on this service
	Logger common.Logger `json:"-"`

	Backends []*BackendConfig `json:"backends"`
}

// Backend returns the default backend for this service
func (c *ServiceConfig) Backend() Backend {
	for _, backendConfig := range c.Backends {
		return backendConfig.Config
	}
	return nil
}

// BackendConfig provides backend configuration for json
type BackendConfig struct {
	Name   string  `json:"name"`
	Type   string  `json:"type"`
	Config Backend `json:"-"`

	ConfigByte []byte `json:"config"`
}

var _ json.Marshaler = &BackendConfig{}
var _ json.Unmarshaler = &BackendConfig{}

func (c *BackendConfig) UnmarshalJSON(data []byte) error {
	type Alias BackendConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return errors.Wrap(err, "could not parse backend entry")
	}
	var (
		loader BackendLoader
		ok     bool
	)
	if loader, ok = loaders[aux.Type]; !ok {
		return fmt.Errorf("unknown config type: %s", aux.Type)
	}
	c.Config = loader.New()
	if err := json.Unmarshal(c.ConfigByte, &c.Config); err != nil {
		return errors.Wrap(err, "could not parse backend config")
	}
	return nil
}

func (c *BackendConfig) MarshalJSON() ([]byte, error) {
	if c.Type == "" {
		return nil, errors.New("no type specified for backend")
	}
	type Alias BackendConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	var err error
	aux.ConfigByte, err = json.Marshal(c.Config)
	if err != nil {
		return nil, errgo.Mask(err)
	}
	return json.Marshal(aux)
}

// Matches returns true if the service name or an alias matches the provided name.
func (c *ServiceConfig) Matches(name string) bool {
	if c.Name == name {
		return true
	}
	for _, alias := range c.Aliases {
		if alias == name {
			return true
		}
	}
	return false
}

// UnmarshalJSON provides additional handling when unmarshaling a service from config.
// Currently, this handles legacy fields and fields with multiple possible types.
func (c *ServiceConfig) UnmarshalJSON(data []byte) error {
	type Alias ServiceConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	if err := json.Unmarshal(data, aux); err != nil {
		return errors.Wrap(err, "could not parse service config")
	}
	for _, m := range legacyUnmarshalers {
		if err := m.Unmarshal(data, c); err != nil {
			return errors.Wrap(err, "could not parse legacy configuration")
		}
	}

	return nil
}

// SetWatch sets the watch configuration for this service
func (c *ServiceConfig) SetWatch(watch ServiceWatch) error {
	msg, err := json.Marshal(watch)
	if err != nil {
		return errors.WithStack(err)
	}
	c.WatchJSON = json.RawMessage(msg)
	return nil
}

// Watch returns the watch configuration for this service
func (c *ServiceConfig) Watch() ([]ServiceWatch, error) {
	var watch = ServiceWatch{
		Service: c,
	}

	if len(c.WatchJSON) == 0 {
		return nil, nil
	}

	var err error

	// Handle multiple
	err = json.Unmarshal(c.WatchJSON, &watch)
	if err == nil {
		return []ServiceWatch{watch}, nil
	}

	// Handle string version
	var include string
	err = json.Unmarshal(c.WatchJSON, &include)
	if err != nil {
		return nil, err
	}
	if include != "" {
		watch.IncludedPaths = append(watch.IncludedPaths, include)
		return []ServiceWatch{watch}, nil
	}

	return nil, nil
}

// ServiceWatch defines a set of directories to be watched for changes to a service's source.
type ServiceWatch struct {
	Service       *ServiceConfig `json:"-"`
	IncludedPaths []string       `json:"include,omitempty"`
	ExcludedPaths []string       `json:"exclude,omitempty"`
}

// MatchesPlatform determines whether or not this service can be run on the current OS
func (c *ServiceConfig) MatchesPlatform() bool {
	return len(c.Platform) == 0 || c.Platform == runtime.GOOS
}

// GetName returns the name for this service
func (c *ServiceConfig) GetName() string {
	return c.Name
}

// GetDescription returns the description for this service
func (c *ServiceConfig) GetDescription() string {
	return c.Description
}

// IsSudo returns true if this service requires sudo to run.
// If this service is excluded by cfg, then will always return false.
func (c *ServiceConfig) IsSudo(cfg OperationConfig) bool {
	if cfg.IsExcluded(c) {
		return false
	}

	return c.RequiresSudo
}

// GetRunLog returns the path to the run log for this service
func (c *ServiceConfig) GetRunLog(logDir string) string {
	return path.Join(logDir, c.Name+".log")
}

// IdentifyingFilename returns a filename that can be used to identify this service uniquely among all services
// that may be configured on a machine.
// The filename will be based on the service name and the path to its Edward config. It does not include an extension.
func (c *ServiceConfig) IdentifyingFilename() string {
	name := c.Name
	hasher := sha1.New()
	hasher.Write([]byte(c.ConfigFile))
	sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
	return fmt.Sprintf("%v.%v", sha, name)
}

func (c *ServiceConfig) GetPid(pidFile string) (int, error) {
	dat, err := ioutil.ReadFile(pidFile)
	if err != nil {
		return 0, errors.WithStack(err)
	}
	pid, err := strconv.Atoi(string(dat))
	if err != nil {
		return 0, errors.WithStack(err)
	}
	return pid, nil
}

func (c *ServiceConfig) GetStateBase(stateDir string) string {
	return path.Join(stateDir, c.IdentifyingFilename())
}

func (c *ServiceConfig) GetStatePath(stateDir string) string {
	return fmt.Sprintf("%v.state", c.GetStateBase(stateDir))
}

func (c *ServiceConfig) GetPidPathLegacy(pidDir string) string {
	name := c.Name
	return path.Join(pidDir, fmt.Sprintf("%v.pid", name))
}
<commit_msg>Remove use of errgo that snuck in.<commit_after>package services

import (
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"path"
	"runtime"
	"strconv"

	"github.com/pkg/errors"
	"github.com/yext/edward/common"
	"github.com/yext/edward/warmup"
)

var _ ServiceOrGroup = &ServiceConfig{}

// ServiceConfig represents a service that can be managed by Edward
type ServiceConfig struct {
	// Service name, used to identify in commands
	Name string `json:"name"`
	// Alternative names for this service
	Aliases []string `json:"aliases,omitempty"`
	// Service description
	Description string `json:"description,omitempty"`
	// Optional path to service. If nil, uses cwd
	Path *string `json:"path,omitempty"`

	// Does this service require sudo privileges?
	RequiresSudo bool `json:"requiresSudo,omitempty"`

	// Env holds environment variables for a service, for example: GOPATH=~/gocode/
	// These will be added to the vars in the environment under which the Edward command was run
	Env []string `json:"env,omitempty"`

	Platform string `json:"platform,omitempty"`

	// Path to watch for updates, relative to config file. If specified, will enable hot reloading.
	WatchJSON json.RawMessage `json:"watch,omitempty"`

	// Action for warming up this service
	Warmup *warmup.Warmup `json:"warmup,omitempty"`

	// Path to config file from which this service was loaded
	// This may be the file that imported the config containing the service definition.
	ConfigFile string `json:"-"`

	// Logger for actions on this service
	Logger common.Logger `json:"-"`

	Backends []*BackendConfig `json:"backends"`
}

// Backend returns the default backend for this service
func (c *ServiceConfig) Backend() Backend {
	for _, backendConfig := range c.Backends {
		return backendConfig.Config
	}
	return nil
}

// BackendConfig provides backend configuration for json
type BackendConfig struct {
	Name   string  `json:"name"`
	Type   string  `json:"type"`
	Config Backend `json:"-"`

	ConfigByte []byte `json:"config"`
}

var _ json.Marshaler = &BackendConfig{}
var _ json.Unmarshaler = &BackendConfig{}

func (c *BackendConfig) UnmarshalJSON(data []byte) error {
	type Alias BackendConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return errors.Wrap(err, "could not parse backend entry")
	}
	var (
		loader BackendLoader
		ok     bool
	)
	if loader, ok = loaders[aux.Type]; !ok {
		return fmt.Errorf("unknown config type: %s", aux.Type)
	}
	c.Config = loader.New()
	if err := json.Unmarshal(c.ConfigByte, &c.Config); err != nil {
		return errors.Wrap(err, "could not parse backend config")
	}
	return nil
}

func (c *BackendConfig) MarshalJSON() ([]byte, error) {
	if c.Type == "" {
		return nil, errors.New("no type specified for backend")
	}
	type Alias BackendConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	var err error
	aux.ConfigByte, err = json.Marshal(c.Config)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	return json.Marshal(aux)
}

// Matches returns true if the service name or an alias matches the provided name.
func (c *ServiceConfig) Matches(name string) bool {
	if c.Name == name {
		return true
	}
	for _, alias := range c.Aliases {
		if alias == name {
			return true
		}
	}
	return false
}

// UnmarshalJSON provides additional handling when unmarshaling a service from config.
// Currently, this handles legacy fields and fields with multiple possible types.
func (c *ServiceConfig) UnmarshalJSON(data []byte) error {
	type Alias ServiceConfig
	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(c),
	}
	if err := json.Unmarshal(data, aux); err != nil {
		return errors.Wrap(err, "could not parse service config")
	}
	for _, m := range legacyUnmarshalers {
		if err := m.Unmarshal(data, c); err != nil {
			return errors.Wrap(err, "could not parse legacy configuration")
		}
	}

	return nil
}

// SetWatch sets the watch configuration for this service
func (c *ServiceConfig) SetWatch(watch ServiceWatch) error {
	msg, err := json.Marshal(watch)
	if err != nil {
		return errors.WithStack(err)
	}
	c.WatchJSON = json.RawMessage(msg)
	return nil
}

// Watch returns the watch configuration for this service
func (c *ServiceConfig) Watch() ([]ServiceWatch, error) {
	var watch = ServiceWatch{
		Service: c,
	}

	if len(c.WatchJSON) == 0 {
		return nil, nil
	}

	var err error

	// Handle multiple
	err = json.Unmarshal(c.WatchJSON, &watch)
	if err == nil {
		return []ServiceWatch{watch}, nil
	}

	// Handle string version
	var include string
	err = json.Unmarshal(c.WatchJSON, &include)
	if err != nil {
		return nil, err
	}
	if include != "" {
		watch.IncludedPaths = append(watch.IncludedPaths, include)
		return []ServiceWatch{watch}, nil
	}

	return nil, nil
}

// ServiceWatch defines a set of directories to be watched for changes to a service's source.
type ServiceWatch struct {
	Service       *ServiceConfig `json:"-"`
	IncludedPaths []string       `json:"include,omitempty"`
	ExcludedPaths []string       `json:"exclude,omitempty"`
}

// MatchesPlatform determines whether or not this service can be run on the current OS
func (c *ServiceConfig) MatchesPlatform() bool {
	return len(c.Platform) == 0 || c.Platform == runtime.GOOS
}

// GetName returns the name for this service
func (c *ServiceConfig) GetName() string {
	return c.Name
}

// GetDescription returns the description for this service
func (c *ServiceConfig) GetDescription() string {
	return c.Description
}

// IsSudo returns true if this service requires sudo to run.
// If this service is excluded by cfg, then will always return false.
func (c *ServiceConfig) IsSudo(cfg OperationConfig) bool {
	if cfg.IsExcluded(c) {
		return false
	}

	return c.RequiresSudo
}

// GetRunLog returns the path to the run log for this service
func (c *ServiceConfig) GetRunLog(logDir string) string {
	return path.Join(logDir, c.Name+".log")
}

// IdentifyingFilename returns a filename that can be used to identify this service uniquely among all services
// that may be configured on a machine.
// The filename will be based on the service name and the path to its Edward config. It does not include an extension.
func (c *ServiceConfig) IdentifyingFilename() string {
	name := c.Name
	hasher := sha1.New()
	hasher.Write([]byte(c.ConfigFile))
	sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
	return fmt.Sprintf("%v.%v", sha, name)
}

func (c *ServiceConfig) GetPid(pidFile string) (int, error) {
	dat, err := ioutil.ReadFile(pidFile)
	if err != nil {
		return 0, errors.WithStack(err)
	}
	pid, err := strconv.Atoi(string(dat))
	if err != nil {
		return 0, errors.WithStack(err)
	}
	return pid, nil
}

func (c *ServiceConfig) GetStateBase(stateDir string) string {
	return path.Join(stateDir, c.IdentifyingFilename())
}

func (c *ServiceConfig) GetStatePath(stateDir string) string {
	return fmt.Sprintf("%v.state", c.GetStateBase(stateDir))
}

func (c *ServiceConfig) GetPidPathLegacy(pidDir string) string {
	name := c.Name
	return path.Join(pidDir, fmt.Sprintf("%v.pid", name))
}
<|endoftext|>


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

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

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

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

package controller

import (
	"fmt"
	"net"
	"time"

	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/errors"
	client "k8s.io/kubernetes/pkg/client/unversioned"
	"k8s.io/kubernetes/pkg/registry/service"
	"k8s.io/kubernetes/pkg/registry/service/ipallocator"
	"k8s.io/kubernetes/pkg/util"
	"k8s.io/kubernetes/pkg/util/wait"
)

// Repair is a controller loop that periodically examines all service ClusterIP allocations
// and logs any errors, and then sets the compacted and accurate list of all allocated IPs.
//
// Handles:
// * Duplicate ClusterIP assignments caused by operator action or undetected race conditions
// * ClusterIPs that do not match the currently configured range
// * Allocations to services that were not actually created due to a crash or powerloss
// * Migrates old versions of Kubernetes services into the atomic ipallocator model automatically
//
// Can be run at infrequent intervals, and is best performed on startup of the master.
// Is level driven and idempotent - all valid ClusterIPs will be updated into the ipallocator
// map at the end of a single execution loop if no race is encountered.
//
// TODO: allocate new IPs if necessary
// TODO: perform repair?
type Repair struct {
	interval time.Duration
	registry service.Registry
	network  *net.IPNet
	alloc    service.RangeRegistry
}

// NewRepair creates a controller that periodically ensures that all clusterIPs are uniquely allocated across the cluster
// and generates informational warnings for a cluster that is not in sync.
func NewRepair(interval time.Duration, registry service.Registry, network *net.IPNet, alloc service.RangeRegistry) *Repair {
	return &Repair{
		interval: interval,
		registry: registry,
		network:  network,
		alloc:    alloc,
	}
}

// RunUntil starts the controller until the provided ch is closed.
func (c *Repair) RunUntil(ch chan struct{}) {
	util.Until(func() {
		if err := c.RunOnce(); err != nil {
			util.HandleError(err)
		}
	}, c.interval, ch)
}

// RunOnce verifies the state of the cluster IP allocations and returns an error if an unrecoverable problem occurs.
func (c *Repair) RunOnce() error {
	return client.RetryOnConflict(client.DefaultBackoff, c.runOnce)
}

// runOnce verifies the state of the cluster IP allocations and returns an error if an unrecoverable problem occurs.
func (c *Repair) runOnce() error {
	// TODO: (per smarterclayton) if Get() or ListServices() is a weak consistency read,
	// or if they are executed against different leaders,
	// the ordering guarantee required to ensure no IP is allocated twice is violated.
	// ListServices must return a ResourceVersion higher than the etcd index Get triggers,
	// and the release code must not release services that have had IPs allocated but not yet been created
	// See #8295

	// If etcd server is not running we should wait for some time and fail only then. This is particularly
	// important when we start apiserver and etcd at the same time.
	var latest *api.RangeAllocation
	var err error
	err = wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error) {
		latest, err = c.alloc.Get()
		return err == nil, err
	})
	if err != nil {
		return fmt.Errorf("unable to refresh the service IP block: %v", err)
	}

	ctx := api.WithNamespace(api.NewDefaultContext(), api.NamespaceAll)
	options := &api.ListOptions{ResourceVersion: latest.ObjectMeta.ResourceVersion}
	list, err := c.registry.ListServices(ctx, options)
	if err != nil {
		return fmt.Errorf("unable to refresh the service IP block: %v", err)
	}

	r := ipallocator.NewCIDRRange(c.network)
	for _, svc := range list.Items {
		if !api.IsServiceIPSet(&svc) {
			continue
		}
		ip := net.ParseIP(svc.Spec.ClusterIP)
		if ip == nil {
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s is not a valid IP; please recreate", svc.Spec.ClusterIP, svc.Name, svc.Namespace))
			continue
		}
		switch err := r.Allocate(ip); err {
		case nil:
		case ipallocator.ErrAllocated:
			// TODO: send event
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s was assigned to multiple services; please recreate", ip, svc.Name, svc.Namespace))
		case ipallocator.ErrNotInRange:
			// TODO: send event
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s is not within the service CIDR %s; please recreate", ip, svc.Name, svc.Namespace, c.network))
		case ipallocator.ErrFull:
			// TODO: send event
			return fmt.Errorf("the service CIDR %v is full; you must widen the CIDR in order to create new services", r)
		default:
			return fmt.Errorf("unable to allocate cluster IP %s for service %s/%s due to an unknown error, exiting: %v", ip, svc.Name, svc.Namespace, err)
		}
	}

	if err := r.Snapshot(latest); err != nil {
		return fmt.Errorf("unable to snapshot the updated service IP allocations: %v", err)
	}

	if err := c.alloc.CreateOrUpdate(latest); err != nil {
		if errors.IsConflict(err) {
			return err
		}
		return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
	}
	return nil
}
<commit_msg>don't cross-pollinate RVs<commit_after>/*
Copyright 2015 The Kubernetes Authors All rights reserved.

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

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

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

package controller

import (
	"fmt"
	"net"
	"time"

	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/errors"
	client "k8s.io/kubernetes/pkg/client/unversioned"
	"k8s.io/kubernetes/pkg/registry/service"
	"k8s.io/kubernetes/pkg/registry/service/ipallocator"
	"k8s.io/kubernetes/pkg/util"
	"k8s.io/kubernetes/pkg/util/wait"
)

// Repair is a controller loop that periodically examines all service ClusterIP allocations
// and logs any errors, and then sets the compacted and accurate list of all allocated IPs.
//
// Handles:
// * Duplicate ClusterIP assignments caused by operator action or undetected race conditions
// * ClusterIPs that do not match the currently configured range
// * Allocations to services that were not actually created due to a crash or powerloss
// * Migrates old versions of Kubernetes services into the atomic ipallocator model automatically
//
// Can be run at infrequent intervals, and is best performed on startup of the master.
// Is level driven and idempotent - all valid ClusterIPs will be updated into the ipallocator
// map at the end of a single execution loop if no race is encountered.
//
// TODO: allocate new IPs if necessary
// TODO: perform repair?
type Repair struct {
	interval time.Duration
	registry service.Registry
	network  *net.IPNet
	alloc    service.RangeRegistry
}

// NewRepair creates a controller that periodically ensures that all clusterIPs are uniquely allocated across the cluster
// and generates informational warnings for a cluster that is not in sync.
func NewRepair(interval time.Duration, registry service.Registry, network *net.IPNet, alloc service.RangeRegistry) *Repair {
	return &Repair{
		interval: interval,
		registry: registry,
		network:  network,
		alloc:    alloc,
	}
}

// RunUntil starts the controller until the provided ch is closed.
func (c *Repair) RunUntil(ch chan struct{}) {
	util.Until(func() {
		if err := c.RunOnce(); err != nil {
			util.HandleError(err)
		}
	}, c.interval, ch)
}

// RunOnce verifies the state of the cluster IP allocations and returns an error if an unrecoverable problem occurs.
func (c *Repair) RunOnce() error {
	return client.RetryOnConflict(client.DefaultBackoff, c.runOnce)
}

// runOnce verifies the state of the cluster IP allocations and returns an error if an unrecoverable problem occurs.
func (c *Repair) runOnce() error {
	// TODO: (per smarterclayton) if Get() or ListServices() is a weak consistency read,
	// or if they are executed against different leaders,
	// the ordering guarantee required to ensure no IP is allocated twice is violated.
	// ListServices must return a ResourceVersion higher than the etcd index Get triggers,
	// and the release code must not release services that have had IPs allocated but not yet been created
	// See #8295

	// If etcd server is not running we should wait for some time and fail only then. This is particularly
	// important when we start apiserver and etcd at the same time.
	var latest *api.RangeAllocation
	var err error
	err = wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error) {
		latest, err = c.alloc.Get()
		return err == nil, err
	})
	if err != nil {
		return fmt.Errorf("unable to refresh the service IP block: %v", err)
	}

	ctx := api.WithNamespace(api.NewDefaultContext(), api.NamespaceAll)
	// We explicitly send no resource version, since the resource version
	// of 'latest' is from a different collection, it's not comparable to
	// the service collection. The caching layer keeps per-collection RVs,
	// and this is proper, since in theory the collections could be hosted
	// in separate etcd (or even non-etcd) instances.
	list, err := c.registry.ListServices(ctx, nil)
	if err != nil {
		return fmt.Errorf("unable to refresh the service IP block: %v", err)
	}

	r := ipallocator.NewCIDRRange(c.network)
	for _, svc := range list.Items {
		if !api.IsServiceIPSet(&svc) {
			continue
		}
		ip := net.ParseIP(svc.Spec.ClusterIP)
		if ip == nil {
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s is not a valid IP; please recreate", svc.Spec.ClusterIP, svc.Name, svc.Namespace))
			continue
		}
		switch err := r.Allocate(ip); err {
		case nil:
		case ipallocator.ErrAllocated:
			// TODO: send event
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s was assigned to multiple services; please recreate", ip, svc.Name, svc.Namespace))
		case ipallocator.ErrNotInRange:
			// TODO: send event
			// cluster IP is broken, reallocate
			util.HandleError(fmt.Errorf("the cluster IP %s for service %s/%s is not within the service CIDR %s; please recreate", ip, svc.Name, svc.Namespace, c.network))
		case ipallocator.ErrFull:
			// TODO: send event
			return fmt.Errorf("the service CIDR %v is full; you must widen the CIDR in order to create new services", r)
		default:
			return fmt.Errorf("unable to allocate cluster IP %s for service %s/%s due to an unknown error, exiting: %v", ip, svc.Name, svc.Namespace, err)
		}
	}

	if err := r.Snapshot(latest); err != nil {
		return fmt.Errorf("unable to snapshot the updated service IP allocations: %v", err)
	}

	if err := c.alloc.CreateOrUpdate(latest); err != nil {
		if errors.IsConflict(err) {
			return err
		}
		return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
	}
	return nil
}
<|endoftext|>


<commit_before>package docker

import (
	"bufio"
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"os"
	"strings"
)

const configurationFile = `./users`
const admin = `admin`

var commaByte = []byte(`,`)

type user struct {
	username string
	password string
	role     string
}

var users map[string]*user

func init() {
	users = readConfiguration(configurationFile)
}

func readConfiguration(path string) map[string]*user {
	configFile, err := os.Open(path)
	defer configFile.Close()

	if err != nil {
		log.Print(err)
		return nil
	}

	users := make(map[string]*user)

	scanner := bufio.NewScanner(configFile)
	for scanner.Scan() {
		parts := bytes.Split(scanner.Bytes(), commaByte)
		user := user{string(parts[0]), string(parts[1]), string(parts[2])}

		users[strings.ToLower(user.username)] = &user
	}

	return users
}

func isAllowed(loggedUser *user, containerID string) (bool, error) {
	if !isAdmin(loggedUser) {
		container, err := inspectContainer(string(containerID))
		if err != nil {
			return false, err
		}

		owner, ok := container.Config.Labels[ownerLabel]
		if !ok || owner != loggedUser.username {
			return false, nil
		}
	}

	return true, nil
}

func isAuthenticatedByBasicAuth(base64value string) (*user, error) {
	data, err := base64.StdEncoding.DecodeString(base64value)
	if err != nil {
		return nil, fmt.Errorf(`Unable to read basic authentication`)
	}

	dataStr := string(data)

	sepIndex := strings.IndexByte(dataStr, ':')
	if sepIndex < 0 {
		return nil, fmt.Errorf(`Unable to read basic authentication`)
	}

	return isAuthenticated(dataStr[:sepIndex], dataStr[sepIndex+1:], err != nil)
}

func isAuthenticated(username string, password string, ok bool) (*user, error) {
	if ok {
		user, ok := users[strings.ToLower(username)]

		if ok && user.password == password {
			return user, nil
		}
		return nil, fmt.Errorf(`Invalid credentials for ` + username)
	}

	return nil, fmt.Errorf(`Unable to read basic authentication`)
}

func isAdmin(loggedUser *user) bool {
	if loggedUser != nil {
		return loggedUser.role == admin
	}
	return false
}
<commit_msg>Update user.go<commit_after>package docker

import (
	"bufio"
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"os"
	"strings"
)

const configurationFile = `./users`
const admin = `admin`

var commaByte = []byte(`,`)

type user struct {
	username string
	password string
	role     string
}

var users map[string]*user

func init() {
	users = readConfiguration(configurationFile)
}

func readConfiguration(path string) map[string]*user {
	configFile, err := os.Open(path)
	defer configFile.Close()

	if err != nil {
		log.Print(err)
		return nil
	}

	users := make(map[string]*user)

	scanner := bufio.NewScanner(configFile)
	for scanner.Scan() {
		parts := bytes.Split(scanner.Bytes(), commaByte)
		user := user{string(parts[0]), string(parts[1]), string(parts[2])}

		users[strings.ToLower(user.username)] = &user
	}

	return users
}

func isAllowed(loggedUser *user, containerID string) (bool, error) {
	if !isAdmin(loggedUser) {
		container, err := inspectContainer(string(containerID))
		if err != nil {
			return false, err
		}

		owner, ok := container.Config.Labels[ownerLabel]
		if !ok || owner != loggedUser.username {
			return false, nil
		}
	}

	return true, nil
}

func isAuthenticatedByBasicAuth(base64value string) (*user, error) {
	data, err := base64.StdEncoding.DecodeString(base64value)
	if err != nil {
		return nil, fmt.Errorf(`Unable to read basic authentication`)
	}

	dataStr := string(data)

	sepIndex := strings.IndexByte(dataStr, ':')
	if sepIndex < 0 {
		return nil, fmt.Errorf(`Unable to read basic authentication`)
	}

	return isAuthenticated(dataStr[:sepIndex], dataStr[sepIndex+1:], true)
}

func isAuthenticated(username string, password string, ok bool) (*user, error) {
	if ok {
		user, ok := users[strings.ToLower(username)]

		if ok && user.password == password {
			return user, nil
		}
		return nil, fmt.Errorf(`Invalid credentials for ` + username)
	}

	return nil, fmt.Errorf(`Unable to read basic authentication`)
}

func isAdmin(loggedUser *user) bool {
	if loggedUser != nil {
		return loggedUser.role == admin
	}
	return false
}
<|endoftext|>


<commit_before>package main

import (
	"crypto/tls"
	"errors"
	"flag"
	"github.com/miekg/dns"
	"github.com/rrva/dockerclient"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

func registerShutdownSignalHandlers() {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
	for s := range sigChan {
		log.Fatalf("Signal (%s) received, shutting down", s)
	}
}

func matches(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

type lookupResponse struct {
	response *string
	err      error
}

type lookupFn func(string) (*string, error)

func lookupWithTimeout(n string, t time.Duration, lookup lookupFn) (*string, error) {

	lookupResponses := make(chan lookupResponse)
	go func(n string) {
		name, err := lookup(n)
		lookupResponses <- lookupResponse{response: name, err: err}
	}(n)

	timeouts := make(chan bool, 1)
	go func() {
		<-time.After(t)
		timeouts <- true
	}()

	for {
		select {
		case r := <-lookupResponses:
			return r.response, r.err
		case <-timeouts:
			return nil, errors.New("Timeout occurred talking to docker")
		}
	}
}

func findContainerNameByIPAddress(n string) (*string, error) {
	containers, err := docker.ListContainers(false, false, "")
	if err != nil {
		return nil, err
	}
	for _, container := range containers {
		info, _ := docker.InspectContainer(container.Id)
		if info.NetworkSettings.IpAddress == n {
			if len(container.Names) > 0 {
				str := container.Names[0][1:]
				return &str, nil
			}
			return nil, nil
		}
	}
	return nil, nil
}

func findContainerIPAddressByName(n string) (*string, error) {
	containers, err := docker.ListContainers(false, false, "")
	if err != nil {
		return nil, err
	}
	for _, container := range containers {
		if matches(container.Names, n) {
			info, err := docker.InspectContainer(container.Id)
			if err != nil {
				return nil, err
			}
			if info != nil {
				return &info.NetworkSettings.IpAddress, nil
			}
		}
	}
	return nil, nil
}

func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) {
	err := w.WriteMsg(m)
	if err != nil {
		log.Printf("Failed writing DNS response: %s", err)
	}
}

func myselfAsResolverErrorResponse(w dns.ResponseWriter, m *dns.Msg, name string) {
	log.Printf("Oh my! I'm pointing to myself!")
	rr := new(dns.A)
	rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}
	rr.A = net.ParseIP("0.0.0.0").To4()
	m.Answer = append(m.Answer, rr)
	writeDNSResponse(w, m)
}

func loopbackSanityCheckResponse(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Name == "dockerdns.loopbacktest." {
			myselfAsResolverErrorResponse(w, m, question.Name)
			return
		}
	}

	writeDNSResponse(w, m)
}

func localLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
			IPAddresses, err := net.LookupHost(strings.TrimSuffix(question.Name, "."))

			if err != nil {
				log.Printf("Local lookup error: %s", err)
				writeDNSResponse(w, m)
				return
			}

			for _, IPAddress := range IPAddresses {
				writeIPAddressResponse(&IPAddress, m, question.Name, question.Qtype)
			}
		}

	}

	writeDNSResponse(w, m)
}

func writeNameResponse(m *dns.Msg, ip string, name *string) {
	rr := new(dns.PTR)
	str := *name + "." + domain + "."
	rr.Hdr = dns.RR_Header{Name: ip, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 1}
	rr.Ptr = str
	m.Answer = append(m.Answer, rr)
}

func writeIPAddressResponse(IPAddress *string, m *dns.Msg, name string, qtype uint16) {
	ip := net.ParseIP(*IPAddress)
	ip4 := ip.To4()
	if ip4 != nil && qtype == dns.TypeA {
		rr := new(dns.A)
		rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		rr.A = ip4
		m.Answer = append(m.Answer, rr)
	} else if ip4 == nil && qtype == dns.TypeAAAA {
		rr := new(dns.AAAA)
		rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		rr.AAAA = ip.To16()
		m.Answer = append(m.Answer, rr)
	}
}

func reverseLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Qtype != dns.TypePTR {
			continue
		}

		ips := strings.Split(strings.Replace(question.Name, ".in-addr.arpa.", "", 1), ".")

		s := ""
		last := len(ips)
		for i := range ips {
			s = s + ips[last-1-i]
			if i < last-1 {
				s = s + "."
			}
		}

		name, err := lookupWithTimeout(s, 1*time.Second, findContainerNameByIPAddress)

		if err != nil {
			log.Printf("Container lookup error: %s", err)
			writeDNSResponse(w, m)
			return
		}

		if name != nil {
			writeNameResponse(m, question.Name, name)
		} else {
			log.Printf("No match found for %s sending empty reply", question.Name)
		}
	}

	writeDNSResponse(w, m)

}

func dockerNameLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {
		rr := new(dns.A)
		rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		name := "/" + strings.TrimSuffix(question.Name, "."+domain+".")

		IPAddress, err := lookupWithTimeout(name, 1*time.Second, findContainerIPAddressByName)

		if err != nil {
			log.Printf("Container lookup error: %s", err)
			writeDNSResponse(w, m)
			return
		}

		if IPAddress != nil {
			writeIPAddressResponse(IPAddress, m, question.Name, question.Qtype)
		} else {
			log.Printf("No match found for %s sending empty reply", question.Name)
		}
	}

	writeDNSResponse(w, m)

}

func checkIfResolverIsMyself() {

	IPAddresses, _ := net.LookupHost("dockerdns.loopbacktest")

	if IPAddresses != nil && IPAddresses[0] == "0.0.0.0" {
		log.Fatal("Configuration error detected, I am pointing to myself as resolver, that won't work")
	}

}

func dnsStarted() {
	log.Printf("DNS server started")

	go checkIfResolverIsMyself()
}

func startDNS(addr string, errors chan error) {

	log.Printf("Trying to listen on %s", addr)
	server := &dns.Server{Addr: addr, Net: "udp", NotifyStartedFunc: dnsStarted}

	err := server.ListenAndServe()

	if err != nil {
		errors <- err
	}
}

var docker *dockerclient.DockerClient
var domain string

func newHTTPClient(host string, tlsConfig *tls.Config, timeout time.Duration) (*http.Client, *url.URL, error) {
	u, err := url.Parse(host)
	if err != nil {
		return nil, nil, err
	}

	httpTransport := &http.Transport{
		TLSClientConfig:       tlsConfig,
		ResponseHeaderTimeout: timeout,
	}

	switch u.Scheme {
	default:
		httpTransport.Dial = func(proto, addr string) (net.Conn, error) {
			return net.DialTimeout(proto, addr, timeout)
		}
	case "tcp":
		u.Scheme = "http"
	case "unix":
		socketPath := u.Path
		unixDial := func(proto, addr string) (net.Conn, error) {
			return net.DialTimeout("unix", socketPath, timeout)
		}
		httpTransport.Dial = unixDial
		u.Scheme = "http"
		u.Host = "unix.sock"
		u.Path = ""
	}
	return &http.Client{Transport: httpTransport}, u, nil
}

func main() {

	domain = *flag.String("domain", "dev", "domain")

	defaultDockerHost := os.Getenv("DOCKER_HOST")
	if defaultDockerHost == "" {
		defaultDockerHost = "tcp://docker:2375"
	}
	var dockerHost = *flag.String("docker-host", defaultDockerHost, "docker host url, or set DOCKER_HOST")
	var listenAddr = flag.String("listen-addr", ":53", "Listen address for DNS")
	var otherLookupsToLocalResolver = *flag.Bool("local-resolver", true, "Perform local gethostbyname queries for other domains")
	flag.Parse()

	var err error
	httpClient, url, err := newHTTPClient(dockerHost, nil, 100*time.Millisecond)
	if err != nil {
		log.Fatalf("Got error %s", err)
	}
	docker, err = dockerclient.NewDockerClientFromHttpClient(url, httpClient, nil)

	_, err = docker.Version()
	if err != nil {
		log.Fatalf("Failed reading docker version: %s specify host with -docker-host", err)
	}

	go registerShutdownSignalHandlers()

	log.Printf("Resolving %s domain against docker container names at %s", domain, dockerHost)
	if otherLookupsToLocalResolver {
		log.Println("Resolving other domains through system resolver")
		dns.HandleFunc(".", localLookupServer)
		dns.HandleFunc("loopbacktest", loopbackSanityCheckResponse)
	}
	dns.HandleFunc(domain, dockerNameLookupServer)
	dns.HandleFunc("arpa", reverseLookupServer)

	dnsListenErrors := make(chan error)
	go startDNS(*listenAddr, dnsListenErrors)
	err = <-dnsListenErrors
	log.Fatalf("Got error %s", err)
}
<commit_msg>Mitigate negative client caching a bit<commit_after>package main

import (
	"crypto/tls"
	"errors"
	"flag"
	"github.com/miekg/dns"
	"github.com/rrva/dockerclient"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

func registerShutdownSignalHandlers() {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
	for s := range sigChan {
		log.Fatalf("Signal (%s) received, shutting down", s)
	}
}

func matches(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

type lookupResponse struct {
	response *string
	err      error
}

type lookupFn func(string) (*string, error)

func lookupWithTimeout(n string, t time.Duration, lookup lookupFn) (*string, error) {

	lookupResponses := make(chan lookupResponse)
	go func(n string) {
		name, err := lookup(n)
		lookupResponses <- lookupResponse{response: name, err: err}
	}(n)

	timeouts := make(chan bool, 1)
	go func() {
		<-time.After(t)
		timeouts <- true
	}()

	for {
		select {
		case r := <-lookupResponses:
			return r.response, r.err
		case <-timeouts:
			return nil, errors.New("Timeout occurred talking to docker")
		}
	}
}

func findContainerNameByIPAddress(n string) (*string, error) {
	containers, err := docker.ListContainers(false, false, "")
	if err != nil {
		return nil, err
	}
	for _, container := range containers {
		info, _ := docker.InspectContainer(container.Id)
		if info.NetworkSettings.IpAddress == n {
			if len(container.Names) > 0 {
				str := container.Names[0][1:]
				return &str, nil
			}
			return nil, nil
		}
	}
	return nil, nil
}

func findContainerIPAddressByName(n string) (*string, error) {
	containers, err := docker.ListContainers(false, false, "")
	if err != nil {
		return nil, err
	}
	for _, container := range containers {
		if matches(container.Names, n) {
			info, err := docker.InspectContainer(container.Id)
			if err != nil {
				return nil, err
			}
			if info != nil {
				return &info.NetworkSettings.IpAddress, nil
			}
		}
	}
	return nil, nil
}

func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) {
	err := w.WriteMsg(m)
	if err != nil {
		log.Printf("Failed writing DNS response: %s", err)
	}
}

func myselfAsResolverErrorResponse(w dns.ResponseWriter, m *dns.Msg, name string) {
	log.Printf("Oh my! I'm pointing to myself!")
	rr := new(dns.A)
	rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}
	rr.A = net.ParseIP("0.0.0.0").To4()
	m.Answer = append(m.Answer, rr)
	writeDNSResponse(w, m)
}

func loopbackSanityCheckResponse(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Name == "dockerdns.loopbacktest." {
			myselfAsResolverErrorResponse(w, m, question.Name)
			return
		}
	}

	writeDNSResponse(w, m)
}

func localLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
			IPAddresses, err := net.LookupHost(strings.TrimSuffix(question.Name, "."))

			if err != nil {
				log.Printf("Local lookup error: %s", err)
				m.SetRcode(req, dns.RcodeServerFailure)
				writeDNSResponse(w, m)
				return
			}

			for _, IPAddress := range IPAddresses {
				writeIPAddressResponse(&IPAddress, m, question.Name, question.Qtype)
			}
		}

	}

	writeDNSResponse(w, m)
}

func writeNameResponse(m *dns.Msg, ip string, name *string) {
	rr := new(dns.PTR)
	str := *name + "." + domain + "."
	rr.Hdr = dns.RR_Header{Name: ip, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 1}
	rr.Ptr = str
	m.Answer = append(m.Answer, rr)
}

func writeIPAddressResponse(IPAddress *string, m *dns.Msg, name string, qtype uint16) {
	ip := net.ParseIP(*IPAddress)
	ip4 := ip.To4()
	if ip4 != nil && qtype == dns.TypeA {
		rr := new(dns.A)
		rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		rr.A = ip4
		m.Answer = append(m.Answer, rr)
	} else if ip4 == nil && qtype == dns.TypeAAAA {
		rr := new(dns.AAAA)
		rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		rr.AAAA = ip.To16()
		m.Answer = append(m.Answer, rr)
	}
}

func inAddrArpaToIpAddress(n string) string {
	ips := strings.Split(strings.Replace(n, ".in-addr.arpa.", "", 1), ".")
	s := ""
	last := len(ips)
	for i := range ips {
		s = s + ips[last-1-i]
		if i < last-1 {
			s = s + "."
		}
	}
	return s
}

func reverseLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {

		if question.Qtype != dns.TypePTR {
			continue
		}

		ip := inAddrArpaToIpAddress(question.Name)

		name, err := lookupWithTimeout(ip, 1*time.Second, findContainerNameByIPAddress)

		if err != nil {
			log.Printf("Container lookup error: %s", err)
			m.SetRcode(req, dns.RcodeServerFailure)
			writeDNSResponse(w, m)
			return
		}

		if name != nil {
			writeNameResponse(m, question.Name, name)
		} else {
			m.SetRcode(req, dns.RcodeRefused)
			log.Printf("No match found for %s sending empty reply", question.Name)
		}
	}

	writeDNSResponse(w, m)

}

func dockerNameLookupServer(w dns.ResponseWriter, req *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(req)

	for _, question := range req.Question {
		rr := new(dns.A)
		rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 1}

		name := "/" + strings.TrimSuffix(question.Name, "."+domain+".")

		IPAddress, err := lookupWithTimeout(name, 1*time.Second, findContainerIPAddressByName)

		if err != nil {
			log.Printf("Container lookup error: %s", err)
			callMeMaybe := "127.0.53.53"
			writeIPAddressResponse(&callMeMaybe, m, question.Name, question.Qtype)
			writeDNSResponse(w, m)
			return
		}

		if IPAddress != nil {
			writeIPAddressResponse(IPAddress, m, question.Name, question.Qtype)
		} else {
			log.Printf("No match found for %s sending empty reply", question.Name)
		}
	}

	writeDNSResponse(w, m)

}

func checkIfResolverIsMyself() {

	IPAddresses, _ := net.LookupHost("dockerdns.loopbacktest")

	if IPAddresses != nil && IPAddresses[0] == "0.0.0.0" {
		log.Fatal("Configuration error detected, I am pointing to myself as resolver, that won't work")
	}

}

func dnsStarted() {
	log.Printf("DNS server started")

	go checkIfResolverIsMyself()
}

func startDNS(addr string, errors chan error) {

	log.Printf("Trying to listen on %s", addr)
	server := &dns.Server{Addr: addr, Net: "udp", NotifyStartedFunc: dnsStarted}

	err := server.ListenAndServe()

	if err != nil {
		errors <- err
	}
}

var docker *dockerclient.DockerClient
var domain string

func newHTTPClient(host string, tlsConfig *tls.Config, timeout time.Duration) (*http.Client, *url.URL, error) {
	u, err := url.Parse(host)
	if err != nil {
		return nil, nil, err
	}

	httpTransport := &http.Transport{
		TLSClientConfig:       tlsConfig,
		ResponseHeaderTimeout: timeout,
	}

	switch u.Scheme {
	default:
		httpTransport.Dial = func(proto, addr string) (net.Conn, error) {
			return net.DialTimeout(proto, addr, timeout)
		}
	case "tcp":
		u.Scheme = "http"
	case "unix":
		socketPath := u.Path
		unixDial := func(proto, addr string) (net.Conn, error) {
			return net.DialTimeout("unix", socketPath, timeout)
		}
		httpTransport.Dial = unixDial
		u.Scheme = "http"
		u.Host = "unix.sock"
		u.Path = ""
	}
	return &http.Client{Transport: httpTransport}, u, nil
}

func main() {

	domain = *flag.String("domain", "dev", "domain")

	defaultDockerHost := os.Getenv("DOCKER_HOST")
	if defaultDockerHost == "" {
		defaultDockerHost = "tcp://docker:2375"
	}
	var dockerHost = *flag.String("docker-host", defaultDockerHost, "docker host url, or set DOCKER_HOST")
	var listenAddr = flag.String("listen-addr", ":53", "Listen address for DNS")
	var otherLookupsToLocalResolver = *flag.Bool("local-resolver", true, "Perform local gethostbyname queries for other domains")
	flag.Parse()

	var err error
	httpClient, url, err := newHTTPClient(dockerHost, nil, 100*time.Millisecond)
	if err != nil {
		log.Fatalf("Got error %s", err)
	}
	docker, err = dockerclient.NewDockerClientFromHttpClient(url, httpClient, nil)

	_, err = docker.Version()
	if err != nil {
		log.Fatalf("Failed reading docker version: %s specify host with -docker-host", err)
	}

	go registerShutdownSignalHandlers()

	log.Printf("Resolving %s domain against docker container names at %s", domain, dockerHost)
	if otherLookupsToLocalResolver {
		log.Println("Resolving other domains through system resolver")
		dns.HandleFunc(".", localLookupServer)
		dns.HandleFunc("loopbacktest", loopbackSanityCheckResponse)
	}
	dns.HandleFunc(domain, dockerNameLookupServer)
	dns.HandleFunc("arpa", reverseLookupServer)

	dnsListenErrors := make(chan error)
	go startDNS(*listenAddr, dnsListenErrors)
	err = <-dnsListenErrors
	log.Fatalf("Got error %s", err)
}
<|endoftext|>


<commit_before>package docker

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"os/signal"
	"syscall"

	"github.com/cpuguy83/docker-grand-ambassador/utils"
)

type (
	Docker interface {
		FetchAllContainers() ([]*Container, error)
		FetchContainer(name string) (*Container, error)
		GetEvents() chan *Event
		Info() (*DaemonInfo, error)
	}

	Event struct {
		ContainerId string `json:"id"`
		Status      string `json:"status"`
	}

	Binding struct {
		HostIp   string
		HostPort string
	}

	NetworkSettings struct {
		IpAddress string
		Ports     map[string][]Binding
	}

	State struct {
		Running bool
	}

	Container struct {
		Id              string
		Name            string
		NetworkSettings *NetworkSettings
		State           State
		Config          struct {
			Image        string
			AttachStderr bool
			AttachStdin  bool
			AttachStdout bool
		}
		HostConfig struct {
			PortBindings map[string][]Binding
		}
	}

	dockerClient struct {
		path string
	}

	DaemonInfo struct {
		Containers         int
		Debug              int
		Driver             string
		DriverStatus       [][]string
		ExecutionDriver    string
		IPv4Forwarding     int
		Images             int
		IndexServerAddress string
		InitPath           string
		INitSha1           string
		KernelVersion      string
		MemoryLimit        int
		NEventsListener    int
		NFd                int
		NGoroutines        int
		Sockets            []string
		SwapLimit          int
	}
)

func (d *DaemonInfo) RootPath() string {
	for _, i := range d.DriverStatus {
		if i[0] == "Root Dir" {
			return i[1]
		}
	}
	return ""
}

func NewClient(path string) (Docker, error) {
	return &dockerClient{path}, nil
}

func (d *dockerClient) newConn() (*httputil.ClientConn, error) {
	proto, path := utils.ParseURL(d.path)
	conn, err := net.Dial(proto, path)

	if err != nil {
		return nil, err
	}
	return httputil.NewClientConn(conn, nil), nil
}

func (docker *dockerClient) PullImage(name string) error {
	var (
		method = "POST"
		uri    = fmt.Sprintf("/images/create?fromImage=%s", name)
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) RemoveContainer(name string, force bool, volumes bool) error {
	var (
		method = "DELETE"
		uri    = fmt.Sprintf("/containers/%s?force=%s&volumes=%s", name, force, volumes)
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) CreateContainer(container map[string]interface{}) (string, error) {
	var (
		method = "POST"
		name   string
		uri    = fmt.Sprintf("/containers/create")
	)

	cJson, err := json.Marshal(container)
	if err != nil {
		return name, err
	}

	req, err := http.NewRequest(method, fmt.Sprintf(uri), bytes.NewBuffer(cJson))
	if err != nil {
		return name, err
	}

	c, err := docker.newConn()
	if err != nil {
		return name, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return name, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return name, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	type createResp struct {
		Id string
	}
	var respData createResp
	err = json.NewDecoder(resp.Body).Decode(&respData)
	if err != nil {
		return name, err
	}
	name = respData.Id

	return name, nil
}

func (docker *dockerClient) StartContainer(name, hostConfig interface{}) error {
	var (
		method = "POST"
		uri    = fmt.Sprintf("/containers/%s/start", name)
	)

	bodyJson, err := json.Marshal(hostConfig)
	if err != nil {
		return err
	}
	bodyData := bytes.NewBuffer(bodyJson)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), bodyData)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) RunContainer(config map[string]interface{}) error {

	name, err := docker.CreateContainer(config)
	if err != nil {
		return err
	}

	return docker.StartContainer(name, config["HostConfig"])
}

func (docker *dockerClient) FetchContainer(name string) (*Container, error) {
	var (
		method = "GET"
		uri    = fmt.Sprintf("/containers/%s/json", name)
	)
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var container *Container
	err = json.NewDecoder(resp.Body).Decode(&container)
	if err != nil {
		return nil, err
	}
	return container, nil
}

func (docker *dockerClient) FetchAllContainers() ([]*Container, error) {
	var (
		method = "GET"
		uri    = "/containers/json"
	)
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var containers []*Container
	if err = json.NewDecoder(resp.Body).Decode(&containers); err != nil {
		return nil, err
	}
	return containers, nil
}

func (docker *dockerClient) newRequest(method, uri string) (io.ReadCloser, error) {
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		return resp.Body, nil
	}
	return nil, fmt.Errorf("invalid HTTP request %d %s", resp.StatusCode, resp.Status)
}

func (d *dockerClient) GetEvents() chan *Event {
	eventChan := make(chan *Event, 100) // 100 event buffer
	go func() {
		defer close(eventChan)

		c, err := d.newConn()
		if err != nil {
			log.Printf("cannot connect to docker: %s", err)
			return
		}
		defer c.Close()

		req, err := http.NewRequest("GET", "/events", nil)
		if err != nil {
			log.Printf("bad request for events: %s", err)
			return
		}

		resp, err := c.Do(req)
		if err != nil {
			log.Printf("cannot connect to events endpoint: %s", err)
			return
		}
		defer resp.Body.Close()

		// handle signals to stop the socket
		sigChan := make(chan os.Signal, 1)
		signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
		go func() {
			for sig := range sigChan {
				log.Printf("received signal '%v', exiting", sig)

				c.Close()
				close(eventChan)
				os.Exit(0)
			}
		}()

		dec := json.NewDecoder(resp.Body)
		for {
			var event *Event
			if err := dec.Decode(&event); err != nil {
				if err == io.EOF {
					break
				}
				log.Printf("cannot decode json: %s", err)
				continue
			}
			eventChan <- event
		}
		log.Printf("closing event channel")
	}()
	return eventChan
}

func (docker *dockerClient) Info() (*DaemonInfo, error) {
	var (
		method = "GET"
		uri    = "/info"
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var info *DaemonInfo
	if err = json.NewDecoder(resp.Body).Decode(&info); err != nil {
		return nil, err
	}
	return info, nil
}
<commit_msg>Add methods to Docker interface<commit_after>package docker

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"os/signal"
	"syscall"

	"github.com/cpuguy83/docker-grand-ambassador/utils"
)

type (
	Docker interface {
		FetchAllContainers() ([]*Container, error)
		FetchContainer(name string) (*Container, error)
		GetEvents() chan *Event
		Info() (*DaemonInfo, error)
		PullImage(name string) error
		CreateContainer(container map[string]interface{}) (string, error)
		StartContainer(string, interface{}) error
		RunContainer(map[string]interface{}) error
	}

	Event struct {
		ContainerId string `json:"id"`
		Status      string `json:"status"`
	}

	Binding struct {
		HostIp   string
		HostPort string
	}

	NetworkSettings struct {
		IpAddress string
		Ports     map[string][]Binding
	}

	State struct {
		Running bool
	}

	Container struct {
		Id              string
		Name            string
		NetworkSettings *NetworkSettings
		State           State
		Config          struct {
			Image        string
			AttachStderr bool
			AttachStdin  bool
			AttachStdout bool
		}
		HostConfig struct {
			PortBindings map[string][]Binding
		}
	}

	dockerClient struct {
		path string
	}

	DaemonInfo struct {
		Containers         int
		Debug              int
		Driver             string
		DriverStatus       [][]string
		ExecutionDriver    string
		IPv4Forwarding     int
		Images             int
		IndexServerAddress string
		InitPath           string
		INitSha1           string
		KernelVersion      string
		MemoryLimit        int
		NEventsListener    int
		NFd                int
		NGoroutines        int
		Sockets            []string
		SwapLimit          int
	}
)

func (d *DaemonInfo) RootPath() string {
	for _, i := range d.DriverStatus {
		if i[0] == "Root Dir" {
			return i[1]
		}
	}
	return ""
}

func NewClient(path string) (Docker, error) {
	return &dockerClient{path}, nil
}

func (d *dockerClient) newConn() (*httputil.ClientConn, error) {
	proto, path := utils.ParseURL(d.path)
	conn, err := net.Dial(proto, path)

	if err != nil {
		return nil, err
	}
	return httputil.NewClientConn(conn, nil), nil
}

func (docker *dockerClient) PullImage(name string) error {
	var (
		method = "POST"
		uri    = fmt.Sprintf("/images/create?fromImage=%s", name)
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) RemoveContainer(name string, force bool, volumes bool) error {
	var (
		method = "DELETE"
		uri    = fmt.Sprintf("/containers/%s?force=%s&volumes=%s", name, force, volumes)
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) CreateContainer(container map[string]interface{}) (string, error) {
	var (
		method = "POST"
		name   string
		uri    = fmt.Sprintf("/containers/create")
	)

	cJson, err := json.Marshal(container)
	if err != nil {
		return name, err
	}

	req, err := http.NewRequest(method, fmt.Sprintf(uri), bytes.NewBuffer(cJson))
	if err != nil {
		return name, err
	}

	c, err := docker.newConn()
	if err != nil {
		return name, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return name, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return name, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	type createResp struct {
		Id string
	}
	var respData createResp
	err = json.NewDecoder(resp.Body).Decode(&respData)
	if err != nil {
		return name, err
	}
	name = respData.Id

	return name, nil
}

func (docker *dockerClient) StartContainer(name string, hostConfig interface{}) error {
	var (
		method = "POST"
		uri    = fmt.Sprintf("/containers/%s/start", name)
	)

	bodyJson, err := json.Marshal(hostConfig)
	if err != nil {
		return err
	}
	bodyData := bytes.NewBuffer(bodyJson)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), bodyData)
	if err != nil {
		return err
	}

	c, err := docker.newConn()
	if err != nil {
		return err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	return nil
}

func (docker *dockerClient) RunContainer(config map[string]interface{}) error {

	name, err := docker.CreateContainer(config)
	if err != nil {
		return err
	}

	return docker.StartContainer(name, config["HostConfig"])
}

func (docker *dockerClient) FetchContainer(name string) (*Container, error) {
	var (
		method = "GET"
		uri    = fmt.Sprintf("/containers/%s/json", name)
	)
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var container *Container
	err = json.NewDecoder(resp.Body).Decode(&container)
	if err != nil {
		return nil, err
	}
	return container, nil
}

func (docker *dockerClient) FetchAllContainers() ([]*Container, error) {
	var (
		method = "GET"
		uri    = "/containers/json"
	)
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var containers []*Container
	if err = json.NewDecoder(resp.Body).Decode(&containers); err != nil {
		return nil, err
	}
	return containers, nil
}

func (docker *dockerClient) newRequest(method, uri string) (io.ReadCloser, error) {
	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		return resp.Body, nil
	}
	return nil, fmt.Errorf("invalid HTTP request %d %s", resp.StatusCode, resp.Status)
}

func (d *dockerClient) GetEvents() chan *Event {
	eventChan := make(chan *Event, 100) // 100 event buffer
	go func() {
		defer close(eventChan)

		c, err := d.newConn()
		if err != nil {
			log.Printf("cannot connect to docker: %s", err)
			return
		}
		defer c.Close()

		req, err := http.NewRequest("GET", "/events", nil)
		if err != nil {
			log.Printf("bad request for events: %s", err)
			return
		}

		resp, err := c.Do(req)
		if err != nil {
			log.Printf("cannot connect to events endpoint: %s", err)
			return
		}
		defer resp.Body.Close()

		// handle signals to stop the socket
		sigChan := make(chan os.Signal, 1)
		signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
		go func() {
			for sig := range sigChan {
				log.Printf("received signal '%v', exiting", sig)

				c.Close()
				close(eventChan)
				os.Exit(0)
			}
		}()

		dec := json.NewDecoder(resp.Body)
		for {
			var event *Event
			if err := dec.Decode(&event); err != nil {
				if err == io.EOF {
					break
				}
				log.Printf("cannot decode json: %s", err)
				continue
			}
			eventChan <- event
		}
		log.Printf("closing event channel")
	}()
	return eventChan
}

func (docker *dockerClient) Info() (*DaemonInfo, error) {
	var (
		method = "GET"
		uri    = "/info"
	)

	req, err := http.NewRequest(method, fmt.Sprintf(uri), nil)
	if err != nil {
		return nil, err
	}

	c, err := docker.newConn()
	if err != nil {
		return nil, err
	}
	defer c.Close()

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf(fmt.Sprintf("Request failed, status: %s", resp.StatusCode))
	}

	var info *DaemonInfo
	if err = json.NewDecoder(resp.Body).Decode(&info); err != nil {
		return nil, err
	}
	return info, nil
}
<|endoftext|>


<commit_before>package docker

import (
	"bufio"
	"bytes"
	"context"
	"github.com/ViBiOh/docker-deploy/jsonHttp"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/strslice"
	"github.com/docker/docker/client"
	"gopkg.in/yaml.v2"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
)

const host = `DOCKER_HOST`
const version = `DOCKER_VERSION`
const configurationFile = `./users`

var commaByte = []byte(`,`)
var splitLogs = regexp.MustCompile(`.{8}(.*?)\n`)

var networkConfig = network.NetworkingConfig{
	EndpointsConfig: map[string]*network.EndpointSettings{
		`traefik`: &network.EndpointSettings{},
	},
}

var containersRequest = regexp.MustCompile(`/containers/?$`)
var containerRequest = regexp.MustCompile(`/containers/([^/]+)/?$`)
var startRequest = regexp.MustCompile(`/containers/([^/]+)/start`)
var stopRequest = regexp.MustCompile(`/containers/([^/]+)/stop`)
var restartRequest = regexp.MustCompile(`/containers/([^/]+)/restart`)
var logRequest = regexp.MustCompile(`/containers/([^/]+)/logs`)

type results struct {
	Results interface{} `json:"results"`
}

type user struct {
	username string
	password string
	role     string
}

type dockerComposeService struct {
	Image       string
	Command     string
	Environment map[string]string
	Labels      map[string]string
	ReadOnly    bool  `yaml:"read_only"`
	CPUShares   int64 `yaml:"cpu_shares"`
	MemoryLimit int64 `yaml:"mem_limit"`
}

type dockerCompose struct {
	Version  string
	Services map[string]dockerComposeService
}

var docker *client.Client
var users map[string]*user

func errorHandler(w http.ResponseWriter, err error) {
	log.Print(err)
	http.Error(w, err.Error(), http.StatusInternalServerError)
}

func init() {
	users = readConfiguration(configurationFile)

	client, err := client.NewClient(os.Getenv(host), os.Getenv(version), nil, nil)
	if err != nil {
		log.Fatal(err)
	} else {
		docker = client
	}
}

func readConfiguration(path string) map[string]*user {
	configFile, err := os.Open(path)
	defer configFile.Close()

	if err != nil {
		log.Print(err)
		return nil
	}

	users := make(map[string]*user)

	scanner := bufio.NewScanner(configFile)
	for scanner.Scan() {
		parts := bytes.Split(scanner.Bytes(), commaByte)
		user := user{string(parts[0]), string(parts[1]), string(parts[2])}

		users[strings.ToLower(user.username)] = &user
	}

	return users
}

func inspectContainerHandler(w http.ResponseWriter, containerID []byte) {
	if container, err := docker.ContainerInspect(context.Background(), string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		jsonHttp.ResponseJSON(w, container)
	}
}

func startContainer(containerID string) error {
	return docker.ContainerStart(context.Background(), string(containerID), types.ContainerStartOptions{})
}

func startContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := startContainer(string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func stopContainer(containerID string) error {
	return docker.ContainerStop(context.Background(), containerID, nil)
}

func stopContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := stopContainer(string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func restartContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := docker.ContainerRestart(context.Background(), string(containerID), nil); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func deleteContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := docker.ContainerRemove(context.Background(), string(containerID), types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func logContainerHandler(w http.ResponseWriter, containerID []byte) {
	logs, err := docker.ContainerLogs(context.Background(), string(containerID), types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: false})
	if err != nil {
		errorHandler(w, err)
		return
	}

	defer logs.Close()

	if logLines, err := ioutil.ReadAll(logs); err != nil {
		errorHandler(w, err)
	} else {
		matches := splitLogs.FindAllSubmatch(logLines, -1)
		cleanLogs := make([]string, 0, len(matches))
		for _, match := range matches {
			cleanLogs = append(cleanLogs, string(match[1]))
		}

		jsonHttp.ResponseJSON(w, results{cleanLogs})
	}
}

func listContainers(loggedUser *user) ([]types.Container, error) {
	options := types.ContainerListOptions{All: true}

	if loggedUser != nil {
		args, err := filters.ParseFlag(`label=owner=`+loggedUser.username, filters.NewArgs())
		if err != nil {
			return nil, err
		}
		options.Filters = args
	}

	return docker.ContainerList(context.Background(), options)
}

func listContainersHandler(w http.ResponseWriter) {
	if containers, err := listContainers(nil); err != nil {
		errorHandler(w, err)
	} else {
		jsonHttp.ResponseJSON(w, results{containers})
	}
}

func readBody(body io.ReadCloser) ([]byte, error) {
	defer body.Close()
	return ioutil.ReadAll(body)
}

func getConfig(service *dockerComposeService, loggedUser *user) *container.Config {
	environments := make([]string, len(service.Environment))
	for key, value := range service.Environment {
		environments = append(environments, key+`=`+value)
	}

	service.Labels[`owner`] = loggedUser.username

	config := container.Config{
		Image:  service.Image,
		Labels: service.Labels,
		Env:    environments,
	}

	if service.Command != `` {
		config.Cmd = strslice.StrSlice([]string{service.Command})
	}

	return &config
}

func getHostConfig(service *dockerComposeService) *container.HostConfig {
	hostConfig := container.HostConfig{
		LogConfig: container.LogConfig{Type: `json-file`, Config: map[string]string{
			`max-size`: `50m`,
		}},
		RestartPolicy: container.RestartPolicy{Name: `on-failure`, MaximumRetryCount: 5},
		Resources: container.Resources{
			CPUShares: 128,
			Memory:    134217728,
		},
		SecurityOpt: []string{`no-new-privileges`},
	}

	if service.ReadOnly {
		hostConfig.ReadonlyRootfs = service.ReadOnly
	}

	if service.CPUShares != 0 {
		hostConfig.Resources.CPUShares = service.CPUShares
	}

	if service.MemoryLimit != 0 {
		hostConfig.Resources.Memory = service.MemoryLimit
	}

	return &hostConfig
}

func runComposeHandler(w http.ResponseWriter, loggedUser *user, name []byte, composeFile []byte) {
	compose := dockerCompose{}

	if err := yaml.Unmarshal(composeFile, &compose); err != nil {
		errorHandler(w, err)
		return
	}

	ownerContainers, err := listContainers(loggedUser)
	if err != nil {
		errorHandler(w, err)
		return
	}
	for _, container := range ownerContainers {
		stopContainer(container.ID)
	}

	ids := make([]string, len(compose.Services))
	for serviceName, service := range compose.Services {
		pull, err := docker.ImagePull(context.Background(), service.Image, types.ImagePullOptions{})
		defer pull.Close();
		if err != nil {
			errorHandler(w, err)
			return
		}

		id, err := docker.ContainerCreate(context.Background(), getConfig(&service, loggedUser), getHostConfig(&service), &networkConfig, string(name)+`_`+serviceName)
		if err != nil {
			errorHandler(w, err)
			return
		}

		startContainer(id.ID)
		ids = append(ids, id.ID)
	}

	jsonHttp.ResponseJSON(w, results{ids})
}

func isAuthenticated(r *http.Request) *user {
	username, password, ok := r.BasicAuth()

	if ok {
		user, ok := users[strings.ToLower(username)]

		if ok && user.password == password {
			return user
		}
	}

	return nil
}

func unauthorized(w http.ResponseWriter) {
	http.Error(w, `Authentication required`, http.StatusUnauthorized)
}

// Handler for Hello request. Should be use with net/http
type Handler struct {
}

func (handler Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add(`Access-Control-Allow-Origin`, `*`)
	w.Header().Add(`Access-Control-Allow-Headers`, `Content-Type, Authorization`)
	w.Header().Add(`Access-Control-Allow-Methods`, `GET, POST, DELETE`)
	w.Header().Add(`X-Content-Type-Options`, `nosniff`)

	if r.Method == http.MethodOptions {
		w.Write(nil)
		return
	}

	urlPath := []byte(r.URL.Path)

	if containersRequest.Match(urlPath) && r.Method == http.MethodGet {
		listContainersHandler(w)
	} else if loggedUser := isAuthenticated(r); loggedUser!= nil {
		if containerRequest.Match(urlPath) && r.Method == http.MethodPost {
			if composeBody, err := readBody(r.Body); err != nil {
				errorHandler(w, err)
			} else {
				runComposeHandler(w, loggedUser, containerRequest.FindSubmatch(urlPath)[1], composeBody)
			}
		} else if containerRequest.Match(urlPath) && r.Method == http.MethodGet {
			inspectContainerHandler(w, containerRequest.FindSubmatch(urlPath)[1])
		} else if startRequest.Match(urlPath) && r.Method == http.MethodPost {
			startContainerHandler(w, startRequest.FindSubmatch(urlPath)[1])
		} else if stopRequest.Match(urlPath) && r.Method == http.MethodPost {
			stopContainerHandler(w, stopRequest.FindSubmatch(urlPath)[1])
		} else if restartRequest.Match(urlPath) && r.Method == http.MethodPost {
			restartContainerHandler(w, restartRequest.FindSubmatch(urlPath)[1])
		} else if containerRequest.Match(urlPath) && r.Method == http.MethodDelete {
			deleteContainerHandler(w, containerRequest.FindSubmatch(urlPath)[1])
		} else if logRequest.Match(urlPath) && r.Method == http.MethodGet {
			logContainerHandler(w, logRequest.FindSubmatch(urlPath)[1])
		}
	} else {
		unauthorized(w)
	}
}
<commit_msg>Extracting inspect func<commit_after>package docker

import (
	"bufio"
	"bytes"
	"context"
	"github.com/ViBiOh/docker-deploy/jsonHttp"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/strslice"
	"github.com/docker/docker/client"
	"gopkg.in/yaml.v2"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
)

const host = `DOCKER_HOST`
const version = `DOCKER_VERSION`
const configurationFile = `./users`

var commaByte = []byte(`,`)
var splitLogs = regexp.MustCompile(`.{8}(.*?)\n`)

var networkConfig = network.NetworkingConfig{
	EndpointsConfig: map[string]*network.EndpointSettings{
		`traefik`: &network.EndpointSettings{},
	},
}

var containersRequest = regexp.MustCompile(`/containers/?$`)
var containerRequest = regexp.MustCompile(`/containers/([^/]+)/?$`)
var startRequest = regexp.MustCompile(`/containers/([^/]+)/start`)
var stopRequest = regexp.MustCompile(`/containers/([^/]+)/stop`)
var restartRequest = regexp.MustCompile(`/containers/([^/]+)/restart`)
var logRequest = regexp.MustCompile(`/containers/([^/]+)/logs`)

type results struct {
	Results interface{} `json:"results"`
}

type user struct {
	username string
	password string
	role     string
}

type dockerComposeService struct {
	Image       string
	Command     string
	Environment map[string]string
	Labels      map[string]string
	ReadOnly    bool  `yaml:"read_only"`
	CPUShares   int64 `yaml:"cpu_shares"`
	MemoryLimit int64 `yaml:"mem_limit"`
}

type dockerCompose struct {
	Version  string
	Services map[string]dockerComposeService
}

var docker *client.Client
var users map[string]*user

func errorHandler(w http.ResponseWriter, err error) {
	log.Print(err)
	http.Error(w, err.Error(), http.StatusInternalServerError)
}

func init() {
	users = readConfiguration(configurationFile)

	client, err := client.NewClient(os.Getenv(host), os.Getenv(version), nil, nil)
	if err != nil {
		log.Fatal(err)
	} else {
		docker = client
	}
}

func readConfiguration(path string) map[string]*user {
	configFile, err := os.Open(path)
	defer configFile.Close()

	if err != nil {
		log.Print(err)
		return nil
	}

	users := make(map[string]*user)

	scanner := bufio.NewScanner(configFile)
	for scanner.Scan() {
		parts := bytes.Split(scanner.Bytes(), commaByte)
		user := user{string(parts[0]), string(parts[1]), string(parts[2])}

		users[strings.ToLower(user.username)] = &user
	}

	return users
}

func listContainers(loggedUser *user) ([]types.Container, error) {
	options := types.ContainerListOptions{All: true}

	if loggedUser != nil {
		args, err := filters.ParseFlag(`label=owner=`+loggedUser.username, filters.NewArgs())
		if err != nil {
			return nil, err
		}
		options.Filters = args
	}

	return docker.ContainerList(context.Background(), options)
}

func inspectContainer(containerID string) (types.ContainerJSON, error) {
	return docker.ContainerInspect(context.Background(), containerID)
}

func startContainer(containerID string) error {
	return docker.ContainerStart(context.Background(), string(containerID), types.ContainerStartOptions{})
}

func stopContainer(containerID string) error {
	return docker.ContainerStop(context.Background(), containerID, nil)
}

func inspectContainerHandler(w http.ResponseWriter, containerID []byte) {
	if container, err := inspectContainer(string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		jsonHttp.ResponseJSON(w, container)
	}
}

func startContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := startContainer(string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func stopContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := stopContainer(string(containerID)); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func restartContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := docker.ContainerRestart(context.Background(), string(containerID), nil); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func deleteContainerHandler(w http.ResponseWriter, containerID []byte) {
	if err := docker.ContainerRemove(context.Background(), string(containerID), types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
		errorHandler(w, err)
	} else {
		w.Write(nil)
	}
}

func logContainerHandler(w http.ResponseWriter, containerID []byte) {
	logs, err := docker.ContainerLogs(context.Background(), string(containerID), types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: false})
	if err != nil {
		errorHandler(w, err)
		return
	}

	defer logs.Close()

	if logLines, err := ioutil.ReadAll(logs); err != nil {
		errorHandler(w, err)
	} else {
		matches := splitLogs.FindAllSubmatch(logLines, -1)
		cleanLogs := make([]string, 0, len(matches))
		for _, match := range matches {
			cleanLogs = append(cleanLogs, string(match[1]))
		}

		jsonHttp.ResponseJSON(w, results{cleanLogs})
	}
}

func listContainersHandler(w http.ResponseWriter) {
	if containers, err := listContainers(nil); err != nil {
		errorHandler(w, err)
	} else {
		jsonHttp.ResponseJSON(w, results{containers})
	}
}

func readBody(body io.ReadCloser) ([]byte, error) {
	defer body.Close()
	return ioutil.ReadAll(body)
}

func getConfig(service *dockerComposeService, loggedUser *user) *container.Config {
	environments := make([]string, len(service.Environment))
	for key, value := range service.Environment {
		environments = append(environments, key+`=`+value)
	}

	service.Labels[`owner`] = loggedUser.username

	config := container.Config{
		Image:  service.Image,
		Labels: service.Labels,
		Env:    environments,
	}

	if service.Command != `` {
		config.Cmd = strslice.StrSlice([]string{service.Command})
	}

	return &config
}

func getHostConfig(service *dockerComposeService) *container.HostConfig {
	hostConfig := container.HostConfig{
		LogConfig: container.LogConfig{Type: `json-file`, Config: map[string]string{
			`max-size`: `50m`,
		}},
		RestartPolicy: container.RestartPolicy{Name: `on-failure`, MaximumRetryCount: 5},
		Resources: container.Resources{
			CPUShares: 128,
			Memory:    134217728,
		},
		SecurityOpt: []string{`no-new-privileges`},
	}

	if service.ReadOnly {
		hostConfig.ReadonlyRootfs = service.ReadOnly
	}

	if service.CPUShares != 0 {
		hostConfig.Resources.CPUShares = service.CPUShares
	}

	if service.MemoryLimit != 0 {
		hostConfig.Resources.Memory = service.MemoryLimit
	}

	return &hostConfig
}

func runComposeHandler(w http.ResponseWriter, loggedUser *user, name []byte, composeFile []byte) {
	compose := dockerCompose{}

	if err := yaml.Unmarshal(composeFile, &compose); err != nil {
		errorHandler(w, err)
		return
	}

	ownerContainers, err := listContainers(loggedUser)
	if err != nil {
		errorHandler(w, err)
		return
	}
	for _, container := range ownerContainers {
		stopContainer(container.ID)
	}

	ids := make([]string, len(compose.Services))
	for serviceName, service := range compose.Services {
		pull, err := docker.ImagePull(context.Background(), service.Image, types.ImagePullOptions{})
		defer pull.Close();
		if err != nil {
			errorHandler(w, err)
			return
		}

		id, err := docker.ContainerCreate(context.Background(), getConfig(&service, loggedUser), getHostConfig(&service), &networkConfig, string(name)+`_`+serviceName)
		if err != nil {
			errorHandler(w, err)
			return
		}

		startContainer(id.ID)
		ids = append(ids, id.ID)
	}

	jsonHttp.ResponseJSON(w, results{ids})
}

func isAuthenticated(r *http.Request) *user {
	username, password, ok := r.BasicAuth()

	if ok {
		user, ok := users[strings.ToLower(username)]

		if ok && user.password == password {
			return user
		}
	}

	return nil
}

func unauthorized(w http.ResponseWriter) {
	http.Error(w, `Authentication required`, http.StatusUnauthorized)
}

// Handler for Hello request. Should be use with net/http
type Handler struct {
}

func (handler Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Add(`Access-Control-Allow-Origin`, `*`)
	w.Header().Add(`Access-Control-Allow-Headers`, `Content-Type, Authorization`)
	w.Header().Add(`Access-Control-Allow-Methods`, `GET, POST, DELETE`)
	w.Header().Add(`X-Content-Type-Options`, `nosniff`)

	if r.Method == http.MethodOptions {
		w.Write(nil)
		return
	}

	urlPath := []byte(r.URL.Path)

	if containersRequest.Match(urlPath) && r.Method == http.MethodGet {
		listContainersHandler(w)
	} else if loggedUser := isAuthenticated(r); loggedUser!= nil {
		if containerRequest.Match(urlPath) && r.Method == http.MethodPost {
			if composeBody, err := readBody(r.Body); err != nil {
				errorHandler(w, err)
			} else {
				runComposeHandler(w, loggedUser, containerRequest.FindSubmatch(urlPath)[1], composeBody)
			}
		} else if containerRequest.Match(urlPath) && r.Method == http.MethodGet {
			inspectContainerHandler(w, containerRequest.FindSubmatch(urlPath)[1])
		} else if startRequest.Match(urlPath) && r.Method == http.MethodPost {
			startContainerHandler(w, startRequest.FindSubmatch(urlPath)[1])
		} else if stopRequest.Match(urlPath) && r.Method == http.MethodPost {
			stopContainerHandler(w, stopRequest.FindSubmatch(urlPath)[1])
		} else if restartRequest.Match(urlPath) && r.Method == http.MethodPost {
			restartContainerHandler(w, restartRequest.FindSubmatch(urlPath)[1])
		} else if containerRequest.Match(urlPath) && r.Method == http.MethodDelete {
			deleteContainerHandler(w, containerRequest.FindSubmatch(urlPath)[1])
		} else if logRequest.Match(urlPath) && r.Method == http.MethodGet {
			logContainerHandler(w, logRequest.FindSubmatch(urlPath)[1])
		}
	} else {
		unauthorized(w)
	}
}
<|endoftext|>


<commit_before>//  Copyright (c) 2015 Couchbase, Inc.
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the
//  License. You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing,
//  software distributed under the License is distributed on an "AS
//  IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
//  express or implied. See the License for the specific language
//  governing permissions and limitations under the License.

package monitor

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"testing"
	"time"
)

func TestEmptyStartMonitorCluster(t *testing.T) {
	HttpGet = func(url string) (resp *http.Response, err error) {
		t.Errorf("expected no get")
		return nil, nil
	}
	defer func() {
		HttpGet = http.Get
	}()

	opt := MonitorClusterOptions{}

	m, err := StartMonitorCluster(nil, nil, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	m.Stop()
}

func TestStartMonitorClusterEmptyCfg(t *testing.T) {
	httpGets := 0
	HttpGet = func(url string) (resp *http.Response, err error) {
		httpGets++

		if url != "url0/api/cfg" {
			t.Errorf("expected http get, url: %s", url)
		}

		return &http.Response{
			StatusCode: 200,
			Body:       ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
		}, nil
	}
	defer func() {
		HttpGet = http.Get
	}()

	sampleCh := make(chan MonitorSample)

	opt := MonitorClusterOptions{}

	m, err := StartMonitorCluster([]string{"url0"},
		sampleCh, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	s, ok := <-sampleCh
	if !ok ||
		s.Kind != "/api/cfg" ||
		s.Url != "url0" ||
		s.UUID != "" ||
		s.Error != nil ||
		string(s.Data) != "{}" {
		t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
	}

	m.Stop()

	select {
	case <-sampleCh:
		t.Errorf("unexpected sample")
	default:
	}

	if httpGets != 1 {
		t.Errorf("expected 1 http gets")
	}
}

func TestStartMonitorCluster(t *testing.T) {
	opt := MonitorClusterOptions{}
	samplesBeforeStop := 2
	expHttpGets := 3
	testStartMonitorCluster(t, "2x3",
		opt, samplesBeforeStop, expHttpGets, false)

	opt = MonitorClusterOptions{
		CfgSampleInterval: 100,
		MonitorNodesOptions: MonitorNodesOptions{
			StatsSampleInterval: 10000 * time.Second,
			DiagSampleInterval:  10000 * time.Second,
		},
	}
	samplesBeforeStop = 5
	expHttpGets = 6
	testStartMonitorCluster(t, "more cfgs",
		opt, samplesBeforeStop, expHttpGets, true)
}

func testStartMonitorCluster(t *testing.T,
	label string,
	opt MonitorClusterOptions,
	samplesBeforeStop int,
	expHttpGets int,
	cfgOk bool) {
	httpGets := 0
	HttpGet = func(url string) (resp *http.Response, err error) {
		httpGets++

		if url == "url0/api/cfg" {
			return &http.Response{
				StatusCode: 200,
				Body: ioutil.NopCloser(bytes.NewBuffer([]byte(`{
"nodeDefsWanted": {
  "nodeDefs": {
    "uuid0": {
      "hostPort": "url0",
      "uuid": "uuid0"
    }
  }
}}`,
				))),
			}, nil
		}

		if url == "http://url0/api/stats" ||
			url == "http://url0/api/diag" {
			return &http.Response{
				StatusCode: 200,
				Body:       ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
			}, nil
		}

		t.Errorf("expected http get, url: %s", url)

		return nil, fmt.Errorf("unexpected to reach here")
	}
	defer func() {
		HttpGet = http.Get
	}()

	sampleCh := make(chan MonitorSample)

	m, err := StartMonitorCluster([]string{"url0"},
		sampleCh, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	s, ok := <-sampleCh
	if !ok ||
		s.Kind != "/api/cfg" ||
		s.Url != "url0" ||
		s.UUID != "" ||
		s.Error != nil ||
		len(s.Data) <= 0 {
		t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
	}

	for i := 0; i < samplesBeforeStop; i++ {
		s, ok := <-sampleCh

		if !ok {
			t.Errorf("unexpected sample ok: %#v, ok: %v", s, ok)
		}

		if cfgOk && s.Kind == "/api/cfg" {
			if s.Url != "url0" ||
				s.UUID != "" ||
				s.Error != nil ||
				len(s.Data) <= 0 {
				t.Errorf("unexpected cfg sample: %#v, ok: %v", s, ok)
			}
			continue
		}

		if (s.Kind != "/api/stats" && s.Kind != "/api/diag") ||
			s.Url != "http://url0" ||
			s.UUID != "uuid0" ||
			s.Error != nil ||
			len(s.Data) == 0 {
			t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
		}
	}

	m.Stop()

	select {
	case s := <-sampleCh:
		t.Errorf("unexpected sample, s: %#v", s)
	default:
	}

	if httpGets != expHttpGets {
		t.Errorf("expected %d http gets, got: %d", expHttpGets, httpGets)
	}
}
<commit_msg>fix race in monitor cluster test on expected gets<commit_after>//  Copyright (c) 2015 Couchbase, Inc.
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the
//  License. You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing,
//  software distributed under the License is distributed on an "AS
//  IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
//  express or implied. See the License for the specific language
//  governing permissions and limitations under the License.

package monitor

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"testing"
	"time"
)

func TestEmptyStartMonitorCluster(t *testing.T) {
	HttpGet = func(url string) (resp *http.Response, err error) {
		t.Errorf("expected no get")
		return nil, nil
	}
	defer func() {
		HttpGet = http.Get
	}()

	opt := MonitorClusterOptions{}

	m, err := StartMonitorCluster(nil, nil, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	m.Stop()
}

func TestStartMonitorClusterEmptyCfg(t *testing.T) {
	httpGets := 0
	HttpGet = func(url string) (resp *http.Response, err error) {
		httpGets++

		if url != "url0/api/cfg" {
			t.Errorf("expected http get, url: %s", url)
		}

		return &http.Response{
			StatusCode: 200,
			Body:       ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
		}, nil
	}
	defer func() {
		HttpGet = http.Get
	}()

	sampleCh := make(chan MonitorSample)

	opt := MonitorClusterOptions{}

	m, err := StartMonitorCluster([]string{"url0"},
		sampleCh, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	s, ok := <-sampleCh
	if !ok ||
		s.Kind != "/api/cfg" ||
		s.Url != "url0" ||
		s.UUID != "" ||
		s.Error != nil ||
		string(s.Data) != "{}" {
		t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
	}

	m.Stop()

	select {
	case <-sampleCh:
		t.Errorf("unexpected sample")
	default:
	}

	if httpGets != 1 {
		t.Errorf("expected 1 http gets")
	}
}

func TestStartMonitorCluster(t *testing.T) {
	opt := MonitorClusterOptions{}
	samplesBeforeStop := 2
	expHttpGets := 3
	testStartMonitorCluster(t, "2x3",
		opt, samplesBeforeStop, expHttpGets, false)

	opt = MonitorClusterOptions{
		CfgSampleInterval: 100,
		MonitorNodesOptions: MonitorNodesOptions{
			StatsSampleInterval: 10000 * time.Second,
			DiagSampleInterval:  10000 * time.Second,
		},
	}
	samplesBeforeStop = 5
	expHttpGets = 6
	testStartMonitorCluster(t, "more cfgs",
		opt, samplesBeforeStop, expHttpGets, true)
}

func testStartMonitorCluster(t *testing.T,
	label string,
	opt MonitorClusterOptions,
	samplesBeforeStop int,
	expHttpGets int,
	cfgOk bool) {
	httpGets := 0
	HttpGet = func(url string) (resp *http.Response, err error) {
		httpGets++

		if url == "url0/api/cfg" {
			return &http.Response{
				StatusCode: 200,
				Body: ioutil.NopCloser(bytes.NewBuffer([]byte(`{
"nodeDefsWanted": {
  "nodeDefs": {
    "uuid0": {
      "hostPort": "url0",
      "uuid": "uuid0"
    }
  }
}}`,
				))),
			}, nil
		}

		if url == "http://url0/api/stats" ||
			url == "http://url0/api/diag" {
			return &http.Response{
				StatusCode: 200,
				Body:       ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
			}, nil
		}

		t.Errorf("expected http get, url: %s", url)

		return nil, fmt.Errorf("unexpected to reach here")
	}
	defer func() {
		HttpGet = http.Get
	}()

	sampleCh := make(chan MonitorSample)

	m, err := StartMonitorCluster([]string{"url0"},
		sampleCh, opt)
	if err != nil || m == nil {
		t.Errorf("expected no err")
	}

	s, ok := <-sampleCh
	if !ok ||
		s.Kind != "/api/cfg" ||
		s.Url != "url0" ||
		s.UUID != "" ||
		s.Error != nil ||
		len(s.Data) <= 0 {
		t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
	}

	for i := 0; i < samplesBeforeStop; i++ {
		s, ok := <-sampleCh

		if !ok {
			t.Errorf("unexpected sample ok: %#v, ok: %v", s, ok)
		}

		if cfgOk && s.Kind == "/api/cfg" {
			if s.Url != "url0" ||
				s.UUID != "" ||
				s.Error != nil ||
				len(s.Data) <= 0 {
				t.Errorf("unexpected cfg sample: %#v, ok: %v", s, ok)
			}
			continue
		}

		if (s.Kind != "/api/stats" && s.Kind != "/api/diag") ||
			s.Url != "http://url0" ||
			s.UUID != "uuid0" ||
			s.Error != nil ||
			len(s.Data) == 0 {
			t.Errorf("unexpected sample: %#v, ok: %v", s, ok)
		}
	}

	m.Stop()

	select {
	case s := <-sampleCh:
		t.Errorf("unexpected sample, s: %#v", s)
	default:
	}

	if httpGets < expHttpGets {
		t.Errorf("expected at least %d http gets, got: %d", expHttpGets, httpGets)
	}
}
<|endoftext|>