<commit_before>// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package main

import (
	"fmt"
	"os"

	"github.com/juju/cmd"
	"github.com/juju/errors"
	"github.com/juju/utils/featureflag"
	"gopkg.in/juju/charm.v5"
	"launchpad.net/gnuflag"

	"github.com/juju/juju/cmd/envcmd"
	"github.com/juju/juju/constraints"
	"github.com/juju/juju/environs"
	"github.com/juju/juju/environs/bootstrap"
	"github.com/juju/juju/environs/configstore"
	"github.com/juju/juju/feature"
	"github.com/juju/juju/instance"
	"github.com/juju/juju/juju"
	"github.com/juju/juju/juju/osenv"
	"github.com/juju/juju/network"
	"github.com/juju/juju/provider"
)

// provisionalProviders is the names of providers that are hidden behind
// feature flags.
var provisionalProviders = map[string]string{
	"cloudsigma": feature.CloudSigma,
	"vsphere":    feature.VSphereProvider,
}

const bootstrapDoc = `
bootstrap starts a new environment of the current type (it will return an error
if the environment has already been bootstrapped).  Bootstrapping an environment
will provision a new machine in the environment and run the juju state server on
that machine.

If constraints are specified in the bootstrap command, they will apply to the
machine provisioned for the juju state server.  They will also be set as default
constraints on the environment for all future machines, exactly as if the
constraints were set with juju set-constraints.

It is possible to override constraints and the automatic machine selection
algorithm by using the "--to" flag. The value associated with "--to" is a
"placement directive", which tells Juju how to identify the first machine to use.
For more information on placement directives, see "juju help placement".

Bootstrap initializes the cloud environment synchronously and displays information
about the current installation steps.  The time for bootstrap to complete varies
across cloud providers from a few seconds to several minutes.  Once bootstrap has
completed, you can run other juju commands against your environment. You can change
the default timeout and retry delays used during the bootstrap by changing the
following settings in your environments.yaml (all values represent number of seconds):

    # How long to wait for a connection to the state server.
    bootstrap-timeout: 600 # default: 10 minutes
    # How long to wait between connection attempts to a state server address.
    bootstrap-retry-delay: 5 # default: 5 seconds
    # How often to refresh state server addresses from the API server.
    bootstrap-addresses-delay: 10 # default: 10 seconds

Private clouds may need to specify their own custom image metadata, and possibly upload
Juju tools to cloud storage if no outgoing Internet access is available. In this case,
use the --metadata-source paramater to tell bootstrap a local directory from which to
upload tools and/or image metadata.

See Also:
   juju help switch
   juju help constraints
   juju help set-constraints
   juju help placement
`

// BootstrapCommand is responsible for launching the first machine in a juju
// environment, and setting up everything necessary to continue working.
type BootstrapCommand struct {
	envcmd.EnvCommandBase
	Constraints           constraints.Value
	UploadTools           bool
	Series                []string
	seriesOld             []string
	MetadataSource        string
	Placement             string
	KeepBrokenEnvironment bool
}

func (c *BootstrapCommand) Info() *cmd.Info {
	return &cmd.Info{
		Name:    "bootstrap",
		Purpose: "start up an environment from scratch",
		Doc:     bootstrapDoc,
	}
}

func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints")
	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
	f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)")
	f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)")
	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
	f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap")
	f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails")
}

func (c *BootstrapCommand) Init(args []string) (err error) {
	if len(c.Series) > 0 && !c.UploadTools {
		return fmt.Errorf("--upload-series requires --upload-tools")
	}
	if len(c.seriesOld) > 0 && !c.UploadTools {
		return fmt.Errorf("--series requires --upload-tools")
	}
	if len(c.Series) > 0 && len(c.seriesOld) > 0 {
		return fmt.Errorf("--upload-series and --series can't be used together")
	}

	// Parse the placement directive. Bootstrap currently only
	// supports provider-specific placement directives.
	if c.Placement != "" {
		_, err = instance.ParsePlacement(c.Placement)
		if err != instance.ErrPlacementScopeMissing {
			// We only support unscoped placement directives for bootstrap.
			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
		}
	}
	return cmd.CheckEmpty(args)
}

type seriesValue struct {
	*cmd.StringsValue
}

// newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function.
func newSeriesValue(defaultValue []string, target *[]string) *seriesValue {
	v := seriesValue{(*cmd.StringsValue)(target)}
	*(v.StringsValue) = defaultValue
	return &v
}

// Implements gnuflag.Value Set.
func (v *seriesValue) Set(s string) error {
	if err := v.StringsValue.Set(s); err != nil {
		return err
	}
	for _, name := range *(v.StringsValue) {
		if !charm.IsValidSeries(name) {
			v.StringsValue = nil
			return fmt.Errorf("invalid series name %q", name)
		}
	}
	return nil
}

// bootstrap functionality that Run calls to support cleaner testing
type BootstrapInterface interface {
	EnsureNotBootstrapped(env environs.Environ) error
	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
}

type bootstrapFuncs struct{}

func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
	return bootstrap.EnsureNotBootstrapped(env)
}

func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
	return bootstrap.Bootstrap(ctx, env, args)
}

var getBootstrapFuncs = func() BootstrapInterface {
	return &bootstrapFuncs{}
}

var getEnvName = func(c *BootstrapCommand) string {
	return c.ConnectionName()
}

// Run connects to the environment specified on the command line and bootstraps
// a juju in that environment if none already exists. If there is as yet no environments.yaml file,
// the user is informed how to create one.
func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
	bootstrapFuncs := getBootstrapFuncs()

	if len(c.seriesOld) > 0 {
		fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
	}
	if len(c.Series) > 0 {
		fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
	}

	envName := getEnvName(c)
	if envName == "" {
		return errors.Errorf("the name of the environment must be specified")
	}
	if err := checkProviderType(envName); errors.IsNotFound(err) {
		// This error will get handled later.
	} else if err != nil {
		return errors.Trace(err)
	}

	environ, cleanup, err := environFromName(
		ctx,
		envName,
		"Bootstrap",
		bootstrapFuncs.EnsureNotBootstrapped,
	)

	// If we error out for any reason, clean up the environment.
	defer func() {
		if resultErr != nil && cleanup != nil {
			if c.KeepBrokenEnvironment {
				logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" +
					"When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" +
					"to clean up the environment.")
			} else {
				handleBootstrapError(ctx, resultErr, cleanup)
			}
		}
	}()

	// Handle any errors from environFromName(...).
	if err != nil {
		return errors.Annotatef(err, "there was an issue examining the environment")
	}

	// Check to see if this environment is already bootstrapped. If it
	// is, we inform the user and exit early. If an error is returned
	// but it is not that the environment is already bootstrapped,
	// then we're in an unknown state.
	if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err {
		if environs.ErrAlreadyBootstrapped == err {
			logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.")
			return err
		}
		return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.")
	}

	// Block interruption during bootstrap. Providers may also
	// register for interrupt notification so they can exit early.
	interrupted := make(chan os.Signal, 1)
	defer close(interrupted)
	ctx.InterruptNotify(interrupted)
	defer ctx.StopInterruptNotify(interrupted)
	go func() {
		for _ = range interrupted {
			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
		}
	}()

	// If --metadata-source is specified, override the default tools metadata source so
	// SyncTools can use it, and also upload any image metadata.
	var metadataDir string
	if c.MetadataSource != "" {
		metadataDir = ctx.AbsPath(c.MetadataSource)
	}

	// TODO (wallyworld): 2013-09-20 bug 1227931
	// We can set a custom tools data source instead of doing an
	// unnecessary upload.
	if environ.Config().Type() == provider.Local {
		c.UploadTools = true
	}

	err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
		Constraints: c.Constraints,
		Placement:   c.Placement,
		UploadTools: c.UploadTools,
		MetadataDir: metadataDir,
	})
	if err != nil {
		return errors.Annotate(err, "failed to bootstrap environment")
	}
	return c.SetBootstrapEndpointAddress(environ)
}

var environType = func(envName string) (string, error) {
	store, err := configstore.Default()
	if err != nil {
		return "", errors.Trace(err)
	}
	cfg, _, err := environs.ConfigForName(envName, store)
	if err != nil {
		return "", errors.Trace(err)
	}
	return cfg.Type(), nil
}

// checkProviderType ensures the provider type is okay.
func checkProviderType(envName string) error {
	envType, err := environType(envName)
	if err != nil {
		return errors.Trace(err)
	}

	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
	for provisional, flag := range provisionalProviders {
		if envType != provisional {
			continue
		}
		if !featureflag.Enabled(flag) {
			msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell environment`
			return errors.Errorf(msg, envType, flag)
		}
	}

	return nil
}

// handleBootstrapError is called to clean up if bootstrap fails.
func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) {
	ch := make(chan os.Signal, 1)
	ctx.InterruptNotify(ch)
	defer ctx.StopInterruptNotify(ch)
	defer close(ch)
	go func() {
		for _ = range ch {
			fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
		}
	}()
	cleanup()
}

var allInstances = func(environ environs.Environ) ([]instance.Instance, error) {
	return environ.AllInstances()
}

var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching

// SetBootstrapEndpointAddress writes the API endpoint address of the
// bootstrap server into the connection information. This should only be run
// once directly after Bootstrap. It assumes that there is just one instance
// in the environment - the bootstrap instance.
func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error {
	instances, err := allInstances(environ)
	if err != nil {
		return errors.Trace(err)
	}
	length := len(instances)
	if length == 0 {
		return errors.Errorf("found no instances, expected at least one")
	}
	if length > 1 {
		logger.Warningf("expected one instance, got %d", length)
	}
	bootstrapInstance := instances[0]
	cfg := environ.Config()
	info, err := envcmd.ConnectionInfoForName(c.ConnectionName())
	if err != nil {
		return errors.Annotate(err, "failed to get connection info")
	}

	// Don't use c.ConnectionEndpoint as it attempts to contact the state
	// server if no addresses are found in connection info.
	endpoint := info.APIEndpoint()
	netAddrs, err := bootstrapInstance.Addresses()
	if err != nil {
		return errors.Annotate(err, "failed to get bootstrap instance addresses")
	}
	apiPort := cfg.APIPort()
	apiHostPorts := network.AddressesWithPort(netAddrs, apiPort)
	addrs, hosts, addrsChanged := prepareEndpointsForCaching(
		info, [][]network.HostPort{apiHostPorts}, network.HostPort{},
	)
	if !addrsChanged {
		// Something's wrong we already have cached addresses?
		return errors.Annotate(err, "cached API endpoints unexpectedly exist")
	}
	endpoint.Addresses = addrs
	endpoint.Hostnames = hosts
	writer, err := c.ConnectionWriter()
	if err != nil {
		return errors.Annotate(err, "failed to get connection writer")
	}
	writer.SetAPIEndpoint(endpoint)
	err = writer.Write()
	if err != nil {
		return errors.Annotate(err, "failed to write API endpoint to connection info")
	}
	return nil
}
<commit_msg>Use map lookup rather than a loop.<commit_after>// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package main

import (
	"fmt"
	"os"

	"github.com/juju/cmd"
	"github.com/juju/errors"
	"github.com/juju/utils/featureflag"
	"gopkg.in/juju/charm.v5"
	"launchpad.net/gnuflag"

	"github.com/juju/juju/cmd/envcmd"
	"github.com/juju/juju/constraints"
	"github.com/juju/juju/environs"
	"github.com/juju/juju/environs/bootstrap"
	"github.com/juju/juju/environs/configstore"
	"github.com/juju/juju/feature"
	"github.com/juju/juju/instance"
	"github.com/juju/juju/juju"
	"github.com/juju/juju/juju/osenv"
	"github.com/juju/juju/network"
	"github.com/juju/juju/provider"
)

// provisionalProviders is the names of providers that are hidden behind
// feature flags.
var provisionalProviders = map[string]string{
	"cloudsigma": feature.CloudSigma,
	"vsphere":    feature.VSphereProvider,
}

const bootstrapDoc = `
bootstrap starts a new environment of the current type (it will return an error
if the environment has already been bootstrapped).  Bootstrapping an environment
will provision a new machine in the environment and run the juju state server on
that machine.

If constraints are specified in the bootstrap command, they will apply to the
machine provisioned for the juju state server.  They will also be set as default
constraints on the environment for all future machines, exactly as if the
constraints were set with juju set-constraints.

It is possible to override constraints and the automatic machine selection
algorithm by using the "--to" flag. The value associated with "--to" is a
"placement directive", which tells Juju how to identify the first machine to use.
For more information on placement directives, see "juju help placement".

Bootstrap initializes the cloud environment synchronously and displays information
about the current installation steps.  The time for bootstrap to complete varies
across cloud providers from a few seconds to several minutes.  Once bootstrap has
completed, you can run other juju commands against your environment. You can change
the default timeout and retry delays used during the bootstrap by changing the
following settings in your environments.yaml (all values represent number of seconds):

    # How long to wait for a connection to the state server.
    bootstrap-timeout: 600 # default: 10 minutes
    # How long to wait between connection attempts to a state server address.
    bootstrap-retry-delay: 5 # default: 5 seconds
    # How often to refresh state server addresses from the API server.
    bootstrap-addresses-delay: 10 # default: 10 seconds

Private clouds may need to specify their own custom image metadata, and possibly upload
Juju tools to cloud storage if no outgoing Internet access is available. In this case,
use the --metadata-source paramater to tell bootstrap a local directory from which to
upload tools and/or image metadata.

See Also:
   juju help switch
   juju help constraints
   juju help set-constraints
   juju help placement
`

// BootstrapCommand is responsible for launching the first machine in a juju
// environment, and setting up everything necessary to continue working.
type BootstrapCommand struct {
	envcmd.EnvCommandBase
	Constraints           constraints.Value
	UploadTools           bool
	Series                []string
	seriesOld             []string
	MetadataSource        string
	Placement             string
	KeepBrokenEnvironment bool
}

func (c *BootstrapCommand) Info() *cmd.Info {
	return &cmd.Info{
		Name:    "bootstrap",
		Purpose: "start up an environment from scratch",
		Doc:     bootstrapDoc,
	}
}

func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints")
	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
	f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)")
	f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)")
	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
	f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap")
	f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails")
}

func (c *BootstrapCommand) Init(args []string) (err error) {
	if len(c.Series) > 0 && !c.UploadTools {
		return fmt.Errorf("--upload-series requires --upload-tools")
	}
	if len(c.seriesOld) > 0 && !c.UploadTools {
		return fmt.Errorf("--series requires --upload-tools")
	}
	if len(c.Series) > 0 && len(c.seriesOld) > 0 {
		return fmt.Errorf("--upload-series and --series can't be used together")
	}

	// Parse the placement directive. Bootstrap currently only
	// supports provider-specific placement directives.
	if c.Placement != "" {
		_, err = instance.ParsePlacement(c.Placement)
		if err != instance.ErrPlacementScopeMissing {
			// We only support unscoped placement directives for bootstrap.
			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
		}
	}
	return cmd.CheckEmpty(args)
}

type seriesValue struct {
	*cmd.StringsValue
}

// newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function.
func newSeriesValue(defaultValue []string, target *[]string) *seriesValue {
	v := seriesValue{(*cmd.StringsValue)(target)}
	*(v.StringsValue) = defaultValue
	return &v
}

// Implements gnuflag.Value Set.
func (v *seriesValue) Set(s string) error {
	if err := v.StringsValue.Set(s); err != nil {
		return err
	}
	for _, name := range *(v.StringsValue) {
		if !charm.IsValidSeries(name) {
			v.StringsValue = nil
			return fmt.Errorf("invalid series name %q", name)
		}
	}
	return nil
}

// bootstrap functionality that Run calls to support cleaner testing
type BootstrapInterface interface {
	EnsureNotBootstrapped(env environs.Environ) error
	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
}

type bootstrapFuncs struct{}

func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
	return bootstrap.EnsureNotBootstrapped(env)
}

func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
	return bootstrap.Bootstrap(ctx, env, args)
}

var getBootstrapFuncs = func() BootstrapInterface {
	return &bootstrapFuncs{}
}

var getEnvName = func(c *BootstrapCommand) string {
	return c.ConnectionName()
}

// Run connects to the environment specified on the command line and bootstraps
// a juju in that environment if none already exists. If there is as yet no environments.yaml file,
// the user is informed how to create one.
func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
	bootstrapFuncs := getBootstrapFuncs()

	if len(c.seriesOld) > 0 {
		fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
	}
	if len(c.Series) > 0 {
		fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
	}

	envName := getEnvName(c)
	if envName == "" {
		return errors.Errorf("the name of the environment must be specified")
	}
	if err := checkProviderType(envName); errors.IsNotFound(err) {
		// This error will get handled later.
	} else if err != nil {
		return errors.Trace(err)
	}

	environ, cleanup, err := environFromName(
		ctx,
		envName,
		"Bootstrap",
		bootstrapFuncs.EnsureNotBootstrapped,
	)

	// If we error out for any reason, clean up the environment.
	defer func() {
		if resultErr != nil && cleanup != nil {
			if c.KeepBrokenEnvironment {
				logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" +
					"When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" +
					"to clean up the environment.")
			} else {
				handleBootstrapError(ctx, resultErr, cleanup)
			}
		}
	}()

	// Handle any errors from environFromName(...).
	if err != nil {
		return errors.Annotatef(err, "there was an issue examining the environment")
	}

	// Check to see if this environment is already bootstrapped. If it
	// is, we inform the user and exit early. If an error is returned
	// but it is not that the environment is already bootstrapped,
	// then we're in an unknown state.
	if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err {
		if environs.ErrAlreadyBootstrapped == err {
			logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.")
			return err
		}
		return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.")
	}

	// Block interruption during bootstrap. Providers may also
	// register for interrupt notification so they can exit early.
	interrupted := make(chan os.Signal, 1)
	defer close(interrupted)
	ctx.InterruptNotify(interrupted)
	defer ctx.StopInterruptNotify(interrupted)
	go func() {
		for _ = range interrupted {
			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
		}
	}()

	// If --metadata-source is specified, override the default tools metadata source so
	// SyncTools can use it, and also upload any image metadata.
	var metadataDir string
	if c.MetadataSource != "" {
		metadataDir = ctx.AbsPath(c.MetadataSource)
	}

	// TODO (wallyworld): 2013-09-20 bug 1227931
	// We can set a custom tools data source instead of doing an
	// unnecessary upload.
	if environ.Config().Type() == provider.Local {
		c.UploadTools = true
	}

	err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
		Constraints: c.Constraints,
		Placement:   c.Placement,
		UploadTools: c.UploadTools,
		MetadataDir: metadataDir,
	})
	if err != nil {
		return errors.Annotate(err, "failed to bootstrap environment")
	}
	return c.SetBootstrapEndpointAddress(environ)
}

var environType = func(envName string) (string, error) {
	store, err := configstore.Default()
	if err != nil {
		return "", errors.Trace(err)
	}
	cfg, _, err := environs.ConfigForName(envName, store)
	if err != nil {
		return "", errors.Trace(err)
	}
	return cfg.Type(), nil
}

// checkProviderType ensures the provider type is okay.
func checkProviderType(envName string) error {
	envType, err := environType(envName)
	if err != nil {
		return errors.Trace(err)
	}

	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
	flag, ok := provisionalProviders[envType]
	if ok && !featureflag.Enabled(flag) {
		msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell environment`
		return errors.Errorf(msg, envType, flag)
	}

	return nil
}

// handleBootstrapError is called to clean up if bootstrap fails.
func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) {
	ch := make(chan os.Signal, 1)
	ctx.InterruptNotify(ch)
	defer ctx.StopInterruptNotify(ch)
	defer close(ch)
	go func() {
		for _ = range ch {
			fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
		}
	}()
	cleanup()
}

var allInstances = func(environ environs.Environ) ([]instance.Instance, error) {
	return environ.AllInstances()
}

var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching

// SetBootstrapEndpointAddress writes the API endpoint address of the
// bootstrap server into the connection information. This should only be run
// once directly after Bootstrap. It assumes that there is just one instance
// in the environment - the bootstrap instance.
func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error {
	instances, err := allInstances(environ)
	if err != nil {
		return errors.Trace(err)
	}
	length := len(instances)
	if length == 0 {
		return errors.Errorf("found no instances, expected at least one")
	}
	if length > 1 {
		logger.Warningf("expected one instance, got %d", length)
	}
	bootstrapInstance := instances[0]
	cfg := environ.Config()
	info, err := envcmd.ConnectionInfoForName(c.ConnectionName())
	if err != nil {
		return errors.Annotate(err, "failed to get connection info")
	}

	// Don't use c.ConnectionEndpoint as it attempts to contact the state
	// server if no addresses are found in connection info.
	endpoint := info.APIEndpoint()
	netAddrs, err := bootstrapInstance.Addresses()
	if err != nil {
		return errors.Annotate(err, "failed to get bootstrap instance addresses")
	}
	apiPort := cfg.APIPort()
	apiHostPorts := network.AddressesWithPort(netAddrs, apiPort)
	addrs, hosts, addrsChanged := prepareEndpointsForCaching(
		info, [][]network.HostPort{apiHostPorts}, network.HostPort{},
	)
	if !addrsChanged {
		// Something's wrong we already have cached addresses?
		return errors.Annotate(err, "cached API endpoints unexpectedly exist")
	}
	endpoint.Addresses = addrs
	endpoint.Hostnames = hosts
	writer, err := c.ConnectionWriter()
	if err != nil {
		return errors.Annotate(err, "failed to get connection writer")
	}
	writer.SetAPIEndpoint(endpoint)
	err = writer.Write()
	if err != nil {
		return errors.Annotate(err, "failed to write API endpoint to connection info")
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package main

import (
	"fmt"

	gc "launchpad.net/gocheck"

	"launchpad.net/juju-core/testing/testbase"
)

type RunTestSuite struct {
	testbase.LoggingSuite
}

var _ = gc.Suite(&RunTestSuite{})

//func (s *RunTestSuite) SetUpTest(c *gc.C) {
//}

func (*RunTestSuite) TestWrongArgs(c *gc.C) {
	for i, test := range []struct {
		title    string
		args     []string
		errMatch string
		unit     string
		commands string
	}{{
		title:    "no args",
		errMatch: "missing unit-name",
	}, {
		title:    "one arg",
		args:     []string{"foo"},
		errMatch: "missing commands",
	}, {
		title:    "more than two arg",
		args:     []string{"foo", "bar", "baz"},
		errMatch: `unrecognized args: \["baz"\]`,
	}, {
		title:    "unit and command assignment",
		args:     []string{"unit-name", "command"},
		unit:     "unit-name",
		commands: "command",
	}, {
		title:    "unit id converted to tag",
		args:     []string{"foo/1", "command"},
		unit:     "unit-foo-1",
		commands: "command",
	},
	} {
		c.Log(fmt.Sprintf("\n%d: %s", i, test.title))
		runCommand := &RunCommand{}
		err := runCommand.Init(test.args)
		if test.errMatch == "" {
			c.Assert(err, gc.IsNil)
			c.Assert(runCommand.unit, gc.Equals, test.unit)
			c.Assert(runCommand.commands, gc.Equals, test.commands)
		} else {
			c.Assert(err, gc.ErrorMatches, test.errMatch)
		}
	}
}
<commit_msg>Test inside hook error.<commit_after>// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package main

import (
	"fmt"

	gc "launchpad.net/gocheck"

	"launchpad.net/juju-core/testing/testbase"
)

type RunTestSuite struct {
	testbase.LoggingSuite
}

var _ = gc.Suite(&RunTestSuite{})

//func (s *RunTestSuite) SetUpTest(c *gc.C) {
//}

func (*RunTestSuite) TestWrongArgs(c *gc.C) {
	for i, test := range []struct {
		title    string
		args     []string
		errMatch string
		unit     string
		commands string
	}{{
		title:    "no args",
		errMatch: "missing unit-name",
	}, {
		title:    "one arg",
		args:     []string{"foo"},
		errMatch: "missing commands",
	}, {
		title:    "more than two arg",
		args:     []string{"foo", "bar", "baz"},
		errMatch: `unrecognized args: \["baz"\]`,
	}, {
		title:    "unit and command assignment",
		args:     []string{"unit-name", "command"},
		unit:     "unit-name",
		commands: "command",
	}, {
		title:    "unit id converted to tag",
		args:     []string{"foo/1", "command"},
		unit:     "unit-foo-1",
		commands: "command",
	},
	} {
		c.Log(fmt.Sprintf("\n%d: %s", i, test.title))
		runCommand := &RunCommand{}
		err := runCommand.Init(test.args)
		if test.errMatch == "" {
			c.Assert(err, gc.IsNil)
			c.Assert(runCommand.unit, gc.Equals, test.unit)
			c.Assert(runCommand.commands, gc.Equals, test.commands)
		} else {
			c.Assert(err, gc.ErrorMatches, test.errMatch)
		}
	}
}

func (s *RunTestSuite) TestInsideContext(c *gc.C) {
	s.PatchEnvironment("JUJU_CONTEXT_ID", "fake-id")
	runCommand := &RunCommand{}
	err := runCommand.Init([]string{"foo", "bar"})
	c.Assert(err, gc.ErrorMatches, "juju-run cannot be called from within a hook.*")
}
<|endoftext|>


<commit_before>package main

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"github.com/Symantec/Dominator/lib/x509util"
	"io/ioutil"
	"os"
)

func printUsage() {
	fmt.Fprintln(os.Stderr,
		"Usage: show-cert certfile")
}

func main() {
	if len(os.Args) != 2 {
		printUsage()
		os.Exit(2)
	}
	data, err := ioutil.ReadFile(os.Args[1])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to read certfile: %s\n", err)
		os.Exit(1)
	}
	block, rest := pem.Decode(data)
	if block == nil {
		fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM")
		os.Exit(1)
	}
	if len(rest) > 0 {
		fmt.Fprintf(os.Stderr, "%d extra bytes in certfile\n", len(rest))
		os.Exit(1)
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to parse certificate: %s\n", err)
		os.Exit(1)
	}
	permittedMethods, err := x509util.GetPermittedMethods(cert)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to get methods: %s\n", err)
		os.Exit(1)
	}
	if len(permittedMethods) > 0 {
		fmt.Println("Permitted methods:")
		for method := range permittedMethods {
			fmt.Println(" ", method)
		}
	} else {
		fmt.Println("No methods are permitted")
	}
}
<commit_msg>Change show-cert to show multiple certificates.<commit_after>package main

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"github.com/Symantec/Dominator/lib/x509util"
	"io/ioutil"
	"os"
)

func printUsage() {
	fmt.Fprintln(os.Stderr,
		"Usage: show-cert certfile")
}

func showCert(filename string) {
	fmt.Println("Certificate:", filename+":")
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to read certfile: %s\n", err)
		return
	}
	block, rest := pem.Decode(data)
	if block == nil {
		fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM")
		return
	}
	if len(rest) > 0 {
		fmt.Fprintf(os.Stderr, "%d extra bytes in certfile\n", len(rest))
		return
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to parse certificate: %s\n", err)
		return
	}
	permittedMethods, err := x509util.GetPermittedMethods(cert)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to get methods: %s\n", err)
		return
	}
	if len(permittedMethods) > 0 {
		fmt.Println("  Permitted methods:")
		for method := range permittedMethods {
			fmt.Println("   ", method)
		}
	} else {
		fmt.Println("  No methods are permitted")
	}
}

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(2)
	}
	for _, filename := range os.Args[1:] {
		showCert(filename)
	}
}
<|endoftext|>


<commit_before>package cmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"text/tabwriter"

	"github.com/mobingi/mobingi-cli/pkg/cli"
	"github.com/mobingilabs/mobingi-sdk-go/mobingi/alm"
	"github.com/mobingilabs/mobingi-sdk-go/pkg/cmdline"
	"github.com/mobingilabs/mobingi-sdk-go/pkg/pretty"
	"github.com/spf13/cobra"
)

func StackDescribeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "describe",
		Short: "display stack details",
		Long: `Display stack details. If you specify the '--out=[filename]' option,
make sure you provide the full path of the file. If the path has
space(s) in it, make sure to surround it with double quotes.

Valid format values: min (default), json, raw

Examples:

  $ ` + cmdline.Args0() + ` stack describe --id=58c2297d25645-Y6NSE4VjP-tk
  $ ` + cmdline.Args0() + ` stack describe --id=58c2297d25645-Y6NSE4VjP-tk --fmt=json`,
		Run: describe,
	}

	cmd.Flags().StringP("id", "i", "", "stack id")
	return cmd
}

func describe(cmd *cobra.Command, args []string) {
	sess, err := clisession()
	cli.ErrorExit(err, 1)

	svc := alm.New(sess)
	in := &alm.StackDescribeInput{
		StackId: cli.GetCliStringFlag(cmd, "id"),
	}

	resp, body, err := svc.Describe(in)
	cli.ErrorExit(err, 1)
	exitOn401(resp)

	// we process `--fmt=raw` option first
	out := cli.GetCliStringFlag(cmd, "out")
	pfmt := cli.GetCliStringFlag(cmd, "fmt")
	if sess.Config.ApiVersion == 3 {
		if pfmt == "min" || pfmt == "" {
			pfmt = "json"
		}
	}

	switch pfmt {
	case "raw":
		fmt.Println(string(body))
		if out != "" {
			err = ioutil.WriteFile(out, body, 0644)
			cli.ErrorExit(err, 1)
		}
	case "json":
		indent := cli.GetCliIntFlag(cmd, "indent")
		js := pretty.JSON(string(body), indent)
		fmt.Println(js)

		// write to file option
		if out != "" {
			err = ioutil.WriteFile(out, []byte(js), 0644)
			cli.ErrorExit(err, 1)
		}
	default:
		if pfmt == "min" || pfmt == "" {
			var stacks []alm.DescribeStack
			err = json.Unmarshal(body, &stacks)
			cli.ErrorExit(err, 1)

			w := tabwriter.NewWriter(os.Stdout, 0, 10, 5, ' ', 0)
			fmt.Fprintf(w, "INSTANCE ID\tINSTANCE TYPE\tINSTANCE MODEL\tPUBLIC IP\tPRIVATE IP\tSTATUS\n")
			for _, inst := range stacks[0].Instances {
				instype := "on-demand"
				if inst.InstanceLifecycle == "spot" {
					instype = inst.InstanceLifecycle
				}

				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
					inst.InstanceId,
					instype,
					inst.InstanceType,
					inst.PublicIpAddress,
					inst.PrivateIpAddress,
					inst.State.Name)
			}

			w.Flush()
		}
	}
}
<commit_msg>Fix v3 describe.<commit_after>package cmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"text/tabwriter"

	"github.com/mobingi/mobingi-cli/pkg/cli"
	"github.com/mobingilabs/mobingi-sdk-go/mobingi/alm"
	"github.com/mobingilabs/mobingi-sdk-go/pkg/cmdline"
	"github.com/mobingilabs/mobingi-sdk-go/pkg/pretty"
	"github.com/spf13/cobra"
)

func StackDescribeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "describe",
		Short: "display stack details",
		Long: `Display stack details. If you specify the '--out=[filename]' option,
make sure you provide the full path of the file. If the path has
space(s) in it, make sure to surround it with double quotes.

Valid format values: min (default), json, raw

Examples:

  $ ` + cmdline.Args0() + ` stack describe --id=58c2297d25645-Y6NSE4VjP-tk
  $ ` + cmdline.Args0() + ` stack describe --id=58c2297d25645-Y6NSE4VjP-tk --fmt=json`,
		Run: describe,
	}

	cmd.Flags().String("id", "", "stack id")
	return cmd
}

func describe(cmd *cobra.Command, args []string) {
	sess, err := clisession()
	cli.ErrorExit(err, 1)

	svc := alm.New(sess)
	in := &alm.StackDescribeInput{
		StackId: cli.GetCliStringFlag(cmd, "id"),
	}

	resp, body, err := svc.Describe(in)
	cli.ErrorExit(err, 1)
	exitOn401(resp)

	// we process `--fmt=raw` option first
	out := cli.GetCliStringFlag(cmd, "out")
	pfmt := cli.GetCliStringFlag(cmd, "fmt")
	/*
		if sess.Config.ApiVersion == 3 {
			if pfmt == "min" || pfmt == "" {
				pfmt = "json"
			}
		}
	*/

	switch pfmt {
	case "raw":
		fmt.Println(string(body))
		if out != "" {
			err = ioutil.WriteFile(out, body, 0644)
			cli.ErrorExit(err, 1)
		}
	case "json":
		indent := cli.GetCliIntFlag(cmd, "indent")
		js := pretty.JSON(string(body), indent)
		fmt.Println(js)

		// write to file option
		if out != "" {
			err = ioutil.WriteFile(out, []byte(js), 0644)
			cli.ErrorExit(err, 1)
		}
	default:
		if pfmt == "min" || pfmt == "" {
			/*
				if sess.Config.ApiVersion >= 3 {
					err = v3DescribeStack(cmd, body)
					cli.ErrorExit(err, 1)
					return
				}
			*/

			type Configuration struct {
				// v3
				Description string          `json:"description,omitempty"`
				Label       string          `json:"label,omitempty"`
				Version     string          `json:"version,omitempty"`
				Vendor      json.RawMessage `json:"vendor,omitempty"`
				// v2
				AWS                 string      `json:"AWS,omitempty"`
				AWSAccountName      string      `json:"AWS_ACCOUNT_NAME,omitempty"`
				AssociatePublicIp   string      `json:"AssociatePublicIP,omitempty"`
				ELBOpen443Port      string      `json:"ELBOpen443Port,omitempty"`
				ELBOpen80Port       string      `json:"ELBOpen80Port,omitempty"`
				SpotInstanceMaxSize int         `json:"SpotInstanceMaxSize,omitempty"`
				SpotInstanceMinSize int         `json:"SpotInstanceMinSize,omitempty"`
				SpotPrice           string      `json:"SpotPrice,omitempty"`
				Architecture        string      `json:"architecture,omitempty"`
				Code                string      `json:"code,omitempty"`
				Image               string      `json:"image,omitempty"`
				Max                 interface{} `json:"max,omitempty"`
				MaxOrigin           interface{} `json:"maxOrigin,omitempty"`
				Min                 interface{} `json:"min,omitempty"`
				MinOrigin           interface{} `json:"minOrigin,omitempty"`
				Nickname            string      `json:"nickname,omitempty"`
				Region              string      `json:"region,omitempty"`
				Type                string      `json:"type,omitempty"`
			}

			type State struct {
				Code string `json:"Code,omitempty"`
				Name string `json:"Name,omitempty"`
			}

			type Instance struct {
				AmiLaunchIndex        string      `json:"AmiLaunchIndex,omitempty"`
				Architecture          string      `json:"Architecture,omitempty"`
				BlockDeviceMappings   interface{} `json:"BlockDeviceMappings,omitempty"`
				ClientToken           string      `json:"ClientToken,omitempty"`
				EbsOptimized          bool        `json:"EbsOptimized,omitempty"`
				Hypervisor            string      `json:"Hypervisor,omitempty"`
				ImageId               string      `json:"ImageId,omitempty"`
				InstanceId            string      `json:"InstanceId,omitempty"`
				InstanceType          string      `json:"InstanceType,omitempty"`
				InstanceLifecycle     string      `json:"InstanceLifecycle,omitempty"`
				SpotInstanceRequestId string      `json:"SpotInstanceRequestId,omitempty"`
				KeyName               string      `json:"KeyName,omitempty"`
				LaunchTime            string      `json:"LaunchTime,omitempty"`
				Monitoring            interface{} `json:"Monitoring,omitempty"`
				NetworkInterfaces     interface{} `json:"NetworkInterfaces,omitempty"`
				Placement             interface{} `json:"Placement,omitempty"`
				PrivateDnsName        string      `json:"PrivateDnsName,omitempty"`
				PrivateIpAddress      string      `json:"PrivateIpAddress,omitempty"`
				ProductCodes          []string    `json:"ProductCodes,omitempty"`
				PublicDnsName         string      `json:"PublicDnsName,omitempty"`
				PublicIpAddress       string      `json:"PublicIpAddress,omitempty"`
				Reservation           interface{} `json:"Reservation,omitempty"`
				RootDeviceName        string      `json:"RootDeviceName,omitempty"`
				RootDeviceType        string      `json:"RootDeviceType,omitempty"`
				SecurityGroups        interface{} `json:"SecurityGroups,omitempty"`
				SourceDestCheck       bool        `json:"SourceDestCheck,omitempty"`
				State                 State       `json:"State,omitempty"`
				StateTransitionReason string      `json:"StateTransitionReason,omitempty"`
				SubnetId              string      `json:"SubnetId,omitempty"`
				Tags                  interface{} `json:"Tags,omitempty"`
				VirtualizationType    string      `json:"VirtualizationType,omitempty"`
				VpcId                 string      `json:"VpcId,omitempty"`
				EnaSupport            string      `json:"enaSupport,omitempty"`
			}

			type DescribeStack struct {
				AuthToken     string        `json:"auth_token,omitempty"`
				Configuration Configuration `json:"configuration,omitempty"`
				CreateTime    string        `json:"create_time,omitempty"`
				Instances     []Instance    `json:"Instances,omitempty"`
				Nickname      string        `json:"nickname,omitempty"`
				StackId       string        `json:"stack_id,omitempty"`
				StackOutputs  interface{}   `json:"stack_outputs,omitempty"`
				StackStatus   string        `json:"stack_status,omitempty"`
				UserId        string        `json:"user_id,omitempty"`
			}

			var stacks []alm.DescribeStack
			var stack alm.DescribeStack

			switch sess.Config.ApiVersion {
			case 3:
				err = json.Unmarshal(body, &stack)
				cli.ErrorExit(err, 1)
			default:
				err = json.Unmarshal(body, &stacks)
				cli.ErrorExit(err, 1)
				stack = stacks[0]
			}

			w := tabwriter.NewWriter(os.Stdout, 0, 10, 5, ' ', 0)
			fmt.Fprintf(w, "INSTANCE ID\tINSTANCE TYPE\tINSTANCE MODEL\tPUBLIC IP\tPRIVATE IP\tSTATUS\n")
			for _, inst := range stack.Instances {
				instype := "on-demand"
				if inst.InstanceLifecycle == "spot" {
					instype = inst.InstanceLifecycle
				}

				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
					inst.InstanceId,
					instype,
					inst.InstanceType,
					inst.PublicIpAddress,
					inst.PrivateIpAddress,
					inst.State.Name)
			}

			w.Flush()
		}
	}
}

func v3DescribeStack(cmd *cobra.Command, body []byte) error {
	return nil
}
<|endoftext|>


<commit_before>// Copyright © 2018 Banzai Cloud
//
// 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 main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"emperror.dev/errors"
	vaultapi "github.com/hashicorp/vault/api"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cast"
	logrusadapter "logur.dev/adapter/logrus"

	"github.com/banzaicloud/bank-vaults/internal/injector"
	"github.com/banzaicloud/bank-vaults/pkg/sdk/vault"
)

// The special value for VAULT_ENV which marks that the login token needs to be passed through to the application
// which was acquired during the new Vault client creation
const vaultLogin = "vault:login"

type sanitizedEnviron []string

var (
	sanitizeEnvmap = map[string]bool{
		"VAULT_TOKEN":                  true,
		"VAULT_ADDR":                   true,
		"VAULT_CACERT":                 true,
		"VAULT_CAPATH":                 true,
		"VAULT_CLIENT_CERT":            true,
		"VAULT_CLIENT_KEY":             true,
		"VAULT_CLIENT_TIMEOUT":         true,
		"VAULT_CLUSTER_ADDR":           true,
		"VAULT_MAX_RETRIES":            true,
		"VAULT_REDIRECT_ADDR":          true,
		"VAULT_SKIP_VERIFY":            true,
		"VAULT_TLS_SERVER_NAME":        true,
		"VAULT_CLI_NO_COLOR":           true,
		"VAULT_RATE_LIMIT":             true,
		"VAULT_NAMESPACE":              true,
		"VAULT_MFA":                    true,
		"VAULT_ROLE":                   true,
		"VAULT_PATH":                   true,
		"VAULT_AUTH_METHOD":            true,
		"VAULT_TRANSIT_KEY_ID":         true,
		"VAULT_TRANSIT_PATH":           true,
		"VAULT_IGNORE_MISSING_SECRETS": true,
		"VAULT_ENV_PASSTHROUGH":        true,
		"VAULT_JSON_LOG":               true,
		"VAULT_LOG_LEVEL":              true,
		"VAULT_REVOKE_TOKEN":           true,
		"VAULT_ENV_DAEMON":             true,
		"VAULT_ENV_FROM_PATH":          true,
	}
)

// Appends variable an entry (name=value) into the environ list.
// VAULT_* variables are not populated into this list.
func (environ *sanitizedEnviron) append(name string, value string) {
	if _, ok := sanitizeEnvmap[name]; !ok {
		*environ = append(*environ, fmt.Sprintf("%s=%s", name, value))
	}
}

type daemonSecretRenewer struct {
	client *vault.Client
	sigs   chan os.Signal
	logger logrus.FieldLogger
}

func (r daemonSecretRenewer) Renew(path string, secret *vaultapi.Secret) error {
	renewerInput := vaultapi.RenewerInput{Secret: secret}
	renewer, err := r.client.RawClient().NewRenewer(&renewerInput)
	if err != nil {
		return errors.Wrap(err, "failed to create secret renewer")
	}

	go renewer.Renew()

	go func() {
		for {
			select {
			case renewOutput := <-renewer.RenewCh():
				r.logger.Infof("secret %s renewed for %ds", path, renewOutput.Secret.LeaseDuration)
			case doneError := <-renewer.DoneCh():
				if !secret.Renewable {
					time.Sleep(time.Duration(secret.LeaseDuration) * time.Second)
					r.logger.Infof("secret lease for %s has expired", path)
				}
				r.logger.WithField("error", doneError).Infof("secret renewal for %s has stopped, sending SIGTERM to process", path)

				r.sigs <- syscall.SIGTERM

				timeout := <-time.After(10 * time.Second)
				r.logger.Infoln("killing process due to SIGTERM timeout =", timeout)
				r.sigs <- syscall.SIGKILL

				return
			}
		}
	}()

	return nil
}

func main() {
	enableJSONLog := cast.ToBool(os.Getenv("VAULT_JSON_LOG"))
	lvl, err := logrus.ParseLevel(os.Getenv("VAULT_LOG_LEVEL"))
	if err != nil {
		lvl = logrus.InfoLevel
	}

	var logger *logrus.Entry
	{
		log := logrus.New()
		log.SetLevel(lvl)
		if enableJSONLog {
			log.SetFormatter(&logrus.JSONFormatter{})
		}
		logger = log.WithField("app", "vault-env")
	}

	daemonMode := cast.ToBool(os.Getenv("VAULT_ENV_DAEMON"))

	sigs := make(chan os.Signal, 1)

	var entrypointCmd []string
	if len(os.Args) == 1 {
		logger.Fatalln("no command is given, vault-env can't determine the entrypoint (command), please specify it explicitly or let the webhook query it (see documentation)")
	} else {
		entrypointCmd = os.Args[1:]
	}

	binary, err := exec.LookPath(entrypointCmd[0])
	if err != nil {
		logger.Fatalln("binary not found", entrypointCmd[0])
	}

	// Used both for reading secrets and transit encryption
	ignoreMissingSecrets := cast.ToBool(os.Getenv("VAULT_IGNORE_MISSING_SECRETS"))

	clientOptions := []vault.ClientOption{vault.ClientLogger(logrusadapter.NewFromEntry(logger))}
	// The login procedure takes the token from a file (if using Vault Agent)
	// or requests one for itself (Kubernetes Auth, or GCP, etc...),
	// so if we got a VAULT_TOKEN for the special value with "vault:login"
	originalVaultTokenEnvVar := os.Getenv("VAULT_TOKEN")
	if tokenFile := os.Getenv("VAULT_TOKEN_FILE"); tokenFile != "" {
		// load token from vault-agent .vault-token or injected webhook
		if b, err := ioutil.ReadFile(tokenFile); err == nil {
			originalVaultTokenEnvVar = string(b)
		} else {
			logger.Fatalf("could not read vault token file: %s", tokenFile)
		}
		clientOptions = append(clientOptions, vault.ClientToken(originalVaultTokenEnvVar))
	} else {
		if originalVaultTokenEnvVar == vaultLogin {
			os.Unsetenv("VAULT_TOKEN")
		}
		// use role/path based authentication
		clientOptions = append(clientOptions,
			vault.ClientRole(os.Getenv("VAULT_ROLE")),
			vault.ClientAuthPath(os.Getenv("VAULT_PATH")),
			vault.ClientAuthMethod(os.Getenv("VAULT_AUTH_METHOD")),
		)
	}

	client, err := vault.NewClientWithOptions(clientOptions...)
	if err != nil {
		logger.Fatal("failed to create vault client", err.Error())
	}

	passthroughEnvVars := strings.Split(os.Getenv("VAULT_ENV_PASSTHROUGH"), ",")

	if originalVaultTokenEnvVar == vaultLogin {
		os.Setenv("VAULT_TOKEN", vaultLogin)
		passthroughEnvVars = append(passthroughEnvVars, "VAULT_TOKEN")
	}

	// do not sanitize env vars specified in VAULT_ENV_PASSTHROUGH
	for _, envVar := range passthroughEnvVars {
		if trimmed := strings.TrimSpace(envVar); trimmed != "" {
			delete(sanitizeEnvmap, trimmed)
		}
	}

	// initial and sanitized environs
	environ := make(map[string]string, len(os.Environ()))
	sanitized := make(sanitizedEnviron, 0, len(environ))

	config := injector.Config{
		TransitKeyID:         os.Getenv("VAULT_TRANSIT_KEY_ID"),
		TransitPath:          os.Getenv("VAULT_TRANSIT_PATH"),
		DaemonMode:           daemonMode,
		IgnoreMissingSecrets: ignoreMissingSecrets,
	}

	var secretRenewer injector.SecretRenewer

	if daemonMode {
		secretRenewer = daemonSecretRenewer{client: client, sigs: sigs, logger: logger}
	}

	secretInjector := injector.NewSecretInjector(config, client, secretRenewer, logger)

	for _, env := range os.Environ() {
		split := strings.SplitN(env, "=", 2)
		name := split[0]
		value := split[1]
		environ[name] = value
	}

	inject := func(key, value string) {
		sanitized.append(key, value)
	}

	err = secretInjector.InjectSecretsFromVault(environ, inject)
	if err != nil {
		logger.Fatalln("failed to inject secrets from vault:", err)
	}

	if paths := os.Getenv("VAULT_ENV_FROM_PATH"); paths != "" {
		err = secretInjector.InjectSecretsFromVaultPath(paths, inject)
	}
	if err != nil {
		logger.Fatalln("failed to inject secrets from vault path:", err)
	}

	if cast.ToBool(os.Getenv("VAULT_REVOKE_TOKEN")) {
		// ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self-
		err = client.RawClient().Auth().Token().RevokeSelf(client.RawClient().Token())
		if err != nil {
			// Do not exit on error, token revoking can be denied by policy
			logger.Warn("failed to revoke token")
		}

		client.Close()
	}

	logger.Infoln("spawning process:", entrypointCmd)

	if daemonMode {
		logger.Infoln("in daemon mode...")
		cmd := exec.Command(binary, entrypointCmd[1:]...)
		cmd.Env = append(os.Environ(), sanitized...)
		cmd.Stdin = os.Stdin
		cmd.Stderr = os.Stderr
		cmd.Stdout = os.Stdout

		signal.Notify(sigs)

		err = cmd.Start()
		if err != nil {
			logger.Fatalln("failed to start process", entrypointCmd, err.Error())
		}

		go func() {
			for sig := range sigs {
				// We don't want to signal a non-running process.
				if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
					break
				}

				err := cmd.Process.Signal(sig)
				if err != nil {
					logger.Warnf("failed to signal process with %s: %v", sig, err)
				} else {
					logger.Infof("received signal: %s", sig)
				}
			}
		}()

		err = cmd.Wait()

		close(sigs)

		if _, ok := err.(*exec.ExitError); ok {
			os.Exit(cmd.ProcessState.ExitCode())
		} else if err != nil {
			logger.Fatalln("failed to exec process", entrypointCmd, err.Error())
			os.Exit(-1)
		} else {
			os.Exit(cmd.ProcessState.ExitCode())
		}
	} else {
		err = syscall.Exec(binary, entrypointCmd, sanitized)
		if err != nil {
			logger.Fatalln("failed to exec process", entrypointCmd, err.Error())
		}
	}
}
<commit_msg>vault-env: smarter env sanitization in case of vault:login (#1265)<commit_after>// Copyright © 2018 Banzai Cloud
//
// 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 main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"emperror.dev/errors"
	vaultapi "github.com/hashicorp/vault/api"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cast"
	logrusadapter "logur.dev/adapter/logrus"

	"github.com/banzaicloud/bank-vaults/internal/injector"
	"github.com/banzaicloud/bank-vaults/pkg/sdk/vault"
)

// The special value for VAULT_ENV which marks that the login token needs to be passed through to the application
// which was acquired during the new Vault client creation
const vaultLogin = "vault:login"

type sanitizedEnviron struct {
	env   []string
	login bool
}

type envType struct {
	login bool
}

var (
	sanitizeEnvmap = map[string]envType{
		"VAULT_TOKEN":                  {login: true},
		"VAULT_ADDR":                   {login: true},
		"VAULT_AGENT_ADDR":             {login: true},
		"VAULT_CACERT":                 {login: true},
		"VAULT_CAPATH":                 {login: true},
		"VAULT_CLIENT_CERT":            {login: true},
		"VAULT_CLIENT_KEY":             {login: true},
		"VAULT_CLIENT_TIMEOUT":         {login: true},
		"VAULT_SRV_LOOKUP":             {login: true},
		"VAULT_SKIP_VERIFY":            {login: true},
		"VAULT_NAMESPACE":              {login: true},
		"VAULT_TLS_SERVER_NAME":        {login: true},
		"VAULT_WRAP_TTL":               {login: true},
		"VAULT_MFA":                    {login: true},
		"VAULT_MAX_RETRIES":            {login: true},
		"VAULT_CLUSTER_ADDR":           {login: false},
		"VAULT_REDIRECT_ADDR":          {login: false},
		"VAULT_CLI_NO_COLOR":           {login: false},
		"VAULT_RATE_LIMIT":             {login: false},
		"VAULT_ROLE":                   {login: false},
		"VAULT_PATH":                   {login: false},
		"VAULT_AUTH_METHOD":            {login: false},
		"VAULT_TRANSIT_KEY_ID":         {login: false},
		"VAULT_TRANSIT_PATH":           {login: false},
		"VAULT_IGNORE_MISSING_SECRETS": {login: false},
		"VAULT_ENV_PASSTHROUGH":        {login: false},
		"VAULT_JSON_LOG":               {login: false},
		"VAULT_LOG_LEVEL":              {login: false},
		"VAULT_REVOKE_TOKEN":           {login: false},
		"VAULT_ENV_DAEMON":             {login: false},
		"VAULT_ENV_FROM_PATH":          {login: false},
	}
)

// Appends variable an entry (name=value) into the environ list.
// VAULT_* variables are not populated into this list if this is not a login scenario.
func (e *sanitizedEnviron) append(name string, value string) {
	if envType, ok := sanitizeEnvmap[name]; !ok || (e.login && envType.login) {
		e.env = append(e.env, fmt.Sprintf("%s=%s", name, value))
	}
}

type daemonSecretRenewer struct {
	client *vault.Client
	sigs   chan os.Signal
	logger logrus.FieldLogger
}

func (r daemonSecretRenewer) Renew(path string, secret *vaultapi.Secret) error {
	renewerInput := vaultapi.RenewerInput{Secret: secret}
	renewer, err := r.client.RawClient().NewRenewer(&renewerInput)
	if err != nil {
		return errors.Wrap(err, "failed to create secret renewer")
	}

	go renewer.Renew()

	go func() {
		for {
			select {
			case renewOutput := <-renewer.RenewCh():
				r.logger.Infof("secret %s renewed for %ds", path, renewOutput.Secret.LeaseDuration)
			case doneError := <-renewer.DoneCh():
				if !secret.Renewable {
					time.Sleep(time.Duration(secret.LeaseDuration) * time.Second)
					r.logger.Infof("secret lease for %s has expired", path)
				}
				r.logger.WithField("error", doneError).Infof("secret renewal for %s has stopped, sending SIGTERM to process", path)

				r.sigs <- syscall.SIGTERM

				timeout := <-time.After(10 * time.Second)
				r.logger.Infoln("killing process due to SIGTERM timeout =", timeout)
				r.sigs <- syscall.SIGKILL

				return
			}
		}
	}()

	return nil
}

func main() {
	enableJSONLog := cast.ToBool(os.Getenv("VAULT_JSON_LOG"))
	lvl, err := logrus.ParseLevel(os.Getenv("VAULT_LOG_LEVEL"))
	if err != nil {
		lvl = logrus.InfoLevel
	}

	var logger *logrus.Entry
	{
		log := logrus.New()
		log.SetLevel(lvl)
		if enableJSONLog {
			log.SetFormatter(&logrus.JSONFormatter{})
		}
		logger = log.WithField("app", "vault-env")
	}

	daemonMode := cast.ToBool(os.Getenv("VAULT_ENV_DAEMON"))

	sigs := make(chan os.Signal, 1)

	var entrypointCmd []string
	if len(os.Args) == 1 {
		logger.Fatalln("no command is given, vault-env can't determine the entrypoint (command), please specify it explicitly or let the webhook query it (see documentation)")
	} else {
		entrypointCmd = os.Args[1:]
	}

	binary, err := exec.LookPath(entrypointCmd[0])
	if err != nil {
		logger.Fatalln("binary not found", entrypointCmd[0])
	}

	// Used both for reading secrets and transit encryption
	ignoreMissingSecrets := cast.ToBool(os.Getenv("VAULT_IGNORE_MISSING_SECRETS"))

	clientOptions := []vault.ClientOption{vault.ClientLogger(logrusadapter.NewFromEntry(logger))}
	// The login procedure takes the token from a file (if using Vault Agent)
	// or requests one for itself (Kubernetes Auth, or GCP, etc...),
	// so if we got a VAULT_TOKEN for the special value with "vault:login"
	originalVaultTokenEnvVar := os.Getenv("VAULT_TOKEN")
	isLogin := originalVaultTokenEnvVar == vaultLogin
	if tokenFile := os.Getenv("VAULT_TOKEN_FILE"); tokenFile != "" {
		// load token from vault-agent .vault-token or injected webhook
		if b, err := ioutil.ReadFile(tokenFile); err == nil {
			originalVaultTokenEnvVar = string(b)
		} else {
			logger.Fatalf("could not read vault token file: %s", tokenFile)
		}
		clientOptions = append(clientOptions, vault.ClientToken(originalVaultTokenEnvVar))
	} else {
		if isLogin {
			os.Unsetenv("VAULT_TOKEN")
		}
		// use role/path based authentication
		clientOptions = append(clientOptions,
			vault.ClientRole(os.Getenv("VAULT_ROLE")),
			vault.ClientAuthPath(os.Getenv("VAULT_PATH")),
			vault.ClientAuthMethod(os.Getenv("VAULT_AUTH_METHOD")),
		)
	}

	client, err := vault.NewClientWithOptions(clientOptions...)
	if err != nil {
		logger.Fatal("failed to create vault client", err.Error())
	}

	passthroughEnvVars := strings.Split(os.Getenv("VAULT_ENV_PASSTHROUGH"), ",")

	if isLogin {
		os.Setenv("VAULT_TOKEN", vaultLogin)
		passthroughEnvVars = append(passthroughEnvVars, "VAULT_TOKEN")
	}

	// do not sanitize env vars specified in VAULT_ENV_PASSTHROUGH
	for _, envVar := range passthroughEnvVars {
		if trimmed := strings.TrimSpace(envVar); trimmed != "" {
			delete(sanitizeEnvmap, trimmed)
		}
	}

	// initial and sanitized environs
	environ := make(map[string]string, len(os.Environ()))
	sanitized := sanitizedEnviron{login: isLogin}

	config := injector.Config{
		TransitKeyID:         os.Getenv("VAULT_TRANSIT_KEY_ID"),
		TransitPath:          os.Getenv("VAULT_TRANSIT_PATH"),
		DaemonMode:           daemonMode,
		IgnoreMissingSecrets: ignoreMissingSecrets,
	}

	var secretRenewer injector.SecretRenewer

	if daemonMode {
		secretRenewer = daemonSecretRenewer{client: client, sigs: sigs, logger: logger}
	}

	secretInjector := injector.NewSecretInjector(config, client, secretRenewer, logger)

	for _, env := range os.Environ() {
		split := strings.SplitN(env, "=", 2)
		name := split[0]
		value := split[1]
		environ[name] = value
	}

	inject := func(key, value string) {
		sanitized.append(key, value)
	}

	err = secretInjector.InjectSecretsFromVault(environ, inject)
	if err != nil {
		logger.Fatalln("failed to inject secrets from vault:", err)
	}

	if paths := os.Getenv("VAULT_ENV_FROM_PATH"); paths != "" {
		err = secretInjector.InjectSecretsFromVaultPath(paths, inject)
	}
	if err != nil {
		logger.Fatalln("failed to inject secrets from vault path:", err)
	}

	if cast.ToBool(os.Getenv("VAULT_REVOKE_TOKEN")) {
		// ref: https://www.vaultproject.io/api/auth/token/index.html#revoke-a-token-self-
		err = client.RawClient().Auth().Token().RevokeSelf(client.RawClient().Token())
		if err != nil {
			// Do not exit on error, token revoking can be denied by policy
			logger.Warn("failed to revoke token")
		}

		client.Close()
	}

	logger.Infoln("spawning process:", entrypointCmd)

	if daemonMode {
		logger.Infoln("in daemon mode...")
		cmd := exec.Command(binary, entrypointCmd[1:]...)
		cmd.Env = append(os.Environ(), sanitized.env...)
		cmd.Stdin = os.Stdin
		cmd.Stderr = os.Stderr
		cmd.Stdout = os.Stdout

		signal.Notify(sigs)

		err = cmd.Start()
		if err != nil {
			logger.Fatalln("failed to start process", entrypointCmd, err.Error())
		}

		go func() {
			for sig := range sigs {
				// We don't want to signal a non-running process.
				if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
					break
				}

				err := cmd.Process.Signal(sig)
				if err != nil {
					logger.Warnf("failed to signal process with %s: %v", sig, err)
				} else {
					logger.Infof("received signal: %s", sig)
				}
			}
		}()

		err = cmd.Wait()

		close(sigs)

		if _, ok := err.(*exec.ExitError); ok {
			os.Exit(cmd.ProcessState.ExitCode())
		} else if err != nil {
			logger.Fatalln("failed to exec process", entrypointCmd, err.Error())
			os.Exit(-1)
		} else {
			os.Exit(cmd.ProcessState.ExitCode())
		}
	} else {
		err = syscall.Exec(binary, entrypointCmd, sanitized.env)
		if err != nil {
			logger.Fatalln("failed to exec process", entrypointCmd, err.Error())
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2013 Matthew Baird
// 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 core

import (
	"fmt"
	u "github.com/araddon/gou"
	"testing"
)

func TestUrlGeneration(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGeneration Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoIndex(t *testing.T) {
	_, err := GetIndexUrl("", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err != nil, t, "err should have been returned")
}

func TestUrlGenerationNoType(t *testing.T) {
	_, err := GetIndexUrl("Index", "", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err != nil, t, "err should have been returned")
}

func TestUrlGenerationNoId(t *testing.T) {
	expectedUrl := "/Index/Type?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoId Should get %s, instead got %s", expectedUrl, url))
}

// if Id is blank, op_type should default to create
func TestUrlGenerationNoIdAndOpTypeNotCreate(t *testing.T) {
	expectedUrl := "/Index/Type?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "", "Parent", 1, "notcreate", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoId Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoParent(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoParent Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoVersion(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 0, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoVersion Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoOpType(t *testing.T) {
	expectedUrl := "/Index/Type/Id?parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "", "Routing", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoOpType Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoRouting(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "", "TimeStamp", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoRouting Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTimestamp(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "", 10, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTimestamp Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTTL(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 0, "Percolate", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTTL Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoPercolate(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "", "Timeout")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoPercolate Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTimeout(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "")
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTimeout Should get %s, instead got %s", expectedUrl, url))
}
<commit_msg>Fixed GetIndexUrl Tests<commit_after>// Copyright 2013 Matthew Baird
// 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 core

import (
	"fmt"
	u "github.com/araddon/gou"
	"testing"
)

func TestUrlGeneration(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGeneration Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoIndex(t *testing.T) {
	_, err := GetIndexUrl("", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err != nil, t, "err should have been returned")
}

func TestUrlGenerationNoType(t *testing.T) {
	_, err := GetIndexUrl("Index", "", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err != nil, t, "err should have been returned")
}

func TestUrlGenerationNoId(t *testing.T) {
	expectedUrl := "/Index/Type?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoId Should get %s, instead got %s", expectedUrl, url))
}

// if Id is blank, op_type should default to create
func TestUrlGenerationNoIdAndOpTypeNotCreate(t *testing.T) {
	expectedUrl := "/Index/Type?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "", "Parent", 1, "notcreate", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoId Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoParent(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoParent Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoVersion(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 0, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoVersion Should get %s, instead got %s", expectedUrl, url))
}

func TestUrlGenerationNoOpType(t *testing.T) {
	expectedUrl := "/Index/Type/Id?parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "", "Routing", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoOpType Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoRouting(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "", "TimeStamp", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoRouting Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTimestamp(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "", 10, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTimestamp Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTTL(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 0, "Percolate", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTTL Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoPercolate(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&refresh=true&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "", "Timeout", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoPercolate Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoTimeout(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&refresh=true&routing=Routing&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "", true)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGenerationNoTimeout Should get %s, instead got %s", expectedUrl, url))
}
func TestUrlGenerationNoRefresh(t *testing.T) {
	expectedUrl := "/Index/Type/Id?op_type=create&parent=Parent&percolate=Percolate&routing=Routing&timeout=Timeout&timestamp=TimeStamp&ttl=10&version=1"
	url, err := GetIndexUrl("Index", "Type", "Id", "Parent", 1, "create", "Routing", "TimeStamp", 10, "Percolate", "Timeout", false)
	u.Assert(err == nil, t, "err was not nil")
	u.Assert(url == expectedUrl, t, fmt.Sprintf("TestUrlGeneration Should get %s, instead got %s", expectedUrl, url))
}
<|endoftext|>


<commit_before>package cod

import (
	"github.com/adabei/goldenbot/events"
	"github.com/adabei/goldenbot/events/cod"
	"github.com/adabei/goldenbot/rcon"
	"log"
	"strconv"
	"strings"
)

var players = make([]string, 1)

type Integrated struct {
	requests chan rcon.RCONQuery
	events   chan interface{}
}

func NewIntegrated(requests chan rcon.RCONQuery, ea events.Aggregator) *Integrated {
	i := new(Integrated)
	i.requests = requests
	i.events = ea.Subscribe(i)
	return i
}

func (i *Integrated) Setup() error {
	return nil
}

func (i *Integrated) maxClients() int {
	maxch := make(chan []byte)
	i.requests <- rcon.RCONQuery{Command: "serverinfo", Response: maxch}
	res := <-maxch
	for _, line := range strings.Split(string(res), "\n") {
		if strings.HasPrefix(line, "sv_maxclients") {
			max, err := strconv.Atoi(strings.Fields(line)[1])
			if err != nil {
				return -1
			} else {
				return max
			}
		}
	}
	return -1
}

func (i *Integrated) Start() {
	maxClients := i.maxClients()
	for maxClients == -1 {
		log.Println("integrated: failed to fetch sv_maxclients. Trying again.")
		maxClients = i.maxClients()
	}
	players = make([]string, maxClients)

	statusch := make(chan []byte)
	i.requests <- rcon.RCONQuery{Command: "status", Response: statusch}
	resp := <-statusch
	if resp != nil {
		for _, line := range strings.Split(string(resp), "\n")[4:] {
			if line != "" {
				p := strings.Fields(line)
				num, err := strconv.Atoi(p[0])
				if err != nil {
					log.Println("integrated: could not parse status, num is no integer")
					continue
				}
				players[num] = p[3]
			}
		}
	}

	for {
		ev := <-i.events
		switch ev := ev.(type) {
		case cod.Join:
			// do nothing if the GUID is already matched
			if _, prev := num(ev.GUID); !prev {
				if num := firstFreeNum(players); num != -1 {
					players[num] = ev.GUID
					log.Println("integrated: guid", ev.GUID, "is assigned num", num)
				} else {
					log.Fatal("integrated: more players than originally possible on server")
				}
			}
		case cod.Quit:
			num, _ := Num(ev.GUID)
			players[num] = ""
		}
	}
}

// Returns the first empty index.
// The server should prevent joins if already full.
func firstFreeNum(p []string) int {
	for i, v := range p {
		if v == "" {
			return i
		}
	}

	// turns out it is full
	return -1
}

// Num returns the num for a player.
// If that player is not in our list we try to predict it.
// If the server is full, -1 and false will be returned, as no num will match.
func Num(id string) (int, bool) {
	if num, ok := num(id); ok {
		return num, true
	}

	// guid not in our players list, predict it
	num := firstFreeNum(players)
	if num != -1 {
		log.Println("integrated: predicting num", num, "for guid", id)
		return num, true
	}

	return -1, false
}

// num returns the num and true only if the id could be looked up.
func num(id string) (int, bool) {
	for i, v := range players {
		if v == id {
			return i, true
		}
	}

	return -1, false
}
<commit_msg>fixes imports due to github name change<commit_after>package cod

import (
	"github.com/schwarz/goldenbot/events"
	"github.com/schwarz/goldenbot/events/cod"
	"github.com/schwarz/goldenbot/rcon"
	"log"
	"strconv"
	"strings"
)

var players = make([]string, 1)

type Integrated struct {
	requests chan rcon.RCONQuery
	events   chan interface{}
}

func NewIntegrated(requests chan rcon.RCONQuery, ea events.Aggregator) *Integrated {
	i := new(Integrated)
	i.requests = requests
	i.events = ea.Subscribe(i)
	return i
}

func (i *Integrated) Setup() error {
	return nil
}

func (i *Integrated) maxClients() int {
	maxch := make(chan []byte)
	i.requests <- rcon.RCONQuery{Command: "serverinfo", Response: maxch}
	res := <-maxch
	for _, line := range strings.Split(string(res), "\n") {
		if strings.HasPrefix(line, "sv_maxclients") {
			max, err := strconv.Atoi(strings.Fields(line)[1])
			if err != nil {
				return -1
			} else {
				return max
			}
		}
	}
	return -1
}

func (i *Integrated) Start() {
	maxClients := i.maxClients()
	for maxClients == -1 {
		log.Println("integrated: failed to fetch sv_maxclients. Trying again.")
		maxClients = i.maxClients()
	}
	players = make([]string, maxClients)

	statusch := make(chan []byte)
	i.requests <- rcon.RCONQuery{Command: "status", Response: statusch}
	resp := <-statusch
	if resp != nil {
		for _, line := range strings.Split(string(resp), "\n")[4:] {
			if line != "" {
				p := strings.Fields(line)
				num, err := strconv.Atoi(p[0])
				if err != nil {
					log.Println("integrated: could not parse status, num is no integer")
					continue
				}
				players[num] = p[3]
			}
		}
	}

	for {
		ev := <-i.events
		switch ev := ev.(type) {
		case cod.Join:
			// do nothing if the GUID is already matched
			if _, prev := num(ev.GUID); !prev {
				if num := firstFreeNum(players); num != -1 {
					players[num] = ev.GUID
					log.Println("integrated: guid", ev.GUID, "is assigned num", num)
				} else {
					log.Fatal("integrated: more players than originally possible on server")
				}
			}
		case cod.Quit:
			num, _ := Num(ev.GUID)
			players[num] = ""
		}
	}
}

// Returns the first empty index.
// The server should prevent joins if already full.
func firstFreeNum(p []string) int {
	for i, v := range p {
		if v == "" {
			return i
		}
	}

	// turns out it is full
	return -1
}

// Num returns the num for a player.
// If that player is not in our list we try to predict it.
// If the server is full, -1 and false will be returned, as no num will match.
func Num(id string) (int, bool) {
	if num, ok := num(id); ok {
		return num, true
	}

	// guid not in our players list, predict it
	num := firstFreeNum(players)
	if num != -1 {
		log.Println("integrated: predicting num", num, "for guid", id)
		return num, true
	}

	return -1, false
}

// num returns the num and true only if the id could be looked up.
func num(id string) (int, bool) {
	for i, v := range players {
		if v == id {
			return i, true
		}
	}

	return -1, false
}
<|endoftext|>