<commit_before>package config

import (
	"errors"
	"fmt"
	"net/url"
	"time"
)

const serverConfigFile = "petze.yml"

type Expect struct {
	Max      *int64
	Min      *int64
	Count    *int64
	Contains string
	Equals   interface{}
}

type Check struct {
	Comment     string
	Data        map[string]Expect
	Goquery     map[string]Expect `yaml:"goquery"`
	Header      map[string][]string
	Duration    time.Duration
	StatusCode  int64
	ContentType string `yaml:"content-type"`
}

type Call struct {
	URI         string
	URL         string
	Method      string
	Data        interface{}
	ContentType string `yaml:"content-type"`
	Check       []Check
	Header      map[string][]string
}

// Service a service to monitor
type Service struct {
	ID       string
	Endpoint string
	Interval int64
	Session  []Call
}

type Server struct {
	Address       string
	BasicAuthFile string
	TLS           *struct {
		Address string
		Cert    string
		Key     string
	}
}

func (s *Service) GetURL() (u *url.URL, e error) {
	return url.Parse(s.Endpoint)
}

func (s *Service) IsValid() (valid bool, err error) {
	valid = false
	_, errURL := s.GetURL()
	if errURL != nil {

		err = errors.New("endpoint is invalid: " + errURL.Error())
		return
	}
	for callIndex, call := range s.Session {
		_, callErr := call.IsValid()
		if callErr != nil {
			err = errors.New(fmt.Sprint("invalid call in session @", callIndex, " : ", callErr))
			return
		}
	}
	valid = true
	return
}
func (c *Call) GetURL() (u *url.URL, e error) {
	return url.Parse(c.URI)
}

func (c *Call) IsValid() (valid bool, err error) {
	valid = true
	_, errURL := c.GetURL()
	if errURL != nil {
		err = errors.New("invalid uri " + c.URI + " : " + errURL.Error())
		return
	}
	return
}
<commit_msg>added mime for application/json<commit_after>package config

import (
	"errors"
	"fmt"
	"net/url"
	"time"
)

const (
	ContentTypeJSON = "application/json"
)

const serverConfigFile = "petze.yml"

type Expect struct {
	Max      *int64
	Min      *int64
	Count    *int64
	Contains string
	Equals   interface{}
}

type Check struct {
	Comment     string
	Data        map[string]Expect
	Goquery     map[string]Expect `yaml:"goquery"`
	Header      map[string][]string
	Duration    time.Duration
	StatusCode  int64
	ContentType string `yaml:"content-type"`
}

type Call struct {
	URI         string
	URL         string
	Method      string
	Data        interface{}
	ContentType string `yaml:"content-type"`
	Check       []Check
	Header      map[string][]string
}

// Service a service to monitor
type Service struct {
	ID       string
	Endpoint string
	Interval int64
	Session  []Call
}

type Server struct {
	Address       string
	BasicAuthFile string
	TLS           *struct {
		Address string
		Cert    string
		Key     string
	}
}

func (s *Service) GetURL() (u *url.URL, e error) {
	return url.Parse(s.Endpoint)
}

func (s *Service) IsValid() (valid bool, err error) {
	valid = false
	_, errURL := s.GetURL()
	if errURL != nil {

		err = errors.New("endpoint is invalid: " + errURL.Error())
		return
	}
	for callIndex, call := range s.Session {
		_, callErr := call.IsValid()
		if callErr != nil {
			err = errors.New(fmt.Sprint("invalid call in session @", callIndex, " : ", callErr))
			return
		}
	}
	valid = true
	return
}
func (c *Call) GetURL() (u *url.URL, e error) {
	return url.Parse(c.URI)
}

func (c *Call) IsValid() (valid bool, err error) {
	valid = true
	_, errURL := c.GetURL()
	if errURL != nil {
		err = errors.New("invalid uri " + c.URI + " : " + errURL.Error())
		return
	}
	return
}
<|endoftext|>


<commit_before>package config

import (
	"os"
	"os/user"
	"path/filepath"

	"github.com/gsamokovarov/jump/scoring"
)

const (
	defaultScoreFile  = "scores.json"
	defaultSearchFile = "search.json"
	defaultPinsFile   = "pins.json"
	defaultDirName    = ".jump"
)

// Config represents the config directory and all the miscellaneous
// configuration files we can have in there.
type Config interface {
	ReadEntries() (scoring.Entries, error)
	WriteEntries(scoring.Entries) error

	ReadSearch() Search
	WriteSearch(string, int) error

	ReadPins() (map[string]string, error)
	FindPin(string) (string, bool)
	WritePin(string, string) error
	RemovePin(string) error
}

type fileConfig struct {
	Dir    string
	Scores string
	Search string
	Pins   string
}

// Setup setups the config folder from a directory path.
//
// If the directories don't already exists, they are created and if the score
// file is present, it is loaded.
func Setup(dir string) (Config, error) {
	// We get the directory check for free form os.MkdirAll.
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	scores := filepath.Join(dir, defaultScoreFile)
	search := filepath.Join(dir, defaultSearchFile)
	pins := filepath.Join(dir, defaultPinsFile)

	return &fileConfig{dir, scores, search, pins}, nil
}

// SetupDefault setups the config folder from a directory path.
//
// If the directory path is an empty string, the path is automatically guessed.
func SetupDefault(dir string) (Config, error) {
	dir, err := normalizeDir(dir)
	if err != nil {
		return nil, err
	}

	return Setup(dir)
}

func normalizeDir(dir string) (string, error) {
	if dir != "" {
		return dir, nil
	}

	usr, err := user.Current()
	if err != nil {
		return dir, err
	}

	return filepath.Join(usr.HomeDir, defaultDirName), nil
}
<commit_msg>Respect $HOME for config location<commit_after>package config

import (
	"os"
	"os/user"
	"path/filepath"

	"github.com/gsamokovarov/jump/scoring"
)

const (
	defaultScoreFile  = "scores.json"
	defaultSearchFile = "search.json"
	defaultPinsFile   = "pins.json"
	defaultDirName    = ".jump"
)

// Config represents the config directory and all the miscellaneous
// configuration files we can have in there.
type Config interface {
	ReadEntries() (scoring.Entries, error)
	WriteEntries(scoring.Entries) error

	ReadSearch() Search
	WriteSearch(string, int) error

	ReadPins() (map[string]string, error)
	FindPin(string) (string, bool)
	WritePin(string, string) error
	RemovePin(string) error
}

type fileConfig struct {
	Dir    string
	Scores string
	Search string
	Pins   string
}

// Setup setups the config folder from a directory path.
//
// If the directories don't already exists, they are created and if the score
// file is present, it is loaded.
func Setup(dir string) (Config, error) {
	// We get the directory check for free form os.MkdirAll.
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	scores := filepath.Join(dir, defaultScoreFile)
	search := filepath.Join(dir, defaultSearchFile)
	pins := filepath.Join(dir, defaultPinsFile)

	return &fileConfig{dir, scores, search, pins}, nil
}

// SetupDefault setups the config folder from a directory path.
//
// If the directory path is an empty string, the path is automatically guessed.
func SetupDefault(dir string) (Config, error) {
	dir, err := normalizeDir(dir)
	if err != nil {
		return nil, err
	}

	return Setup(dir)
}

func normalizeDir(dir string) (string, error) {
	if dir != "" {
		return dir, nil
	}

	home, err := homeDir()
	if err != nil {
		return dir, err
	}

	return filepath.Join(home, defaultDirName), nil
}

// See https://github.com/golang/go/issues/26463
func homeDir() (string, error) {
	home := os.Getenv("HOME")
	if home != "" {
		return home, nil
	}
	usr, err := user.Current()
	if err != nil {
		return "", err
	}

	return usr.HomeDir, nil
}
<|endoftext|>


<commit_before>package config

import (
	"io"

	"github.com/BurntSushi/toml"
)

type Config struct {
	Address string
	Port    int

	Events map[string][]Action
}

const (
	Deployment = "deployment"
	Push       = "push"
	Ping       = "ping"
	Release    = "release"
)

type Type string

const (
	Command Type = "command" // Calls <Command>
	Install      = "install" // Calls go install <Package>
	Test         = "test"    //Calls go test <Package>
)

type Action struct {
	Type Type
	//Filter Filter

	Payload string
}

type Filter struct {
	Owner      string
	Repository string
	//Branch      string // Only if the event regards this branch. Not sure how to implement at this time
	Environment string
}

func LoadConfig(r io.Reader) (c Config, err error) {
	_, err = toml.DecodeReader(r, &c)
	return
}

func DefautlConfig() (c Config) {
	return Config{
		Address: "",
		Port:    8080,
		Events: map[string][]Action{
			Deployment: []Action{
				Action{Type: Test, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Install, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Command, Payload: "echo 'test'"},
			},
			Push: []Action{
				Action{Type: Install, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Install, Payload: "github.com/zephyyrr/goda"},
			},
		},
	}
}
<commit_msg>Add functionality to write config file<commit_after>package config

import (
	"io"

	"github.com/BurntSushi/toml"
)

type Config struct {
	Address string
	Port    int

	Events map[string][]Action
}

const (
	Deployment = "deployment"
	Push       = "push"
	Ping       = "ping"
	Release    = "release"
)

type Type string

const (
	Command Type = "command" // Calls <Command>
	Install      = "install" // Calls go install <Package>
	Test         = "test"    //Calls go test <Package>
)

type Action struct {
	Type Type
	//Filter Filter

	Payload string
}

type Filter struct {
	Owner      string
	Repository string
	//Branch      string // Only if the event regards this branch. Not sure how to implement at this time
	Environment string
}

func LoadConfig(r io.Reader) (c Config, err error) {
	_, err = toml.DecodeReader(r, &c)
	return
}

func DefautlConfig() (c Config) {
	return Config{
		Address: "",
		Port:    8080,
		Events: map[string][]Action{
			Deployment: []Action{
				Action{Type: Test, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Install, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Command, Payload: "echo 'test'"},
			},
			Push: []Action{
				Action{Type: Install, Payload: "github.com/zephyyrr/autobot"},
				Action{Type: Install, Payload: "github.com/zephyyrr/goda"},
			},
		},
	}
}

func WriteConfig(w io.Writer, c Config) (err error) {
	enc := toml.NewEncoder(w)
	err = enc.Encode(c)
	return
}
<|endoftext|>


<commit_before>package config

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

	"github.com/Luzifer/go_helpers/str"
	"github.com/cnf/structhash"
	"github.com/robfig/cron"
	"gopkg.in/yaml.v2"
)

// Config represents the map of container configurations
type Config map[string]*ContainerConfig

// ContainerConfig represents a single container to be started on the specified Hosts
type ContainerConfig struct {
	Command         []string          `yaml:"command,omitempty" json:"command"`
	Environment     []string          `yaml:"environment,omitempty" json:"environment"`
	Hosts           []string          `yaml:"hosts" json:"hosts"`
	Image           string            `yaml:"image" json:"image"`
	Links           []string          `yaml:"links" json:"links"`
	Ports           []PortConfig      `yaml:"ports,omitempty" json:"ports"`
	Tag             string            `yaml:"tag" json:"tag"`
	UpdateTimes     []string          `yaml:"update_times,omitempty" json:"updatetimes"`
	Volumes         []string          `yaml:"volumes,omitempty" json:"volumes"`
	StartTimes      string            `yaml:"start_times" json:"starttimes"`
	StopTimeout     uint              `yaml:"stop_timeout" json:"stoptimes"`
	Labels          map[string]string `yaml:"labels" json:"labels"`
	AddCapabilities []string          `yaml:"cap_add" json:"cap_add"`

	nextRun *time.Time
}

// PortConfig maps container ports to host ports
type PortConfig struct {
	Container string `yaml:"container" json:"container"`
	Local     string `yaml:"local" json:"local"`
}

// LoadConfigFromURL retrieves a Config object from a remote URL
func LoadConfigFromURL(url string) (Config, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("Unable to fetch config from URL: %s", err)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("Unable to read HTTP body: %s", err)
	}

	return parseConfig(body)
}

// LoadConfigFromFile retrieves a Config object from a local file
func LoadConfigFromFile(filename string) (Config, error) {
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("Unable to read config from file %q: %s", filename, err)
	}

	return parseConfig(body)
}

func parseConfig(body []byte) (Config, error) {
	result := make(Config)

	err := yaml.Unmarshal(body, &result)
	if err != nil {
		return nil, fmt.Errorf("Unable to parse config: %s", err)
	}

	for k := range result {
		if err := result[k].UpdateNextRun(); err != nil {
			return nil, fmt.Errorf("Unable to update next run: %s", err)
		}
	}

	return result, nil
}

func (c *ContainerConfig) UpdateNextRun() error {
	if c.StartTimes == "" {
		c.nextRun = nil
		return nil
	}

	schedule, err := cron.Parse("0 " + c.StartTimes)
	if err != nil {
		return fmt.Errorf("Invalid start_times %q: %s", c.StartTimes, err)
	}

	nxt := schedule.Next(time.Now())
	c.nextRun = &nxt

	return nil
}

// ShouldBeRunning determines whether a ContainerConfig object should be started
func (c ContainerConfig) ShouldBeRunning(hostname string) bool {
	// Not for our host? Nope.
	if !str.StringInSlice(hostname, c.Hosts) && !str.StringInSlice("ALL", c.Hosts) {
		return false
	}

	return c.nextRun == nil || c.nextRun.Before(time.Now())
}

// Checksum generates a hash over the ContainerConfig to compare it to older versions
func (c ContainerConfig) Checksum() (string, error) {
	return fmt.Sprintf("%x", structhash.Sha1(c, 1)), nil
}

// UpdateAllowedAt checks whether a container may be updated at the given time
func (c ContainerConfig) UpdateAllowedAt(pit time.Time) (bool, error) {
	if len(c.UpdateTimes) == 0 {
		return true, nil
	}

	for _, timeFrame := range c.UpdateTimes {
		times := strings.Split(timeFrame, "-")
		if len(times) != 2 {
			continue
		}

		day := pit.Format("2006-01-02")
		timezone := pit.Format("-0700")

		t1, et1 := time.Parse("2006-01-02 15:04 -0700", fmt.Sprintf("%s %s %s", day, times[0], timezone))
		t2, et2 := time.Parse("2006-01-02 15:04 -0700", fmt.Sprintf("%s %s %s", day, times[1], timezone))
		if et1 != nil || et2 != nil {
			return false, fmt.Errorf("Timeframe '%s' is invalid. Format is HH:MM-HH:MM", timeFrame)
		}

		if t1.Before(pit) && t2.After(pit) {
			return true, nil
		}
	}

	return false, nil
}

func (c ContainerConfig) GetDependencies() []string {
	deps := []string{}

	// Introduce all linked containers as dependencies
	for _, lnk := range c.Links {
		parts := strings.Split(lnk, ":")
		if len(parts) == 2 {
			deps = append(deps, parts[0])
		}
	}

	return deps
}

func (c Config) GetDependencyChain() ([]string, error) {
	chain := []string{}

	// Run as long as not all elements are in the chain
	for len(chain) < len(c) {

		// Detect whether we changed something
		iterationChangedChain := false
		for name, cfg := range c {
			// Already in the chain? Skip it.
			if str.StringInSlice(name, chain) {
				continue
			}

			deps := cfg.GetDependencies()
			if len(deps) == 0 {
				// Doesn't has dependencies? Great, off to the chain!
				chain = append(chain, name)
				iterationChangedChain = true
				continue
			}

			// Look how many of its dependencies are fulfilled
			resolvedDeps := []string{}
			for _, d := range deps {
				if str.StringInSlice(d, chain) {
					resolvedDeps = append(resolvedDeps, d)
				}
			}

			// All of them fulfilled? Great, off to the chain!
			if len(resolvedDeps) == len(deps) {
				chain = append(chain, name)
				iterationChangedChain = true
				continue
			}
		}

		// Didn't manage to add something to the chain? Hello circle!
		if !iterationChangedChain {
			return nil, errors.New("Detected cyclic dependency")
		}

	}

	return chain, nil
}

func (c Config) GetImageList() []string {
	images := []string{}

	for _, cont := range c {
		images = append(images, fmt.Sprintf("%s:%s", cont.Image, cont.Tag))
	}

	return images
}
<commit_msg>Do not take private fields in account for hashes<commit_after>package config

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

	"github.com/Luzifer/go_helpers/str"
	"github.com/cnf/structhash"
	"github.com/robfig/cron"
	"gopkg.in/yaml.v2"
)

// Config represents the map of container configurations
type Config map[string]*ContainerConfig

// ContainerConfig represents a single container to be started on the specified Hosts
type ContainerConfig struct {
	Command         []string          `yaml:"command,omitempty" json:"command"`
	Environment     []string          `yaml:"environment,omitempty" json:"environment"`
	Hosts           []string          `yaml:"hosts" json:"hosts"`
	Image           string            `yaml:"image" json:"image"`
	Links           []string          `yaml:"links" json:"links"`
	Ports           []PortConfig      `yaml:"ports,omitempty" json:"ports"`
	Tag             string            `yaml:"tag" json:"tag"`
	UpdateTimes     []string          `yaml:"update_times,omitempty" json:"updatetimes"`
	Volumes         []string          `yaml:"volumes,omitempty" json:"volumes"`
	StartTimes      string            `yaml:"start_times" json:"starttimes"`
	StopTimeout     uint              `yaml:"stop_timeout" json:"stoptimes"`
	Labels          map[string]string `yaml:"labels" json:"labels"`
	AddCapabilities []string          `yaml:"cap_add" json:"cap_add"`

	nextRun *time.Time `hash:"-"`
}

// PortConfig maps container ports to host ports
type PortConfig struct {
	Container string `yaml:"container" json:"container"`
	Local     string `yaml:"local" json:"local"`
}

// LoadConfigFromURL retrieves a Config object from a remote URL
func LoadConfigFromURL(url string) (Config, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("Unable to fetch config from URL: %s", err)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("Unable to read HTTP body: %s", err)
	}

	return parseConfig(body)
}

// LoadConfigFromFile retrieves a Config object from a local file
func LoadConfigFromFile(filename string) (Config, error) {
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("Unable to read config from file %q: %s", filename, err)
	}

	return parseConfig(body)
}

func parseConfig(body []byte) (Config, error) {
	result := make(Config)

	err := yaml.Unmarshal(body, &result)
	if err != nil {
		return nil, fmt.Errorf("Unable to parse config: %s", err)
	}

	for k := range result {
		if err := result[k].UpdateNextRun(); err != nil {
			return nil, fmt.Errorf("Unable to update next run: %s", err)
		}
	}

	return result, nil
}

func (c *ContainerConfig) UpdateNextRun() error {
	if c.StartTimes == "" {
		c.nextRun = nil
		return nil
	}

	schedule, err := cron.Parse("0 " + c.StartTimes)
	if err != nil {
		return fmt.Errorf("Invalid start_times %q: %s", c.StartTimes, err)
	}

	nxt := schedule.Next(time.Now())
	c.nextRun = &nxt

	return nil
}

// ShouldBeRunning determines whether a ContainerConfig object should be started
func (c ContainerConfig) ShouldBeRunning(hostname string) bool {
	// Not for our host? Nope.
	if !str.StringInSlice(hostname, c.Hosts) && !str.StringInSlice("ALL", c.Hosts) {
		return false
	}

	return c.nextRun == nil || c.nextRun.Before(time.Now())
}

// Checksum generates a hash over the ContainerConfig to compare it to older versions
func (c ContainerConfig) Checksum() (string, error) {
	return fmt.Sprintf("%x", structhash.Sha1(c, 1)), nil
}

// UpdateAllowedAt checks whether a container may be updated at the given time
func (c ContainerConfig) UpdateAllowedAt(pit time.Time) (bool, error) {
	if len(c.UpdateTimes) == 0 {
		return true, nil
	}

	for _, timeFrame := range c.UpdateTimes {
		times := strings.Split(timeFrame, "-")
		if len(times) != 2 {
			continue
		}

		day := pit.Format("2006-01-02")
		timezone := pit.Format("-0700")

		t1, et1 := time.Parse("2006-01-02 15:04 -0700", fmt.Sprintf("%s %s %s", day, times[0], timezone))
		t2, et2 := time.Parse("2006-01-02 15:04 -0700", fmt.Sprintf("%s %s %s", day, times[1], timezone))
		if et1 != nil || et2 != nil {
			return false, fmt.Errorf("Timeframe '%s' is invalid. Format is HH:MM-HH:MM", timeFrame)
		}

		if t1.Before(pit) && t2.After(pit) {
			return true, nil
		}
	}

	return false, nil
}

func (c ContainerConfig) GetDependencies() []string {
	deps := []string{}

	// Introduce all linked containers as dependencies
	for _, lnk := range c.Links {
		parts := strings.Split(lnk, ":")
		if len(parts) == 2 {
			deps = append(deps, parts[0])
		}
	}

	return deps
}

func (c Config) GetDependencyChain() ([]string, error) {
	chain := []string{}

	// Run as long as not all elements are in the chain
	for len(chain) < len(c) {

		// Detect whether we changed something
		iterationChangedChain := false
		for name, cfg := range c {
			// Already in the chain? Skip it.
			if str.StringInSlice(name, chain) {
				continue
			}

			deps := cfg.GetDependencies()
			if len(deps) == 0 {
				// Doesn't has dependencies? Great, off to the chain!
				chain = append(chain, name)
				iterationChangedChain = true
				continue
			}

			// Look how many of its dependencies are fulfilled
			resolvedDeps := []string{}
			for _, d := range deps {
				if str.StringInSlice(d, chain) {
					resolvedDeps = append(resolvedDeps, d)
				}
			}

			// All of them fulfilled? Great, off to the chain!
			if len(resolvedDeps) == len(deps) {
				chain = append(chain, name)
				iterationChangedChain = true
				continue
			}
		}

		// Didn't manage to add something to the chain? Hello circle!
		if !iterationChangedChain {
			return nil, errors.New("Detected cyclic dependency")
		}

	}

	return chain, nil
}

func (c Config) GetImageList() []string {
	images := []string{}

	for _, cont := range c {
		images = append(images, fmt.Sprintf("%s:%s", cont.Image, cont.Tag))
	}

	return images
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The Ceph-CSI Authors.

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

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

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

package util

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/hashicorp/vault/api"
	loss "github.com/libopenstorage/secrets"
	"github.com/libopenstorage/secrets/vault"
)

const (
	// path to service account token that will be used to authenticate with Vault
	// #nosec
	serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

	// vault configuration defaults
	vaultDefaultAuthPath       = "/v1/auth/kubernetes/login"
	vaultDefaultRole           = "csi-kubernetes"
	vaultDefaultNamespace      = ""
	vaultDefaultPassphrasePath = ""
)

/*
VaultKMS represents a Hashicorp Vault KMS configuration

Example JSON structure in the KMS config is,
{
	"local_vault_unique_identifier": {
		"encryptionKMSType": "vault",
		"vaultAddress": "https://127.0.0.1:8500",
		"vaultAuthPath": "/v1/auth/kubernetes/login",
		"vaultRole": "csi-kubernetes",
		"vaultNamespace": "",
		"vaultPassphraseRoot": "/v1/secret",
		"vaultPassphrasePath": "",
		"vaultCAVerify": true,
		"vaultCAFromSecret": "vault-ca"
	},
	...
}.
*/
type VaultKMS struct {
	EncryptionKMSID string

	// vaultPassphrasePath (VPP) used to be added before the "key" of the
	// secret (like /v1/secret/data/<VPP>/key)
	vaultPassphrasePath string

	secrets    loss.Secrets
	keyContext map[string]string
}

// InitVaultKMS returns an interface to HashiCorp Vault KMS.
//
// nolint:gocyclo // this is a long function, as it constructs the Vault config
func InitVaultKMS(kmsID string, config, secrets map[string]string) (EncryptionKMS, error) {
	var (
		ok  bool
		err error
	)

	vaultConfig := make(map[string]interface{})
	keyContext := make(map[string]string)

	kms := &VaultKMS{}
	kms.EncryptionKMSID = kmsID

	vaultAddress, ok := config["vaultAddress"]
	if !ok || vaultAddress == "" {
		return nil, fmt.Errorf("missing 'vaultAddress' for vault KMS %s", kmsID)
	}
	vaultConfig[api.EnvVaultAddress] = vaultAddress

	vaultAuthPath, ok := config["vaultAuthPath"]
	if !ok || vaultAuthPath == "" {
		vaultAuthPath = vaultDefaultAuthPath
	}
	vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
	if err != nil {
		return nil, fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
	}

	vaultRole, ok := config["vaultRole"]
	if !ok || vaultRole == "" {
		vaultRole = vaultDefaultRole
	}
	vaultConfig[vault.AuthKubernetesRole] = vaultRole

	vaultNamespace, ok := config["vaultNamespace"]
	if !ok || vaultNamespace == "" {
		vaultNamespace = vaultDefaultNamespace
	}
	vaultConfig[api.EnvVaultNamespace] = vaultNamespace
	keyContext[loss.KeyVaultNamespace] = vaultNamespace

	// vault.VaultBackendPathKey is "secret/" by default, use vaultPassphraseRoot if configured
	vaultPassphraseRoot, ok := config["vaultPassphraseRoot"]
	if ok && vaultPassphraseRoot != "" {
		// the old example did have "/v1/secret/", convert that format
		if strings.HasPrefix(vaultPassphraseRoot, "/v1/") {
			vaultConfig[vault.VaultBackendPathKey] = strings.TrimPrefix(vaultPassphraseRoot, "/v1/")
		} else {
			vaultConfig[vault.VaultBackendPathKey] = vaultPassphraseRoot
		}
	}

	kms.vaultPassphrasePath, ok = config["vaultPassphrasePath"]
	if !ok || kms.vaultPassphrasePath == "" {
		kms.vaultPassphrasePath = vaultDefaultPassphrasePath
	}

	verifyCA, ok := config["vaultCAVerify"]
	if ok {
		var vaultCAVerify bool
		vaultCAVerify, err = strconv.ParseBool(verifyCA)
		if err != nil {
			return nil, fmt.Errorf("failed to parse 'vaultCAVerify' for vault <%s> kms config: %w",
				kmsID, err)
		}
		vaultConfig[api.EnvVaultInsecure] = !vaultCAVerify
	}

	vaultCAFromSecret, ok := config["vaultCAFromSecret"]
	if ok && vaultCAFromSecret != "" {
		caPEM, ok := secrets[vaultCAFromSecret]
		if !ok {
			return nil, fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
		}
		vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
		if err != nil {
			return nil, fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
		}
		// TODO: delete f.Name() when VaultKMS is destroyed
	}

	// FIXME: vault.AuthKubernetesTokenPath is not enough? EnvVaultToken needs to be set?
	vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
	vaultConfig[vault.AuthKubernetesTokenPath] = serviceAccountTokenPath

	v, err := vault.New(vaultConfig)
	if err != nil {
		return nil, fmt.Errorf("failed creating new Vault Secrets: %w", err)
	}
	kms.secrets = v

	kms.keyContext = keyContext

	return kms, nil
}

// GetID is returning correlation ID to KMS configuration.
func (kms *VaultKMS) GetID() string {
	return kms.EncryptionKMSID
}

// GetPassphrase returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (kms *VaultKMS) GetPassphrase(key string) (string, error) {
	s, err := kms.secrets.GetSecret(filepath.Join(kms.vaultPassphrasePath, key), kms.keyContext)
	if errors.Is(err, loss.ErrInvalidSecretId) {
		return "", MissingPassphrase{err}
	} else if err != nil {
		return "", err
	}

	data, ok := s["data"].(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("failed parsing data for get passphrase request for %s", key)
	}
	passphrase, ok := data["passphrase"].(string)
	if !ok {
		return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %s", key)
	}

	return passphrase, nil
}

// SavePassphrase saves new passphrase in Vault.
func (kms *VaultKMS) SavePassphrase(key, value string) error {
	data := map[string]interface{}{
		"data": map[string]string{
			"passphrase": value,
		},
	}

	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.PutSecret(pathKey, data, kms.keyContext)
	if err != nil {
		return fmt.Errorf("saving passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// DeletePassphrase deletes passphrase from Vault.
func (kms *VaultKMS) DeletePassphrase(key string) error {
	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.DeleteSecret(pathKey, kms.keyContext)
	if err != nil {
		return fmt.Errorf("delete passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// detectAuthMountPath takes the vaultAuthPath configuration option that
// defaults to "/v1/auth/kubernetes/login" and makes it a vault.AuthMountPath
// like "kubernetes".
func detectAuthMountPath(path string) (string, error) {
	var authMountPath string

	if path == "" {
		return "", errors.New("path is empty")
	}

	// add all components between "login" and "auth" to authMountPath
	match := false
	parts := strings.Split(path, "/")
	for _, part := range parts {
		if part == "auth" {
			match = true
			continue
		}
		if part == "login" {
			break
		}
		if match && authMountPath == "" {
			authMountPath = part
		} else if match {
			authMountPath += "/" + part
		}
	}

	// in case authMountPath is empty, return original path as it was
	if authMountPath == "" {
		authMountPath = path
	}

	return authMountPath, nil
}

// createTempFile writes data to a temporary file that contains the pattern in
// the filename (see ioutil.TempFile for details).
func createTempFile(pattern string, data []byte) (string, error) {
	t, err := ioutil.TempFile("", pattern)
	if err != nil {
		return "", fmt.Errorf("failed to create temporary file: %w", err)
	}

	// delete the tmpfile on error
	defer func() {
		if err != nil {
			// ignore error on failure to remove tmpfile (gosec complains)
			_ = os.Remove(t.Name())
		}
	}()

	s, err := t.Write(data)
	if err != nil || s != len(data) {
		return "", fmt.Errorf("failed to write temporary file: %w", err)
	}
	err = t.Close()
	if err != nil {
		return "", fmt.Errorf("failed to close temporary file: %w", err)
	}

	return t.Name(), nil
}
<commit_msg>rebase: split vaultConnection from VaultKMS<commit_after>/*
Copyright 2019 The Ceph-CSI Authors.

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

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

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

package util

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/hashicorp/vault/api"
	loss "github.com/libopenstorage/secrets"
	"github.com/libopenstorage/secrets/vault"
)

const (
	// path to service account token that will be used to authenticate with Vault
	// #nosec
	serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

	// vault configuration defaults
	vaultDefaultAuthPath       = "/v1/auth/kubernetes/login"
	vaultDefaultRole           = "csi-kubernetes"
	vaultDefaultNamespace      = ""
	vaultDefaultPassphrasePath = ""
)

/*
VaultKMS represents a Hashicorp Vault KMS configuration

Example JSON structure in the KMS config is,
{
	"local_vault_unique_identifier": {
		"encryptionKMSType": "vault",
		"vaultAddress": "https://127.0.0.1:8500",
		"vaultAuthPath": "/v1/auth/kubernetes/login",
		"vaultRole": "csi-kubernetes",
		"vaultNamespace": "",
		"vaultPassphraseRoot": "/v1/secret",
		"vaultPassphrasePath": "",
		"vaultCAVerify": true,
		"vaultCAFromSecret": "vault-ca"
	},
	...
}.
*/

type vaultConnection struct {
	EncryptionKMSID string
	vaultConfig     map[string]interface{}
	keyContext      map[string]string
}

type VaultKMS struct {
	vaultConnection

	// vaultPassphrasePath (VPP) used to be added before the "key" of the
	// secret (like /v1/secret/data/<VPP>/key)
	vaultPassphrasePath string

	secrets loss.Secrets
}

func (vc *vaultConnection) initConnection(kmsID string, config, secrets map[string]string) error {
	vaultConfig := make(map[string]interface{})
	keyContext := make(map[string]string)

	vc.EncryptionKMSID = kmsID

	vaultAddress, ok := config["vaultAddress"]
	if !ok || vaultAddress == "" {
		return errors.New("missing 'vaultAddress' for Vault connection")
	}
	vaultConfig[api.EnvVaultAddress] = vaultAddress

	vaultNamespace, ok := config["vaultNamespace"]
	if !ok || vaultNamespace == "" {
		vaultNamespace = vaultDefaultNamespace
	}
	vaultConfig[api.EnvVaultNamespace] = vaultNamespace
	keyContext[loss.KeyVaultNamespace] = vaultNamespace

	verifyCA, ok := config["vaultCAVerify"]
	if ok {
		var vaultCAVerify bool
		vaultCAVerify, err := strconv.ParseBool(verifyCA)
		if err != nil {
			return fmt.Errorf("failed to parse 'vaultCAVerify': %w", err)
		}
		vaultConfig[api.EnvVaultInsecure] = !vaultCAVerify
	}

	vaultCAFromSecret, ok := config["vaultCAFromSecret"]
	if ok && vaultCAFromSecret != "" {
		caPEM, ok := secrets[vaultCAFromSecret]
		if !ok {
			return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
		}

		var err error
		vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
		if err != nil {
			return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
		}
		// TODO: delete f.Name() when vaultConnection is destroyed
	}

	vc.keyContext = keyContext
	vc.vaultConfig = vaultConfig

	return nil
}

// InitVaultKMS returns an interface to HashiCorp Vault KMS.
func InitVaultKMS(kmsID string, config, secrets map[string]string) (EncryptionKMS, error) {
	var (
		ok  bool
		err error
	)

	kms := &VaultKMS{}
	err = kms.initConnection(kmsID, config, secrets)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
	}

	vaultAuthPath, ok := config["vaultAuthPath"]
	if !ok || vaultAuthPath == "" {
		vaultAuthPath = vaultDefaultAuthPath
	}
	kms.vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
	if err != nil {
		return nil, fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
	}

	vaultRole, ok := config["vaultRole"]
	if !ok || vaultRole == "" {
		vaultRole = vaultDefaultRole
	}
	kms.vaultConfig[vault.AuthKubernetesRole] = vaultRole

	// vault.VaultBackendPathKey is "secret/" by default, use vaultPassphraseRoot if configured
	vaultPassphraseRoot, ok := config["vaultPassphraseRoot"]
	if ok && vaultPassphraseRoot != "" {
		// the old example did have "/v1/secret/", convert that format
		if strings.HasPrefix(vaultPassphraseRoot, "/v1/") {
			kms.vaultConfig[vault.VaultBackendPathKey] = strings.TrimPrefix(vaultPassphraseRoot, "/v1/")
		} else {
			kms.vaultConfig[vault.VaultBackendPathKey] = vaultPassphraseRoot
		}
	}

	kms.vaultPassphrasePath, ok = config["vaultPassphrasePath"]
	if !ok || kms.vaultPassphrasePath == "" {
		kms.vaultPassphrasePath = vaultDefaultPassphrasePath
	}

	// FIXME: vault.AuthKubernetesTokenPath is not enough? EnvVaultToken needs to be set?
	kms.vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
	kms.vaultConfig[vault.AuthKubernetesTokenPath] = serviceAccountTokenPath

	v, err := vault.New(kms.vaultConfig)
	if err != nil {
		return nil, fmt.Errorf("failed creating new Vault Secrets: %w", err)
	}
	kms.secrets = v

	return kms, nil
}

// GetID is returning correlation ID to KMS configuration.
func (vc *vaultConnection) GetID() string {
	return vc.EncryptionKMSID
}

// GetPassphrase returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (kms *VaultKMS) GetPassphrase(key string) (string, error) {
	s, err := kms.secrets.GetSecret(filepath.Join(kms.vaultPassphrasePath, key), kms.keyContext)
	if errors.Is(err, loss.ErrInvalidSecretId) {
		return "", MissingPassphrase{err}
	} else if err != nil {
		return "", err
	}

	data, ok := s["data"].(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("failed parsing data for get passphrase request for %s", key)
	}
	passphrase, ok := data["passphrase"].(string)
	if !ok {
		return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %s", key)
	}

	return passphrase, nil
}

// SavePassphrase saves new passphrase in Vault.
func (kms *VaultKMS) SavePassphrase(key, value string) error {
	data := map[string]interface{}{
		"data": map[string]string{
			"passphrase": value,
		},
	}

	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.PutSecret(pathKey, data, kms.keyContext)
	if err != nil {
		return fmt.Errorf("saving passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// DeletePassphrase deletes passphrase from Vault.
func (kms *VaultKMS) DeletePassphrase(key string) error {
	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.DeleteSecret(pathKey, kms.keyContext)
	if err != nil {
		return fmt.Errorf("delete passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// detectAuthMountPath takes the vaultAuthPath configuration option that
// defaults to "/v1/auth/kubernetes/login" and makes it a vault.AuthMountPath
// like "kubernetes".
func detectAuthMountPath(path string) (string, error) {
	var authMountPath string

	if path == "" {
		return "", errors.New("path is empty")
	}

	// add all components between "login" and "auth" to authMountPath
	match := false
	parts := strings.Split(path, "/")
	for _, part := range parts {
		if part == "auth" {
			match = true
			continue
		}
		if part == "login" {
			break
		}
		if match && authMountPath == "" {
			authMountPath = part
		} else if match {
			authMountPath += "/" + part
		}
	}

	// in case authMountPath is empty, return original path as it was
	if authMountPath == "" {
		authMountPath = path
	}

	return authMountPath, nil
}

// createTempFile writes data to a temporary file that contains the pattern in
// the filename (see ioutil.TempFile for details).
func createTempFile(pattern string, data []byte) (string, error) {
	t, err := ioutil.TempFile("", pattern)
	if err != nil {
		return "", fmt.Errorf("failed to create temporary file: %w", err)
	}

	// delete the tmpfile on error
	defer func() {
		if err != nil {
			// ignore error on failure to remove tmpfile (gosec complains)
			_ = os.Remove(t.Name())
		}
	}()

	s, err := t.Write(data)
	if err != nil || s != len(data) {
		return "", fmt.Errorf("failed to write temporary file: %w", err)
	}
	err = t.Close()
	if err != nil {
		return "", fmt.Errorf("failed to close temporary file: %w", err)
	}

	return t.Name(), nil
}
<|endoftext|>


<commit_before>package cmd

import (
	"github.com/freneticmonkey/migrate/migrate/config"
	"github.com/freneticmonkey/migrate/migrate/git"
	"github.com/freneticmonkey/migrate/migrate/id"
	"github.com/freneticmonkey/migrate/migrate/mysql"
	"github.com/freneticmonkey/migrate/migrate/table"
	"github.com/freneticmonkey/migrate/migrate/util"
	"github.com/freneticmonkey/migrate/migrate/yaml"
	"github.com/urfave/cli"
)

// GetDiffCommand Configure the validate command
func GetDiffCommand(conf *config.Config) (setup cli.Command) {
	problems := 0
	setup = cli.Command{
		Name:  "diff",
		Usage: "Diff the MySQL target database and the YAML schema.",
		Flags: []cli.Flag{
			cli.StringFlag{
				Name:  "project",
				Value: "",
				Usage: "The target project",
			},
			cli.StringFlag{
				Name:  "version",
				Value: "",
				Usage: "The target git version",
			},
		},
		Action: func(ctx *cli.Context) error {
			if ctx.IsSet("project") && ctx.IsSet("version") {
				git.Clone(conf.Project)
			}

			// Read the YAML files cloned from the repo
			err := yaml.ReadTables(conf.Options.WorkingPath)
			if util.ErrorCheck(err) {
				return cli.NewExitError("Diff failed. Unable to read YAML Tables", 1)
			}
			problems, err = id.ValidateSchema(yaml.Schema, "YAML Schema")
			if util.ErrorCheck(err) {
				return cli.NewExitError("Validation failed. YAML Errors found", problems)
			}

			// Read the MySQL tables from the target database
			err = mysql.ReadTables(conf.Project)
			if util.ErrorCheck(err) {
				return cli.NewExitError("Diff failed. Unable to read MySQL Tables", 1)
			}
			problems, err = id.ValidateSchema(mysql.Schema, "Target Database Schema")
			if util.ErrorCheck(err) {
				return cli.NewExitError("Validation failed. Problems with Target Database Detected", problems)
			}

			forwardDiff := table.DiffTables(yaml.Schema, mysql.Schema)
			mysql.GenerateAlters(forwardDiff)

			return cli.NewExitError("Diff completed successfully.", 0)
		},
	}
	return setup
}
<commit_msg>Fixed problem with the diff command which meant that it wouldn't checkout from git<commit_after>package cmd

import (
	"github.com/freneticmonkey/migrate/migrate/config"
	"github.com/freneticmonkey/migrate/migrate/git"
	"github.com/freneticmonkey/migrate/migrate/id"
	"github.com/freneticmonkey/migrate/migrate/mysql"
	"github.com/freneticmonkey/migrate/migrate/table"
	"github.com/freneticmonkey/migrate/migrate/util"
	"github.com/freneticmonkey/migrate/migrate/yaml"
	"github.com/urfave/cli"
)

// GetDiffCommand Configure the validate command
func GetDiffCommand(conf *config.Config) (setup cli.Command) {
	problems := 0
	setup = cli.Command{
		Name:  "diff",
		Usage: "Diff the MySQL target database and the YAML schema.",
		Flags: []cli.Flag{
			cli.StringFlag{
				Name:  "project",
				Value: "",
				Usage: "The target project",
			},
			cli.StringFlag{
				Name:  "version",
				Value: "",
				Usage: "The target git version",
			},
		},
		Action: func(ctx *cli.Context) error {

			// Override the project settings with the command line flags
			if ctx.IsSet("version") {
				conf.Project.Version = ctx.String("version")
			}

			if ctx.IsSet("project") {
				conf.Project.Name = ctx.String("project")
				git.Clone(conf.Project)
			} else {
				util.LogInfo("No project specified.  Comparing the current state of the YAML schema in working path.")
			}

			// Read the YAML files cloned from the repo
			err := yaml.ReadTables(conf.Options.WorkingPath)
			if util.ErrorCheck(err) {
				return cli.NewExitError("Diff failed. Unable to read YAML Tables", 1)
			}
			problems, err = id.ValidateSchema(yaml.Schema, "YAML Schema")
			if util.ErrorCheck(err) {
				return cli.NewExitError("Validation failed. YAML Errors found", problems)
			}

			// Read the MySQL tables from the target database
			err = mysql.ReadTables(conf.Project)
			if util.ErrorCheck(err) {
				return cli.NewExitError("Diff failed. Unable to read MySQL Tables", 1)
			}
			problems, err = id.ValidateSchema(mysql.Schema, "Target Database Schema")
			if util.ErrorCheck(err) {
				return cli.NewExitError("Validation failed. Problems with Target Database Detected", problems)
			}

			forwardDiff := table.DiffTables(yaml.Schema, mysql.Schema)
			mysql.GenerateAlters(forwardDiff)

			return cli.NewExitError("Diff completed successfully.", 0)
		},
	}
	return setup
}
<|endoftext|>


<commit_before>// +build linux

package nodes

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"

	"github.com/dotcloud/docker/pkg/system"
)

// Default list of device nodes to copy
var DefaultNodes = []string{
	"null",
	"zero",
	"full",
	"random",
	"urandom",
	"tty",
}

// CopyN copies the device node from the host into the rootfs
func CopyN(rootfs string, nodesToCopy []string, shouldExist bool) error {
	oldMask := system.Umask(0000)
	defer system.Umask(oldMask)

	for _, node := range nodesToCopy {
		if err := Copy(rootfs, node, shouldExist); err != nil {
			return err
		}
	}
	return nil
}

// Copy copies the device node into the rootfs.  If the node
// on the host system does not exist and the boolean flag is passed
// an error will be returned
func Copy(rootfs, node string, shouldExist bool) error {
	stat, err := os.Stat(filepath.Join("/dev", node))
	if err != nil {
		if os.IsNotExist(err) && !shouldExist {
			return nil
		}
		return err
	}

	var (
		dest = filepath.Join(rootfs, "dev", node)
		st   = stat.Sys().(*syscall.Stat_t)
	)

	if err := system.Mknod(dest, st.Mode, int(st.Rdev)); err != nil && !os.IsExist(err) {
		return fmt.Errorf("mknod %s %s", node, err)
	}
	return nil
}

func GetHostDeviceNodes() ([]string, error) {
	files, err := ioutil.ReadDir("/dev")
	if err != nil {
		return nil, err
	}

	out := []string{}
	for _, f := range files {
		if f.Mode()&os.ModeDevice == os.ModeDevice {
			out = append(out, f.Name())
		}
	}
	return out, nil
}
<commit_msg>add recursive device nodes<commit_after>// +build linux

package nodes

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"

	"github.com/dotcloud/docker/pkg/system"
)

// Default list of device nodes to copy
var DefaultNodes = []string{
	"/dev/null",
	"/dev/zero",
	"/dev/full",
	"/dev/random",
	"/dev/urandom",
	"/dev/tty",
}

// CopyN copies the device node from the host into the rootfs
func CopyN(rootfs string, nodesToCopy []string, shouldExist bool) error {
	oldMask := system.Umask(0000)
	defer system.Umask(oldMask)

	for _, node := range nodesToCopy {
		if err := Copy(rootfs, node, shouldExist); err != nil {
			return err
		}
	}
	return nil
}

// Copy copies the device node into the rootfs.  If the node
// on the host system does not exist and the boolean flag is passed
// an error will be returned
func Copy(rootfs, node string, shouldExist bool) error {
	stat, err := os.Stat(node)
	if err != nil {
		if os.IsNotExist(err) && !shouldExist {
			return nil
		}
		return err
	}

	var (
		dest   = filepath.Join(rootfs, node)
		st     = stat.Sys().(*syscall.Stat_t)
		parent = filepath.Dir(dest)
	)

	if err := os.MkdirAll(parent, 0755); err != nil {
		return err
	}

	if err := system.Mknod(dest, st.Mode, int(st.Rdev)); err != nil && !os.IsExist(err) {
		return fmt.Errorf("mknod %s %s", node, err)
	}
	return nil
}

func getNodes(path string) ([]string, error) {
	out := []string{}
	files, err := ioutil.ReadDir(path)
	if err != nil {
		return nil, err
	}
	for _, f := range files {
		if f.IsDir() && f.Name() != "pts" && f.Name() != "shm" {
			sub, err := getNodes(filepath.Join(path, f.Name()))
			if err != nil {
				return nil, err
			}
			out = append(out, sub...)
		} else if f.Mode()&os.ModeDevice == os.ModeDevice {
			out = append(out, filepath.Join(path, f.Name()))
		}
	}
	return out, nil
}

func GetHostDeviceNodes() ([]string, error) {
	return getNodes("/dev")
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Kubernetes Authors.

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

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

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

package mount

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"
	"testing"
)

func TestDoCleanupMountPoint(t *testing.T) {
	const testMount = "test-mount"
	const defaultPerm = 0750

	tests := map[string]struct {
		corruptedMnt bool
		// Function that prepares the directory structure for the test under
		// the given base directory.
		// Returns a fake MountPoint, a fake error for the mount point,
		// and error if the prepare function encountered a fatal error.
		prepare   func(base string) (MountPoint, error, error)
		expectErr bool
	}{
		"mount-ok": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, nil, nil
			},
		},
		"mount-corrupted": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ESTALE), nil
			},
			corruptedMnt: true,
		},
		"mount-err-not-corrupted": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ETIMEDOUT), nil
			},
			expectErr: true,
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {

			tmpDir, err := ioutil.TempDir("", "unmount-mount-point-test")
			if err != nil {
				t.Fatalf("failed to create tmpdir: %v", err)
			}
			defer os.RemoveAll(tmpDir)

			if tt.prepare == nil {
				t.Fatalf("prepare function required")
			}

			mountPoint, mountError, err := tt.prepare(tmpDir)
			if err != nil {
				t.Fatalf("failed to prepare test: %v", err)
			}

			fake := &FakeMounter{
				MountPoints:      []MountPoint{mountPoint},
				MountCheckErrors: map[string]error{mountPoint.Path: mountError},
			}

			err = doCleanupMountPoint(mountPoint.Path, fake, true, tt.corruptedMnt)
			if tt.expectErr {
				if err == nil {
					t.Errorf("test %s failed, expected error, got none", name)
				}
				if err := validateDirExists(mountPoint.Path); err != nil {
					t.Errorf("test %s failed, mount path doesn't exist: %v", name, err)
				}
			}
			if !tt.expectErr {
				if err != nil {
					t.Errorf("test %s failed: %v", name, err)
				}
				if err := validateDirNotExists(mountPoint.Path); err != nil {
					t.Errorf("test %s failed, mount path still exists: %v", name, err)
				}
			}
		})
	}
}

func validateDirEmpty(dir string) error {
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		return err
	}

	if len(files) != 0 {
		return fmt.Errorf("Directory %q is not empty", dir)
	}
	return nil
}

func validateDirExists(dir string) error {
	_, err := ioutil.ReadDir(dir)
	if err != nil {
		return err
	}
	return nil
}

func validateDirNotExists(dir string) error {
	_, err := ioutil.ReadDir(dir)
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return err
	}
	return fmt.Errorf("dir %q still exists", dir)
}

func validateFileExists(file string) error {
	if _, err := os.Stat(file); err != nil {
		return err
	}
	return nil
}
<commit_msg>remove unused code<commit_after>/*
Copyright 2018 The Kubernetes Authors.

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

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

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

package mount

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"
	"testing"
)

func TestDoCleanupMountPoint(t *testing.T) {
	const testMount = "test-mount"
	const defaultPerm = 0750

	tests := map[string]struct {
		corruptedMnt bool
		// Function that prepares the directory structure for the test under
		// the given base directory.
		// Returns a fake MountPoint, a fake error for the mount point,
		// and error if the prepare function encountered a fatal error.
		prepare   func(base string) (MountPoint, error, error)
		expectErr bool
	}{
		"mount-ok": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, nil, nil
			},
		},
		"mount-corrupted": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ESTALE), nil
			},
			corruptedMnt: true,
		},
		"mount-err-not-corrupted": {
			prepare: func(base string) (MountPoint, error, error) {
				path := filepath.Join(base, testMount)
				if err := os.MkdirAll(path, defaultPerm); err != nil {
					return MountPoint{}, nil, err
				}
				return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ETIMEDOUT), nil
			},
			expectErr: true,
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {

			tmpDir, err := ioutil.TempDir("", "unmount-mount-point-test")
			if err != nil {
				t.Fatalf("failed to create tmpdir: %v", err)
			}
			defer os.RemoveAll(tmpDir)

			if tt.prepare == nil {
				t.Fatalf("prepare function required")
			}

			mountPoint, mountError, err := tt.prepare(tmpDir)
			if err != nil {
				t.Fatalf("failed to prepare test: %v", err)
			}

			fake := &FakeMounter{
				MountPoints:      []MountPoint{mountPoint},
				MountCheckErrors: map[string]error{mountPoint.Path: mountError},
			}

			err = doCleanupMountPoint(mountPoint.Path, fake, true, tt.corruptedMnt)
			if tt.expectErr {
				if err == nil {
					t.Errorf("test %s failed, expected error, got none", name)
				}
				if err := validateDirExists(mountPoint.Path); err != nil {
					t.Errorf("test %s failed, mount path doesn't exist: %v", name, err)
				}
			}
			if !tt.expectErr {
				if err != nil {
					t.Errorf("test %s failed: %v", name, err)
				}
				if err := validateDirNotExists(mountPoint.Path); err != nil {
					t.Errorf("test %s failed, mount path still exists: %v", name, err)
				}
			}
		})
	}
}

func validateDirExists(dir string) error {
	_, err := ioutil.ReadDir(dir)
	if err != nil {
		return err
	}
	return nil
}

func validateDirNotExists(dir string) error {
	_, err := ioutil.ReadDir(dir)
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return err
	}
	return fmt.Errorf("dir %q still exists", dir)
}
<|endoftext|>


<commit_before>package moves

import (
	"errors"

	"github.com/jkomoros/boardgame"
	"github.com/jkomoros/boardgame/moves/interfaces"
)

//SeatPlayer is a game that seats a new player into an open seat in the game. It
//is a special interface point for the server library to interact with your game
//logic. The core engine has no notion of whether or not a real user is
//associated with any given player slot. The server package does distinguish
//this, keeping track of which player slots need to be filled by real users. But
//by default your core game logic can't detect which player slots haven't been
//filled, or when they are filled. SeatPlayer, when used in conjunction with
//behaviors.Seat, introduces the notion of a Seat to each player slot. Those
//properties communicate whether the seat is filled with a physical player, and
//whether it is open to having a player sit in it. SeatPlayer is a special type
//of move that will be proposed by the server engine when it has a player
//waiting to be seated. Your core game logic can decide when it should be legal
//based on which phases it is configured to be legal in. If you do not
//explicitly configure SeatPlayer (or a move that derives from it) in your game
//then the server will not alert you when a player has been seated.
//
//You may use this move directly, or embed it in a move of your own that
//overrides some logic, like for example DefaultsForState to override where the
//next player is seated.
//
//If you don't want a seat to have players seated in it, even if it's not yet
//filled, then you can call SetSeatClosed() method on the player state. The move
//CloseEmptySeats will automatically mark all currently unfilled seats as
//closed, so no new players will be accepted.
//
//It is NOT a FixUp move; it is a special move that is only every proposed by
//the server itself.
//
//boardgame:codegen
type SeatPlayer struct {
	Default
	TargetPlayerIndex boardgame.PlayerIndex
}

//IsSeatPlayerMove returns true. This is a way for moves to signal to other
//libraries that it's a SeatPlayer move, even if it isn't literally this move
//struct but a subclass of it. Implements interfaces.SeatPlayerMover.
func (s *SeatPlayer) IsSeatPlayerMove() bool {
	return true
}

//DefaultsForState sets TargetPlayerIndex to the next player who is neither
//filled nor closed.
func (s *SeatPlayer) DefaultsForState(state boardgame.ImmutableState) {
	var index int
	for index = 0; index < len(state.ImmutablePlayerStates()); index++ {
		player := state.ImmutablePlayerStates()[index]
		if seat, ok := player.(interfaces.Seater); ok {
			if seat.SeatIsClosed() || seat.SeatIsFilled() {
				continue
			}
			s.TargetPlayerIndex = boardgame.PlayerIndex(index)
			return
		}
	}
}

//Legal verifies that TargetPlayerIndex is set to a player who is both not
//filled and not closed, and that the proposer is the admin, since only server
//should propose this move.
func (s *SeatPlayer) Legal(state boardgame.ImmutableState, proposer boardgame.PlayerIndex) error {
	if err := s.Default.Legal(state, proposer); err != nil {
		return err
	}
	if proposer != boardgame.AdminPlayerIndex {
		return errors.New("This move may only be proposed by an admin")
	}
	if s.TargetPlayerIndex < 0 || int(s.TargetPlayerIndex) >= len(state.ImmutablePlayerStates()) {
		return errors.New("TargetPlayerIndex is invalid")
	}
	seat, ok := state.ImmutablePlayerStates()[s.TargetPlayerIndex].(interfaces.Seater)
	if !ok {
		return errors.New("The selected player did not implement interfaces.Seater")
	}
	if seat.SeatIsClosed() {
		return errors.New("The selected seat was closed")
	}
	if seat.SeatIsFilled() {
		return errors.New("The selected seat was already filled")
	}
	return nil
}

//Apply sets the targeted player to be Filled. If the player state also
//implements interfaces.Inactiver (for example because it implements
//behaviors.PlayerInactive), then it will also set the player to inactive. This
//is often the behavior you want; if you're in the middle of a round you
//typically don't want a new player to be active in the middle of it. But if you
//do use behaviors.PlayerInactive, remember to implement ActivateInactivePlayer
//at the beginning of rounds to activate any new seated players.
func (s *SeatPlayer) Apply(state boardgame.State) error {
	player := state.ImmutablePlayerStates()[s.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	seat.SetSeatFilled()
	if inactiver, ok := player.(interfaces.PlayerInactiver); ok {
		inactiver.SetPlayerInactive()
	}
	return nil
}

//ValidConfiguration checks that player states implement interfaces.Seater
func (s *SeatPlayer) ValidConfiguration(exampleState boardgame.State) error {
	player := exampleState.ImmutablePlayerStates()[0]
	_, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater. behaviors.Seat implements it for free")
	}
	return nil
}

//FallbackHelpText returns "Marks the next available seat as seated, which when
//done will mean the next player is part of the game"
func (s *SeatPlayer) FallbackHelpText() string {
	return "Marks the next available seat as seated, which when done will mean the next player is part of the game"
}

//FallbackName returns "Seat Player"
func (s *SeatPlayer) FallbackName() string {
	return "Seat Player"
}

//CloseEmptySeat is a move that will go through and repeatedly apply itself to
//close any seat that is not filled. Typically you put this at the end of a
//SetUp phase, once all of the players are there who you care to wait for, and
//want to tell the game to not try to seat any more people in them.
//
//boardgame:codegen
type CloseEmptySeat struct {
	FixUpMulti
	TargetPlayerIndex boardgame.PlayerIndex
}

//DefaultsForState sets TargetPlayerIndex to the next player who is currently
//marked as empty, according to interfaces.Seater.
func (c *CloseEmptySeat) DefaultsForState(state boardgame.ImmutableState) {
	var index int
	for index = 0; index < len(state.ImmutablePlayerStates()); index++ {
		player := state.ImmutablePlayerStates()[index]
		if seat, ok := player.(interfaces.Seater); ok {
			if !seat.SeatIsFilled() && !seat.SeatIsClosed() {
				c.TargetPlayerIndex = boardgame.PlayerIndex(index)
				return
			}
		}
	}
}

//Legal verifies that TargetPlayerIndex is set to a player that is currently
//empty and not currently closed.
func (c *CloseEmptySeat) Legal(state boardgame.ImmutableState, proposer boardgame.PlayerIndex) error {
	if err := c.FixUpMulti.Legal(state, proposer); err != nil {
		return err
	}
	if c.TargetPlayerIndex < 0 || int(c.TargetPlayerIndex) >= len(state.ImmutablePlayerStates()) {
		return errors.New("Invalid TargetPlayerIndex")
	}
	player := state.ImmutablePlayerStates()[c.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	if !seat.SeatIsFilled() {
		return errors.New("The selected player seat is already filled, not empty. There must not be any seats left to apply to")
	}
	if !seat.SeatIsClosed() {
		return errors.New("The selected player seat is already closed. There must not be any seats left to apply to")
	}
	return nil
}

//Apply sets the TargetPlayerIndex to be closed via interfaces.Seater
func (c *CloseEmptySeat) Apply(state boardgame.State) error {
	player := state.ImmutablePlayerStates()[c.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	seat.SetSeatClosed()
	return nil
}

//ValidConfiguration checks that player states implement interfaces.Seater
func (c *CloseEmptySeat) ValidConfiguration(exampleState boardgame.State) error {
	player := exampleState.ImmutablePlayerStates()[0]
	_, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater. behaviors.Seat implements it for free")
	}
	return nil
}

//FallbackHelpText returns "Marks any empty seats as being not open for more people to be seated."
func (c *CloseEmptySeat) FallbackHelpText() string {
	return "Marks any empty seats as being not open for more people to be seated."
}

//FallbackName returns "Close Empty Seat"
func (c *CloseEmptySeat) FallbackName() string {
	return "Close Empty Seat"
}
<commit_msg>Fix logic error in last commit where CloseEmptySeats Legal had backwards logic. Part of #755.<commit_after>package moves

import (
	"errors"

	"github.com/jkomoros/boardgame"
	"github.com/jkomoros/boardgame/moves/interfaces"
)

//SeatPlayer is a game that seats a new player into an open seat in the game. It
//is a special interface point for the server library to interact with your game
//logic. The core engine has no notion of whether or not a real user is
//associated with any given player slot. The server package does distinguish
//this, keeping track of which player slots need to be filled by real users. But
//by default your core game logic can't detect which player slots haven't been
//filled, or when they are filled. SeatPlayer, when used in conjunction with
//behaviors.Seat, introduces the notion of a Seat to each player slot. Those
//properties communicate whether the seat is filled with a physical player, and
//whether it is open to having a player sit in it. SeatPlayer is a special type
//of move that will be proposed by the server engine when it has a player
//waiting to be seated. Your core game logic can decide when it should be legal
//based on which phases it is configured to be legal in. If you do not
//explicitly configure SeatPlayer (or a move that derives from it) in your game
//then the server will not alert you when a player has been seated.
//
//You may use this move directly, or embed it in a move of your own that
//overrides some logic, like for example DefaultsForState to override where the
//next player is seated.
//
//If you don't want a seat to have players seated in it, even if it's not yet
//filled, then you can call SetSeatClosed() method on the player state. The move
//CloseEmptySeats will automatically mark all currently unfilled seats as
//closed, so no new players will be accepted.
//
//It is NOT a FixUp move; it is a special move that is only every proposed by
//the server itself.
//
//boardgame:codegen
type SeatPlayer struct {
	Default
	TargetPlayerIndex boardgame.PlayerIndex
}

//IsSeatPlayerMove returns true. This is a way for moves to signal to other
//libraries that it's a SeatPlayer move, even if it isn't literally this move
//struct but a subclass of it. Implements interfaces.SeatPlayerMover.
func (s *SeatPlayer) IsSeatPlayerMove() bool {
	return true
}

//DefaultsForState sets TargetPlayerIndex to the next player who is neither
//filled nor closed.
func (s *SeatPlayer) DefaultsForState(state boardgame.ImmutableState) {
	var index int
	for index = 0; index < len(state.ImmutablePlayerStates()); index++ {
		player := state.ImmutablePlayerStates()[index]
		if seat, ok := player.(interfaces.Seater); ok {
			if seat.SeatIsClosed() || seat.SeatIsFilled() {
				continue
			}
			s.TargetPlayerIndex = boardgame.PlayerIndex(index)
			return
		}
	}
}

//Legal verifies that TargetPlayerIndex is set to a player who is both not
//filled and not closed, and that the proposer is the admin, since only server
//should propose this move.
func (s *SeatPlayer) Legal(state boardgame.ImmutableState, proposer boardgame.PlayerIndex) error {
	if err := s.Default.Legal(state, proposer); err != nil {
		return err
	}
	if proposer != boardgame.AdminPlayerIndex {
		return errors.New("This move may only be proposed by an admin")
	}
	if s.TargetPlayerIndex < 0 || int(s.TargetPlayerIndex) >= len(state.ImmutablePlayerStates()) {
		return errors.New("TargetPlayerIndex is invalid")
	}
	seat, ok := state.ImmutablePlayerStates()[s.TargetPlayerIndex].(interfaces.Seater)
	if !ok {
		return errors.New("The selected player did not implement interfaces.Seater")
	}
	if seat.SeatIsClosed() {
		return errors.New("The selected seat was closed")
	}
	if seat.SeatIsFilled() {
		return errors.New("The selected seat was already filled")
	}
	return nil
}

//Apply sets the targeted player to be Filled. If the player state also
//implements interfaces.Inactiver (for example because it implements
//behaviors.PlayerInactive), then it will also set the player to inactive. This
//is often the behavior you want; if you're in the middle of a round you
//typically don't want a new player to be active in the middle of it. But if you
//do use behaviors.PlayerInactive, remember to implement ActivateInactivePlayer
//at the beginning of rounds to activate any new seated players.
func (s *SeatPlayer) Apply(state boardgame.State) error {
	player := state.ImmutablePlayerStates()[s.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	seat.SetSeatFilled()
	if inactiver, ok := player.(interfaces.PlayerInactiver); ok {
		inactiver.SetPlayerInactive()
	}
	return nil
}

//ValidConfiguration checks that player states implement interfaces.Seater
func (s *SeatPlayer) ValidConfiguration(exampleState boardgame.State) error {
	player := exampleState.ImmutablePlayerStates()[0]
	_, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater. behaviors.Seat implements it for free")
	}
	return nil
}

//FallbackHelpText returns "Marks the next available seat as seated, which when
//done will mean the next player is part of the game"
func (s *SeatPlayer) FallbackHelpText() string {
	return "Marks the next available seat as seated, which when done will mean the next player is part of the game"
}

//FallbackName returns "Seat Player"
func (s *SeatPlayer) FallbackName() string {
	return "Seat Player"
}

//CloseEmptySeat is a move that will go through and repeatedly apply itself to
//close any seat that is not filled. Typically you put this at the end of a
//SetUp phase, once all of the players are there who you care to wait for, and
//want to tell the game to not try to seat any more people in them.
//
//boardgame:codegen
type CloseEmptySeat struct {
	FixUpMulti
	TargetPlayerIndex boardgame.PlayerIndex
}

//DefaultsForState sets TargetPlayerIndex to the next player who is currently
//marked as empty, according to interfaces.Seater.
func (c *CloseEmptySeat) DefaultsForState(state boardgame.ImmutableState) {
	var index int
	for index = 0; index < len(state.ImmutablePlayerStates()); index++ {
		player := state.ImmutablePlayerStates()[index]
		if seat, ok := player.(interfaces.Seater); ok {
			if !seat.SeatIsFilled() && !seat.SeatIsClosed() {
				c.TargetPlayerIndex = boardgame.PlayerIndex(index)
				return
			}
		}
	}
}

//Legal verifies that TargetPlayerIndex is set to a player that is currently
//empty and not currently closed.
func (c *CloseEmptySeat) Legal(state boardgame.ImmutableState, proposer boardgame.PlayerIndex) error {
	if err := c.FixUpMulti.Legal(state, proposer); err != nil {
		return err
	}
	if c.TargetPlayerIndex < 0 || int(c.TargetPlayerIndex) >= len(state.ImmutablePlayerStates()) {
		return errors.New("Invalid TargetPlayerIndex")
	}
	player := state.ImmutablePlayerStates()[c.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	if seat.SeatIsFilled() {
		return errors.New("The selected player seat is already filled, not empty. There must not be any seats left to apply to")
	}
	if seat.SeatIsClosed() {
		return errors.New("The selected player seat is already closed. There must not be any seats left to apply to")
	}
	return nil
}

//Apply sets the TargetPlayerIndex to be closed via interfaces.Seater
func (c *CloseEmptySeat) Apply(state boardgame.State) error {
	player := state.ImmutablePlayerStates()[c.TargetPlayerIndex]
	seat, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater")
	}
	seat.SetSeatClosed()
	return nil
}

//ValidConfiguration checks that player states implement interfaces.Seater
func (c *CloseEmptySeat) ValidConfiguration(exampleState boardgame.State) error {
	player := exampleState.ImmutablePlayerStates()[0]
	_, ok := player.(interfaces.Seater)
	if !ok {
		return errors.New("Player state didn't implement interfaces.Seater. behaviors.Seat implements it for free")
	}
	return nil
}

//FallbackHelpText returns "Marks any empty seats as being not open for more people to be seated."
func (c *CloseEmptySeat) FallbackHelpText() string {
	return "Marks any empty seats as being not open for more people to be seated."
}

//FallbackName returns "Close Empty Seat"
func (c *CloseEmptySeat) FallbackName() string {
	return "Close Empty Seat"
}
<|endoftext|>


<commit_before>// Copyright 2014-2017 Amazon.com, Inc. or its affiliates. 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. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 acsclient

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/wsclient"
	"github.com/aws/amazon-ecs-agent/agent/wsclient/mock"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/golang/mock/gomock"
	"github.com/gorilla/websocket"
	"github.com/stretchr/testify/assert"
)

const (
	sampleCredentialsMessage = `
{
  "type": "IAMRoleCredentialsMessage",
  "message": {
    "messageId": "123",
    "clusterArn": "default",
    "taskArn": "t1",
    "roleCredentials": {
      "credentialsId": "credsId",
      "accessKeyId": "accessKeyId",
      "expiration": "2016-03-25T06:17:19.318+0000",
      "roleArn": "roleArn",
      "secretAccessKey": "secretAccessKey",
      "sessionToken": "token"
    }
  }
}
`
	sampleAttachENIMessage = `
{
  "type": "AttachTaskNetworkInterfacesMessage",
  "message": {
    "messageId": "123",
    "clusterArn": "default",
    "taskArn": "task",
    "elasticNetworkInterfaces":[{
      "attachmentArn": "attach_arn",
      "ec2Id": "eni_id",
      "ipv4Addresses":[{
        "primary": true,
        "privateAddress": "ipv4"
      }],
      "ipv6Addresses":[{
        "address": "ipv6"
      }],
      "macAddress": "mac"
    }]
  }
}
`
)

const (
	TestClusterArn  = "arn:aws:ec2:123:container/cluster:123456"
	TestInstanceArn = "arn:aws:ec2:123:container/containerInstance/12345678"
)

var testCfg = &config.Config{
	AcceptInsecureCert: true,
	AWSRegion:          "us-east-1",
}

func TestMakeUnrecognizedRequest(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().Close()

	cs := testCS(conn)
	defer cs.Close()
	// 'testing.T' should not be a known type ;)
	err := cs.MakeRequest(t)
	if _, ok := err.(*wsclient.UnrecognizedWSRequestType); !ok {
		t.Fatal("Expected unrecognized request type")
	}
}

func TestWriteAckRequest(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	// capture bytes written
	var writes []byte
	conn.EXPECT().WriteMessage(gomock.Any(), gomock.Any()).Do(func(_ int, data []byte) {
		writes = data
	})

	// send request
	err := cs.MakeRequest(&ecsacs.AckRequest{})
	assert.NoError(t, err)

	// unmarshal bytes written to the socket
	msg := &wsclient.RequestMessage{}
	err = json.Unmarshal(writes, msg)
	assert.NoError(t, err)
	assert.Equal(t, "AckRequest", msg.Type)
}

func TestPayloadHandlerCalled(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().AnyTimes().Return(websocket.TextMessage, []byte(`{"type":"PayloadMessage","message":{"tasks":[{"arn":"arn"}]}}`), nil)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	messageChannel := make(chan *ecsacs.PayloadMessage)
	reqHandler := func(payload *ecsacs.PayloadMessage) {
		messageChannel <- payload
	}
	cs.AddRequestHandler(reqHandler)
	go cs.Serve()

	expectedMessage := &ecsacs.PayloadMessage{
		Tasks: []*ecsacs.Task{{
			Arn: aws.String("arn"),
		}},
	}

	assert.Equal(t, expectedMessage, <-messageChannel)
}

func TestRefreshCredentialsHandlerCalled(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().AnyTimes().Return(websocket.TextMessage, []byte(sampleCredentialsMessage), nil)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	messageChannel := make(chan *ecsacs.IAMRoleCredentialsMessage)
	reqHandler := func(message *ecsacs.IAMRoleCredentialsMessage) {
		messageChannel <- message
	}
	cs.AddRequestHandler(reqHandler)

	go cs.Serve()

	expectedMessage := &ecsacs.IAMRoleCredentialsMessage{
		MessageId: aws.String("123"),
		TaskArn:   aws.String("t1"),
		RoleCredentials: &ecsacs.IAMRoleCredentials{
			CredentialsId:   aws.String("credsId"),
			AccessKeyId:     aws.String("accessKeyId"),
			Expiration:      aws.String("2016-03-25T06:17:19.318+0000"),
			RoleArn:         aws.String("roleArn"),
			SecretAccessKey: aws.String("secretAccessKey"),
			SessionToken:    aws.String("token"),
		},
	}
	assert.Equal(t, <-messageChannel, expectedMessage)
}

func TestClosingConnection(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Returning EOF tells the ClientServer that the connection is closed
	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().Return(0, nil, io.EOF)
	conn.EXPECT().WriteMessage(gomock.Any(), gomock.Any()).Return(io.EOF)
	cs := testCS(conn)

	serveErr := cs.Serve()
	assert.Error(t, serveErr)

	err := cs.MakeRequest(&ecsacs.AckRequest{})
	assert.Error(t, err)
}

func TestConnect(t *testing.T) {
	closeWS := make(chan bool)
	server, serverChan, requestChan, serverErr, err := startMockAcsServer(t, closeWS)
	defer server.Close()
	if err != nil {
		t.Fatal(err)
	}
	go func() {
		t.Fatal(<-serverErr)
	}()

	cs := New(server.URL, testCfg, credentials.AnonymousCredentials)
	// Wait for up to a second for the mock server to launch
	for i := 0; i < 100; i++ {
		err = cs.Connect()
		if err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if err != nil {
		t.Fatal(err)
	}

	errs := make(chan error)
	cs.AddRequestHandler(func(msg *ecsacs.PayloadMessage) {
		if *msg.MessageId != "messageId" || len(msg.Tasks) != 1 || *msg.Tasks[0].Arn != "arn1" {
			errs <- errors.New("incorrect payloadMessage arguments")
		} else {
			errs <- nil
		}
	})

	go func() {
		_ = cs.Serve()
	}()

	go func() {
		serverChan <- `{"type":"PayloadMessage","message":{"tasks":[{"arn":"arn1","desiredStatus":"RUNNING","overrides":"{}","family":"test","version":"v1","containers":[{"name":"c1","image":"redis","command":["arg1","arg2"],"cpu":10,"memory":20,"links":["db"],"portMappings":[{"containerPort":22,"hostPort":22}],"essential":true,"entryPoint":["bash"],"environment":{"key":"val"},"overrides":"{}","desiredStatus":"RUNNING"}]}],"messageId":"messageId"}}` + "\n"
	}()
	// Error for handling a 'PayloadMessage' request
	err = <-errs
	if err != nil {
		t.Fatal(err)
	}

	mid := "messageId"
	cluster := TestClusterArn
	ci := TestInstanceArn
	go func() {
		cs.MakeRequest(&ecsacs.AckRequest{
			MessageId:         &mid,
			Cluster:           &cluster,
			ContainerInstance: &ci,
		})
	}()

	request := <-requestChan

	// A request should have a 'type' and a 'message'
	intermediate := struct {
		Type    string             `json:"type"`
		Message *ecsacs.AckRequest `json:"message"`
	}{}
	err = json.Unmarshal([]byte(request), &intermediate)
	if err != nil {
		t.Fatal(err)
	}
	if intermediate.Type != "AckRequest" || *intermediate.Message.MessageId != mid || *intermediate.Message.ContainerInstance != ci || *intermediate.Message.Cluster != cluster {
		t.Fatal("Unexpected request")
	}
	closeWS <- true
	close(serverChan)
}

func TestConnectClientError(t *testing.T) {
	testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(400)
		w.Write([]byte(`{"InvalidClusterException":"Invalid cluster"}` + "\n"))
	}))
	defer testServer.Close()

	cs := New(testServer.URL, testCfg, credentials.AnonymousCredentials)
	err := cs.Connect()
	_, ok := err.(*wsclient.WSError)
	assert.True(t, ok)
	assert.EqualError(t, err, "InvalidClusterException: Invalid cluster")
}

func testCS(conn *mock_wsclient.MockWebsocketConn) wsclient.ClientServer {
	testCreds := credentials.AnonymousCredentials
	cs := New("localhost:443", testCfg, testCreds).(*clientServer)
	cs.SetConnection(conn)
	return cs
}

// TODO: replace with gomock
func startMockAcsServer(t *testing.T, closeWS <-chan bool) (*httptest.Server, chan<- string, <-chan string, <-chan error, error) {
	serverChan := make(chan string)
	requestsChan := make(chan string)
	errChan := make(chan error)

	upgrader := websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ws, err := upgrader.Upgrade(w, r, nil)
		go func() {
			<-closeWS
			ws.Close()
		}()
		if err != nil {
			errChan <- err
		}
		go func() {
			_, msg, err := ws.ReadMessage()
			if err != nil {
				errChan <- err
			} else {
				requestsChan <- string(msg)
			}
		}()
		for str := range serverChan {
			err := ws.WriteMessage(websocket.TextMessage, []byte(str))
			if err != nil {
				errChan <- err
			}
		}
	})

	server := httptest.NewTLSServer(handler)
	return server, serverChan, requestsChan, errChan, nil
}

func TestAttachENIHandlerCalled(t *testing.T) {
	cs, ml := testCS()

	handlerCalled := make(chan struct{})
	var handledMessage *ecsacs.AttachTaskNetworkInterfacesMessage
	reqHandler := func(message *ecsacs.AttachTaskNetworkInterfacesMessage) {
		handledMessage = message
		handlerCalled <- struct{}{}
	}

	cs.AddRequestHandler(reqHandler)

	ml.reads = [][]byte{[]byte(sampleAttachENIMessage)}
	var isClosed bool
	go func() {
		err := cs.Serve()
		if !isClosed {
			assert.NoError(t, err)
		}
	}()

	<-handlerCalled

	expectedMessage := &ecsacs.AttachTaskNetworkInterfacesMessage{
		MessageId:  aws.String("123"),
		ClusterArn: aws.String("default"),
		TaskArn:    aws.String("task"),
		ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{
			{AttachmentArn: aws.String("attach_arn"),
				Ec2Id: aws.String("eni_id"),
				Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{
					{
						Primary:        aws.Bool(true),
						PrivateAddress: aws.String("ipv4"),
					},
				},
				Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{
					{
						Address: aws.String("ipv6"),
					},
				},
				MacAddress: aws.String("mac"),
			},
		},
	}

	assert.Equal(t, expectedMessage, handledMessage)

	isClosed = true
	cs.Close()
}
<commit_msg>agent/acs/client: use mock webserver instead of message logger in tests<commit_after>// Copyright 2014-2017 Amazon.com, Inc. or its affiliates. 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. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 acsclient

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/wsclient"
	"github.com/aws/amazon-ecs-agent/agent/wsclient/mock"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/golang/mock/gomock"
	"github.com/gorilla/websocket"
	"github.com/stretchr/testify/assert"
)

const (
	sampleCredentialsMessage = `
{
  "type": "IAMRoleCredentialsMessage",
  "message": {
    "messageId": "123",
    "clusterArn": "default",
    "taskArn": "t1",
    "roleCredentials": {
      "credentialsId": "credsId",
      "accessKeyId": "accessKeyId",
      "expiration": "2016-03-25T06:17:19.318+0000",
      "roleArn": "roleArn",
      "secretAccessKey": "secretAccessKey",
      "sessionToken": "token"
    }
  }
}
`
	sampleAttachENIMessage = `
{
  "type": "AttachTaskNetworkInterfacesMessage",
  "message": {
    "messageId": "123",
    "clusterArn": "default",
    "taskArn": "task",
    "elasticNetworkInterfaces":[{
      "attachmentArn": "attach_arn",
      "ec2Id": "eni_id",
      "ipv4Addresses":[{
        "primary": true,
        "privateAddress": "ipv4"
      }],
      "ipv6Addresses":[{
        "address": "ipv6"
      }],
      "macAddress": "mac"
    }]
  }
}
`
)

const (
	TestClusterArn  = "arn:aws:ec2:123:container/cluster:123456"
	TestInstanceArn = "arn:aws:ec2:123:container/containerInstance/12345678"
)

var testCfg = &config.Config{
	AcceptInsecureCert: true,
	AWSRegion:          "us-east-1",
}

func TestMakeUnrecognizedRequest(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().Close()

	cs := testCS(conn)
	defer cs.Close()
	// 'testing.T' should not be a known type ;)
	err := cs.MakeRequest(t)
	if _, ok := err.(*wsclient.UnrecognizedWSRequestType); !ok {
		t.Fatal("Expected unrecognized request type")
	}
}

func TestWriteAckRequest(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	// capture bytes written
	var writes []byte
	conn.EXPECT().WriteMessage(gomock.Any(), gomock.Any()).Do(func(_ int, data []byte) {
		writes = data
	})

	// send request
	err := cs.MakeRequest(&ecsacs.AckRequest{})
	assert.NoError(t, err)

	// unmarshal bytes written to the socket
	msg := &wsclient.RequestMessage{}
	err = json.Unmarshal(writes, msg)
	assert.NoError(t, err)
	assert.Equal(t, "AckRequest", msg.Type)
}

func TestPayloadHandlerCalled(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().AnyTimes().Return(websocket.TextMessage, []byte(`{"type":"PayloadMessage","message":{"tasks":[{"arn":"arn"}]}}`), nil)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	messageChannel := make(chan *ecsacs.PayloadMessage)
	reqHandler := func(payload *ecsacs.PayloadMessage) {
		messageChannel <- payload
	}
	cs.AddRequestHandler(reqHandler)
	go cs.Serve()

	expectedMessage := &ecsacs.PayloadMessage{
		Tasks: []*ecsacs.Task{{
			Arn: aws.String("arn"),
		}},
	}

	assert.Equal(t, expectedMessage, <-messageChannel)
}

func TestRefreshCredentialsHandlerCalled(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().AnyTimes().Return(websocket.TextMessage, []byte(sampleCredentialsMessage), nil)
	conn.EXPECT().Close()
	cs := testCS(conn)
	defer cs.Close()

	messageChannel := make(chan *ecsacs.IAMRoleCredentialsMessage)
	reqHandler := func(message *ecsacs.IAMRoleCredentialsMessage) {
		messageChannel <- message
	}
	cs.AddRequestHandler(reqHandler)

	go cs.Serve()

	expectedMessage := &ecsacs.IAMRoleCredentialsMessage{
		MessageId: aws.String("123"),
		TaskArn:   aws.String("t1"),
		RoleCredentials: &ecsacs.IAMRoleCredentials{
			CredentialsId:   aws.String("credsId"),
			AccessKeyId:     aws.String("accessKeyId"),
			Expiration:      aws.String("2016-03-25T06:17:19.318+0000"),
			RoleArn:         aws.String("roleArn"),
			SecretAccessKey: aws.String("secretAccessKey"),
			SessionToken:    aws.String("token"),
		},
	}
	assert.Equal(t, <-messageChannel, expectedMessage)
}

func TestClosingConnection(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Returning EOF tells the ClientServer that the connection is closed
	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	conn.EXPECT().ReadMessage().Return(0, nil, io.EOF)
	conn.EXPECT().WriteMessage(gomock.Any(), gomock.Any()).Return(io.EOF)
	cs := testCS(conn)

	serveErr := cs.Serve()
	assert.Error(t, serveErr)

	err := cs.MakeRequest(&ecsacs.AckRequest{})
	assert.Error(t, err)
}

func TestConnect(t *testing.T) {
	closeWS := make(chan bool)
	server, serverChan, requestChan, serverErr, err := startMockAcsServer(t, closeWS)
	defer server.Close()
	if err != nil {
		t.Fatal(err)
	}
	go func() {
		t.Fatal(<-serverErr)
	}()

	cs := New(server.URL, testCfg, credentials.AnonymousCredentials)
	// Wait for up to a second for the mock server to launch
	for i := 0; i < 100; i++ {
		err = cs.Connect()
		if err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if err != nil {
		t.Fatal(err)
	}

	errs := make(chan error)
	cs.AddRequestHandler(func(msg *ecsacs.PayloadMessage) {
		if *msg.MessageId != "messageId" || len(msg.Tasks) != 1 || *msg.Tasks[0].Arn != "arn1" {
			errs <- errors.New("incorrect payloadMessage arguments")
		} else {
			errs <- nil
		}
	})

	go func() {
		_ = cs.Serve()
	}()

	go func() {
		serverChan <- `{"type":"PayloadMessage","message":{"tasks":[{"arn":"arn1","desiredStatus":"RUNNING","overrides":"{}","family":"test","version":"v1","containers":[{"name":"c1","image":"redis","command":["arg1","arg2"],"cpu":10,"memory":20,"links":["db"],"portMappings":[{"containerPort":22,"hostPort":22}],"essential":true,"entryPoint":["bash"],"environment":{"key":"val"},"overrides":"{}","desiredStatus":"RUNNING"}]}],"messageId":"messageId"}}` + "\n"
	}()
	// Error for handling a 'PayloadMessage' request
	err = <-errs
	if err != nil {
		t.Fatal(err)
	}

	mid := "messageId"
	cluster := TestClusterArn
	ci := TestInstanceArn
	go func() {
		cs.MakeRequest(&ecsacs.AckRequest{
			MessageId:         &mid,
			Cluster:           &cluster,
			ContainerInstance: &ci,
		})
	}()

	request := <-requestChan

	// A request should have a 'type' and a 'message'
	intermediate := struct {
		Type    string             `json:"type"`
		Message *ecsacs.AckRequest `json:"message"`
	}{}
	err = json.Unmarshal([]byte(request), &intermediate)
	if err != nil {
		t.Fatal(err)
	}
	if intermediate.Type != "AckRequest" || *intermediate.Message.MessageId != mid || *intermediate.Message.ContainerInstance != ci || *intermediate.Message.Cluster != cluster {
		t.Fatal("Unexpected request")
	}
	closeWS <- true
	close(serverChan)
}

func TestConnectClientError(t *testing.T) {
	testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(400)
		w.Write([]byte(`{"InvalidClusterException":"Invalid cluster"}` + "\n"))
	}))
	defer testServer.Close()

	cs := New(testServer.URL, testCfg, credentials.AnonymousCredentials)
	err := cs.Connect()
	_, ok := err.(*wsclient.WSError)
	assert.True(t, ok)
	assert.EqualError(t, err, "InvalidClusterException: Invalid cluster")
}

func testCS(conn *mock_wsclient.MockWebsocketConn) wsclient.ClientServer {
	testCreds := credentials.AnonymousCredentials
	cs := New("localhost:443", testCfg, testCreds).(*clientServer)
	cs.SetConnection(conn)
	return cs
}

// TODO: replace with gomock
func startMockAcsServer(t *testing.T, closeWS <-chan bool) (*httptest.Server, chan<- string, <-chan string, <-chan error, error) {
	serverChan := make(chan string)
	requestsChan := make(chan string)
	errChan := make(chan error)

	upgrader := websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ws, err := upgrader.Upgrade(w, r, nil)
		go func() {
			<-closeWS
			ws.Close()
		}()
		if err != nil {
			errChan <- err
		}
		go func() {
			_, msg, err := ws.ReadMessage()
			if err != nil {
				errChan <- err
			} else {
				requestsChan <- string(msg)
			}
		}()
		for str := range serverChan {
			err := ws.WriteMessage(websocket.TextMessage, []byte(str))
			if err != nil {
				errChan <- err
			}
		}
	})

	server := httptest.NewTLSServer(handler)
	return server, serverChan, requestsChan, errChan, nil
}

func TestAttachENIHandlerCalled(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	conn := mock_wsclient.NewMockWebsocketConn(ctrl)
	cs := testCS(conn)
	defer cs.Close()

	conn.EXPECT().ReadMessage().AnyTimes().Return(websocket.TextMessage, []byte(sampleAttachENIMessage), nil)
	conn.EXPECT().Close()

	messageChannel := make(chan *ecsacs.AttachTaskNetworkInterfacesMessage)
	reqHandler := func(message *ecsacs.AttachTaskNetworkInterfacesMessage) {
		messageChannel <- message
	}

	cs.AddRequestHandler(reqHandler)

	go cs.Serve()

	expectedMessage := &ecsacs.AttachTaskNetworkInterfacesMessage{
		MessageId:  aws.String("123"),
		ClusterArn: aws.String("default"),
		TaskArn:    aws.String("task"),
		ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{
			{AttachmentArn: aws.String("attach_arn"),
				Ec2Id: aws.String("eni_id"),
				Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{
					{
						Primary:        aws.Bool(true),
						PrivateAddress: aws.String("ipv4"),
					},
				},
				Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{
					{
						Address: aws.String("ipv6"),
					},
				},
				MacAddress: aws.String("mac"),
			},
		},
	}

	assert.Equal(t, <-messageChannel, expectedMessage)
}
<|endoftext|>