<commit_before>package cmd

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/golang/glog"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	restclient "k8s.io/client-go/rest"
	kapi "k8s.io/kubernetes/pkg/api"

	s2iapi "github.com/openshift/source-to-image/pkg/api"
	s2igit "github.com/openshift/source-to-image/pkg/scm/git"

	buildapi "github.com/openshift/origin/pkg/build/apis/build"
	"github.com/openshift/origin/pkg/build/apis/build/validation"
	bld "github.com/openshift/origin/pkg/build/builder"
	"github.com/openshift/origin/pkg/build/builder/cmd/scmauth"
	"github.com/openshift/origin/pkg/build/builder/timing"
	buildutil "github.com/openshift/origin/pkg/build/util"
	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/generate/git"
	"github.com/openshift/origin/pkg/version"
)

type builder interface {
	Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error
}

type builderConfig struct {
	out             io.Writer
	build           *buildapi.Build
	sourceSecretDir string
	dockerClient    *docker.Client
	dockerEndpoint  string
	buildsClient    client.BuildInterface
}

func newBuilderConfigFromEnvironment(out io.Writer, needsDocker bool) (*builderConfig, error) {
	cfg := &builderConfig{}
	var err error

	cfg.out = out

	buildStr := os.Getenv("BUILD")
	cfg.build = &buildapi.Build{}

	obj, groupVersionKind, err := kapi.Codecs.UniversalDecoder().Decode([]byte(buildStr), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("unable to parse build string: %v", err)
	}
	ok := false
	cfg.build, ok = obj.(*buildapi.Build)
	if !ok {
		return nil, fmt.Errorf("build string is not a build: %v", err)
	}
	if glog.V(4) {
		redactedBuild := buildutil.SafeForLoggingBuild(cfg.build)
		if err != nil {
			return nil, fmt.Errorf("unable to strip proxy credentials from build: %v", err)
		}
		bytes, err := runtime.Encode(kapi.Codecs.LegacyCodec(groupVersionKind.GroupVersion()), redactedBuild)
		if err != nil {
			return nil, fmt.Errorf("unable to serialize build: %v", err)
		}
		glog.V(4).Infof("redacted build: %v", string(bytes))
	}
	if errs := validation.ValidateBuild(cfg.build); len(errs) > 0 {
		return nil, errors.NewInvalid(schema.GroupKind{Kind: "Build"}, cfg.build.Name, errs)
	}

	masterVersion := os.Getenv(buildapi.OriginVersion)
	thisVersion := version.Get().String()
	if len(masterVersion) != 0 && masterVersion != thisVersion {
		glog.V(3).Infof("warning: OpenShift server version %q differs from this image %q\n", masterVersion, thisVersion)
	} else {
		glog.V(4).Infof("Master version %q, Builder version %q", masterVersion, thisVersion)
	}

	// sourceSecretsDir (SOURCE_SECRET_PATH)
	cfg.sourceSecretDir = os.Getenv("SOURCE_SECRET_PATH")

	if needsDocker {
		// dockerClient and dockerEndpoint (DOCKER_HOST)
		// usually not set, defaults to docker socket
		cfg.dockerClient, cfg.dockerEndpoint, err = bld.GetDockerClient()
		if err != nil {
			return nil, fmt.Errorf("no Docker configuration defined: %v", err)
		}
	}

	// buildsClient (KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT)
	clientConfig, err := restclient.InClusterConfig()
	if err != nil {
		return nil, fmt.Errorf("cannot connect to the server: %v", err)
	}
	osClient, err := client.New(clientConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to get client: %v", err)
	}
	cfg.buildsClient = osClient.Builds(cfg.build.Namespace)

	return cfg, nil
}

func (c *builderConfig) setupGitEnvironment() (string, []string, error) {

	// For now, we only handle git. If not specified, we're done
	gitSource := c.build.Spec.Source.Git
	if gitSource == nil {
		return "", []string{}, nil
	}

	sourceSecret := c.build.Spec.Source.SourceSecret
	gitEnv := []string{"GIT_ASKPASS=true"}
	// If a source secret is present, set it up and add its environment variables
	if sourceSecret != nil {
		// TODO: this should be refactored to let each source type manage which secrets
		// it accepts
		sourceURL, err := s2igit.Parse(gitSource.URI)
		if err != nil {
			return "", nil, fmt.Errorf("cannot parse build URL: %s", gitSource.URI)
		}
		scmAuths := scmauth.GitAuths(sourceURL)

		// TODO: remove when not necessary to fix up the secret dir permission
		secretsEnv, overrideURL, err := scmAuths.Setup(c.sourceSecretDir)
		if err != nil {
			return c.sourceSecretDir, nil, fmt.Errorf("cannot setup source secret: %v", err)
		}
		if overrideURL != nil {
			gitSource.URI = overrideURL.String()
		}
		gitEnv = append(gitEnv, secretsEnv...)
	}
	if gitSource.HTTPProxy != nil && len(*gitSource.HTTPProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("HTTP_PROXY=%s", *gitSource.HTTPProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("http_proxy=%s", *gitSource.HTTPProxy))
	}
	if gitSource.HTTPSProxy != nil && len(*gitSource.HTTPSProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("HTTPS_PROXY=%s", *gitSource.HTTPSProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("https_proxy=%s", *gitSource.HTTPSProxy))
	}
	if gitSource.NoProxy != nil && len(*gitSource.NoProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("NO_PROXY=%s", *gitSource.NoProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("no_proxy=%s", *gitSource.NoProxy))
	}
	return c.sourceSecretDir, bld.MergeEnv(os.Environ(), gitEnv), nil
}

// clone is responsible for cloning the source referenced in the buildconfig
func (c *builderConfig) clone() error {
	ctx := timing.NewContext(context.Background())
	var sourceRev *buildapi.SourceRevision
	defer func() {
		c.build.Status.Stages = timing.GetStages(ctx)
		bld.HandleBuildStatusUpdate(c.build, c.buildsClient, sourceRev)
	}()
	secretTmpDir, gitEnv, err := c.setupGitEnvironment()
	if err != nil {
		return err
	}
	defer os.RemoveAll(secretTmpDir)

	gitClient := git.NewRepositoryWithEnv(gitEnv)

	buildDir := buildutil.InputContentPath
	sourceInfo, err := bld.GitClone(ctx, gitClient, c.build.Spec.Source.Git, c.build.Spec.Revision, buildDir)
	if err != nil {
		c.build.Status.Phase = buildapi.BuildPhaseFailed
		c.build.Status.Reason = buildapi.StatusReasonFetchSourceFailed
		c.build.Status.Message = buildapi.StatusMessageFetchSourceFailed
		return err
	}

	if sourceInfo != nil {
		sourceRev = bld.GetSourceRevision(c.build, sourceInfo)
	}

	err = bld.ExtractInputBinary(os.Stdin, c.build.Spec.Source.Binary, buildDir)
	if err != nil {
		c.build.Status.Phase = buildapi.BuildPhaseFailed
		c.build.Status.Reason = buildapi.StatusReasonFetchSourceFailed
		c.build.Status.Message = buildapi.StatusMessageFetchSourceFailed
		return err
	}

	if len(c.build.Spec.Source.ContextDir) > 0 {
		if _, err := os.Stat(filepath.Join(buildDir, c.build.Spec.Source.ContextDir)); os.IsNotExist(err) {
			err = fmt.Errorf("provided context directory does not exist: %s", c.build.Spec.Source.ContextDir)
			c.build.Status.Phase = buildapi.BuildPhaseFailed
			c.build.Status.Reason = buildapi.StatusReasonInvalidContextDirectory
			c.build.Status.Message = buildapi.StatusMessageInvalidContextDirectory
			return err
		}
	}

	return nil
}

func (c *builderConfig) extractImageContent() error {
	ctx := timing.NewContext(context.Background())
	defer func() {
		c.build.Status.Stages = timing.GetStages(ctx)
		bld.HandleBuildStatusUpdate(c.build, c.buildsClient, nil)
	}()

	buildDir := buildutil.InputContentPath
	return bld.ExtractImageContent(ctx, c.dockerClient, buildDir, c.build)
}

// execute is responsible for running a build
func (c *builderConfig) execute(b builder) error {
	cgLimits, err := bld.GetCGroupLimits()
	if err != nil {
		return fmt.Errorf("failed to retrieve cgroup limits: %v", err)
	}
	glog.V(4).Infof("Running build with cgroup limits: %#v", *cgLimits)

	if err := b.Build(c.dockerClient, c.dockerEndpoint, c.buildsClient, c.build, cgLimits); err != nil {
		return fmt.Errorf("build error: %v", err)
	}

	if c.build.Spec.Output.To == nil || len(c.build.Spec.Output.To.Name) == 0 {
		fmt.Fprintf(c.out, "Build complete, no image push requested\n")
	}

	return nil
}

type dockerBuilder struct{}

// Build starts a Docker build.
func (dockerBuilder) Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error {
	return bld.NewDockerBuilder(dockerClient, buildsClient, build, cgLimits).Build()
}

type s2iBuilder struct{}

// Build starts an S2I build.
func (s2iBuilder) Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error {
	return bld.NewS2IBuilder(dockerClient, sock, buildsClient, build, cgLimits).Build()
}

func runBuild(out io.Writer, builder builder) error {
	cfg, err := newBuilderConfigFromEnvironment(out, true)
	if err != nil {
		return err
	}
	return cfg.execute(builder)
}

// RunDockerBuild creates a docker builder and runs its build
func RunDockerBuild(out io.Writer) error {
	return runBuild(out, dockerBuilder{})
}

// RunS2IBuild creates a S2I builder and runs its build
func RunS2IBuild(out io.Writer) error {
	return runBuild(out, s2iBuilder{})
}

// RunGitClone performs a git clone using the build defined in the environment
func RunGitClone(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, false)
	if err != nil {
		return err
	}
	return cfg.clone()
}

// RunManageDockerfile manipulates the dockerfile for docker builds.
// It will write the inline dockerfile to the working directory (possibly
// overwriting an existing dockerfile) and then update the dockerfile
// in the working directory (accounting for contextdir+dockerfilepath)
// with new FROM image information based on the imagestream/imagetrigger
// and also adds some env and label values to the dockerfile based on
// the build information.
func RunManageDockerfile(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, false)
	if err != nil {
		return err
	}
	return bld.ManageDockerfile(buildutil.InputContentPath, cfg.build)
}

// RunExtractImageContent extracts files from existing images
// into the build working directory.
func RunExtractImageContent(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, true)
	if err != nil {
		return err
	}
	return cfg.extractImageContent()
}
<commit_msg>gabe3<commit_after>package cmd

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/golang/glog"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	restclient "k8s.io/client-go/rest"
	kapi "k8s.io/kubernetes/pkg/api"

	s2iapi "github.com/openshift/source-to-image/pkg/api"
	s2igit "github.com/openshift/source-to-image/pkg/scm/git"

	buildapi "github.com/openshift/origin/pkg/build/apis/build"
	"github.com/openshift/origin/pkg/build/apis/build/validation"
	bld "github.com/openshift/origin/pkg/build/builder"
	"github.com/openshift/origin/pkg/build/builder/cmd/scmauth"
	"github.com/openshift/origin/pkg/build/builder/timing"
	buildutil "github.com/openshift/origin/pkg/build/util"
	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/generate/git"
	"github.com/openshift/origin/pkg/version"
)

type builder interface {
	Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error
}

type builderConfig struct {
	out             io.Writer
	build           *buildapi.Build
	sourceSecretDir string
	dockerClient    *docker.Client
	dockerEndpoint  string
	buildsClient    client.BuildInterface
}

func newBuilderConfigFromEnvironment(out io.Writer, needsDocker bool) (*builderConfig, error) {
	cfg := &builderConfig{}
	var err error

	cfg.out = out

	buildStr := os.Getenv("BUILD")
	cfg.build = &buildapi.Build{}

	obj, groupVersionKind, err := kapi.Codecs.UniversalDecoder().Decode([]byte(buildStr), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("unable to parse build string: %v", err)
	}
	ok := false
	cfg.build, ok = obj.(*buildapi.Build)
	if !ok {
		return nil, fmt.Errorf("build string is not a build: %v", err)
	}
	if glog.V(4) {
		redactedBuild := buildutil.SafeForLoggingBuild(cfg.build)
		if err != nil {
			return nil, fmt.Errorf("unable to strip proxy credentials from build: %v", err)
		}
		bytes, err := runtime.Encode(kapi.Codecs.LegacyCodec(groupVersionKind.GroupVersion()), redactedBuild)
		if err != nil {
			return nil, fmt.Errorf("unable to serialize build: %v", err)
		}
		glog.V(4).Infof("redacted build: %v", string(bytes))
	}
	if errs := validation.ValidateBuild(cfg.build); len(errs) > 0 {
		return nil, errors.NewInvalid(schema.GroupKind{Kind: "Build"}, cfg.build.Name, errs)
	}

	masterVersion := os.Getenv(buildapi.OriginVersion)
	thisVersion := version.Get().String()
	if len(masterVersion) != 0 && masterVersion != thisVersion {
		glog.V(3).Infof("warning: OpenShift server version %q differs from this image %q\n", masterVersion, thisVersion)
	} else {
		glog.V(4).Infof("Master version %q, Builder version %q", masterVersion, thisVersion)
	}

	// sourceSecretsDir (SOURCE_SECRET_PATH)
	cfg.sourceSecretDir = os.Getenv("SOURCE_SECRET_PATH")

	if needsDocker {
		// dockerClient and dockerEndpoint (DOCKER_HOST)
		// usually not set, defaults to docker socket
		cfg.dockerClient, cfg.dockerEndpoint, err = bld.GetDockerClient()
		if err != nil {
			return nil, fmt.Errorf("no Docker configuration defined: %v", err)
		}
	}

	// buildsClient (KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT)
	clientConfig, err := restclient.InClusterConfig()
	if err != nil {
		return nil, fmt.Errorf("cannot connect to the server: %v", err)
	}
	osClient, err := client.New(clientConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to get client: %v", err)
	}
	cfg.buildsClient = osClient.Builds(cfg.build.Namespace)

	return cfg, nil
}

func (c *builderConfig) setupGitEnvironment() (string, []string, error) {

	// For now, we only handle git. If not specified, we're done
	gitSource := c.build.Spec.Source.Git
	if gitSource == nil {
		return "", []string{}, nil
	}

	sourceSecret := c.build.Spec.Source.SourceSecret
	gitEnv := []string{"GIT_ASKPASS=true"}
	// If a source secret is present, set it up and add its environment variables
	if sourceSecret != nil {
		// TODO: this should be refactored to let each source type manage which secrets
		// it accepts
		sourceURL, err := s2igit.Parse(gitSource.URI)
		if err != nil {
			return "", nil, fmt.Errorf("cannot parse build URL: %s", gitSource.URI)
		}
		scmAuths := scmauth.GitAuths(sourceURL)

		secretsEnv, overrideURL, err := scmAuths.Setup(c.sourceSecretDir)
		if err != nil {
			return c.sourceSecretDir, nil, fmt.Errorf("cannot setup source secret: %v", err)
		}
		if overrideURL != nil {
			gitSource.URI = overrideURL.String()
		}
		gitEnv = append(gitEnv, secretsEnv...)
	}
	if gitSource.HTTPProxy != nil && len(*gitSource.HTTPProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("HTTP_PROXY=%s", *gitSource.HTTPProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("http_proxy=%s", *gitSource.HTTPProxy))
	}
	if gitSource.HTTPSProxy != nil && len(*gitSource.HTTPSProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("HTTPS_PROXY=%s", *gitSource.HTTPSProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("https_proxy=%s", *gitSource.HTTPSProxy))
	}
	if gitSource.NoProxy != nil && len(*gitSource.NoProxy) > 0 {
		gitEnv = append(gitEnv, fmt.Sprintf("NO_PROXY=%s", *gitSource.NoProxy))
		gitEnv = append(gitEnv, fmt.Sprintf("no_proxy=%s", *gitSource.NoProxy))
	}
	return c.sourceSecretDir, bld.MergeEnv(os.Environ(), gitEnv), nil
}

// clone is responsible for cloning the source referenced in the buildconfig
func (c *builderConfig) clone() error {
	ctx := timing.NewContext(context.Background())
	var sourceRev *buildapi.SourceRevision
	defer func() {
		c.build.Status.Stages = timing.GetStages(ctx)
		bld.HandleBuildStatusUpdate(c.build, c.buildsClient, sourceRev)
	}()
	secretTmpDir, gitEnv, err := c.setupGitEnvironment()
	if err != nil {
		return err
	}
	defer os.RemoveAll(secretTmpDir)

	gitClient := git.NewRepositoryWithEnv(gitEnv)

	buildDir := buildutil.InputContentPath
	sourceInfo, err := bld.GitClone(ctx, gitClient, c.build.Spec.Source.Git, c.build.Spec.Revision, buildDir)
	if err != nil {
		c.build.Status.Phase = buildapi.BuildPhaseFailed
		c.build.Status.Reason = buildapi.StatusReasonFetchSourceFailed
		c.build.Status.Message = buildapi.StatusMessageFetchSourceFailed
		return err
	}

	if sourceInfo != nil {
		sourceRev = bld.GetSourceRevision(c.build, sourceInfo)
	}

	err = bld.ExtractInputBinary(os.Stdin, c.build.Spec.Source.Binary, buildDir)
	if err != nil {
		c.build.Status.Phase = buildapi.BuildPhaseFailed
		c.build.Status.Reason = buildapi.StatusReasonFetchSourceFailed
		c.build.Status.Message = buildapi.StatusMessageFetchSourceFailed
		return err
	}

	if len(c.build.Spec.Source.ContextDir) > 0 {
		if _, err := os.Stat(filepath.Join(buildDir, c.build.Spec.Source.ContextDir)); os.IsNotExist(err) {
			err = fmt.Errorf("provided context directory does not exist: %s", c.build.Spec.Source.ContextDir)
			c.build.Status.Phase = buildapi.BuildPhaseFailed
			c.build.Status.Reason = buildapi.StatusReasonInvalidContextDirectory
			c.build.Status.Message = buildapi.StatusMessageInvalidContextDirectory
			return err
		}
	}

	return nil
}

func (c *builderConfig) extractImageContent() error {
	ctx := timing.NewContext(context.Background())
	defer func() {
		c.build.Status.Stages = timing.GetStages(ctx)
		bld.HandleBuildStatusUpdate(c.build, c.buildsClient, nil)
	}()

	buildDir := buildutil.InputContentPath
	return bld.ExtractImageContent(ctx, c.dockerClient, buildDir, c.build)
}

// execute is responsible for running a build
func (c *builderConfig) execute(b builder) error {
	cgLimits, err := bld.GetCGroupLimits()
	if err != nil {
		return fmt.Errorf("failed to retrieve cgroup limits: %v", err)
	}
	glog.V(4).Infof("Running build with cgroup limits: %#v", *cgLimits)

	if err := b.Build(c.dockerClient, c.dockerEndpoint, c.buildsClient, c.build, cgLimits); err != nil {
		return fmt.Errorf("build error: %v", err)
	}

	if c.build.Spec.Output.To == nil || len(c.build.Spec.Output.To.Name) == 0 {
		fmt.Fprintf(c.out, "Build complete, no image push requested\n")
	}

	return nil
}

type dockerBuilder struct{}

// Build starts a Docker build.
func (dockerBuilder) Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error {
	return bld.NewDockerBuilder(dockerClient, buildsClient, build, cgLimits).Build()
}

type s2iBuilder struct{}

// Build starts an S2I build.
func (s2iBuilder) Build(dockerClient bld.DockerClient, sock string, buildsClient client.BuildInterface, build *buildapi.Build, cgLimits *s2iapi.CGroupLimits) error {
	return bld.NewS2IBuilder(dockerClient, sock, buildsClient, build, cgLimits).Build()
}

func runBuild(out io.Writer, builder builder) error {
	cfg, err := newBuilderConfigFromEnvironment(out, true)
	if err != nil {
		return err
	}
	return cfg.execute(builder)
}

// RunDockerBuild creates a docker builder and runs its build
func RunDockerBuild(out io.Writer) error {
	return runBuild(out, dockerBuilder{})
}

// RunS2IBuild creates a S2I builder and runs its build
func RunS2IBuild(out io.Writer) error {
	return runBuild(out, s2iBuilder{})
}

// RunGitClone performs a git clone using the build defined in the environment
func RunGitClone(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, false)
	if err != nil {
		return err
	}
	return cfg.clone()
}

// RunManageDockerfile manipulates the dockerfile for docker builds.
// It will write the inline dockerfile to the working directory (possibly
// overwriting an existing dockerfile) and then update the dockerfile
// in the working directory (accounting for contextdir+dockerfilepath)
// with new FROM image information based on the imagestream/imagetrigger
// and also adds some env and label values to the dockerfile based on
// the build information.
func RunManageDockerfile(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, false)
	if err != nil {
		return err
	}
	return bld.ManageDockerfile(buildutil.InputContentPath, cfg.build)
}

// RunExtractImageContent extracts files from existing images
// into the build working directory.
func RunExtractImageContent(out io.Writer) error {
	cfg, err := newBuilderConfigFromEnvironment(out, true)
	if err != nil {
		return err
	}
	return cfg.extractImageContent()
}
<|endoftext|>


<commit_before>package modes

import (
	"errors"
	"testing"

	"src.elv.sh/pkg/cli"
	. "src.elv.sh/pkg/cli/clitest"
	"src.elv.sh/pkg/cli/lscolors"
	"src.elv.sh/pkg/cli/term"
	"src.elv.sh/pkg/cli/tk"
	"src.elv.sh/pkg/must"
	"src.elv.sh/pkg/testutil"
	"src.elv.sh/pkg/ui"
)

var testDir = testutil.Dir{
	"a": "",
	"d": testutil.Dir{
		"d1": "content\td1\nline 2",
		"d2": testutil.Dir{
			"d21":       "content d21",
			"d22":       "content d22",
			"other.png": "",
		},
		"d3":  testutil.Dir{},
		".dh": "hidden",
	},
	"f": "",
}

func TestErrorInAscend(t *testing.T) {
	f := Setup()
	defer f.Stop()

	c := getTestCursor()
	c.ascendErr = errors.New("cannot ascend")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TTY.Inject(term.K(ui.Left))
	f.TestTTYNotes(t,
		"error: cannot ascend", Styles,
		"!!!!!!")
}

func TestErrorInDescend(t *testing.T) {
	f := Setup()
	defer f.Stop()

	c := getTestCursor()
	c.descendErr = errors.New("cannot descend")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TTY.Inject(term.K(ui.Down))
	f.TTY.Inject(term.K(ui.Right))
	f.TestTTYNotes(t,
		"error: cannot descend", Styles,
		"!!!!!!")
}

func TestErrorInCurrent(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	c.currentErr = errors.New("ERR")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a   ERR            \n", Styles,
		"     !!!",
		" d  \n", Styles,
		"////",
		" f  ",
	)

	// Test that Right does nothing.
	f.TTY.Inject(term.K(ui.Right))
	// We can't just test that the buffer hasn't changed, because that might
	// capture the state of the buffer before the Right key is handled. Instead
	// we inject a key and test the result of that instead, to ensure that the
	// Right key had no effect.
	f.TTY.Inject(term.K('x'))
	f.TestTTY(t,
		"x", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a   ERR            \n", Styles,
		"     !!!",
		" d  \n", Styles,
		"////",
		" f  ",
	)
}

func TestErrorInParent(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	c.parentErr = errors.New("ERR")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		"ERR   d1            content    d1\n", Styles,
		"!!!  ++++++++++++++",
		"      d2            line 2\n", Styles,
		"     //////////////",
		"      d3           ", Styles,
		"     //////////////",
	)
}

func TestWidthRatio(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	startNavigation(f.App, NavigationSpec{
		Cursor:     c,
		WidthRatio: func() [3]int { return [3]int{1, 1, 1} },
	})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a            d1           content    d1\n", Styles,
		"             +++++++++++++",
		" d            d2           line 2\n", Styles,
		"############ /////////////",
		" f            d3          ", Styles,
		"             /////////////",
	)
}

func TestGetSelectedName(t *testing.T) {
	f := Setup()
	defer f.Stop()

	w := startNavigation(f.App, NavigationSpec{Cursor: getTestCursor()})

	wantName := "d1"
	if name := w.SelectedName(); name != wantName {
		t.Errorf("Got name %q, want %q", name, wantName)
	}
}

func TestNavigation_FakeFS(t *testing.T) {
	cursor := getTestCursor()
	testNavigation(t, cursor)
}

func TestNavigation_RealFS(t *testing.T) {
	testutil.InTempDir(t)
	testutil.ApplyDir(testDir)

	must.Chdir("d")
	testNavigation(t, nil)
}

func testNavigation(t *testing.T, c NavigationCursor) {
	f := setupNav(t)
	defer f.Stop()

	w := startNavigation(f.App, NavigationSpec{Cursor: c})

	// Test initial UI and file preview.
	// NOTE: Buffers are named after the file that is now being selected.
	d1Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1            content    d1\n", Styles,
		"     ++++++++++++++",
		" d    d2            line 2\n", Styles,
		"#### //////////////",
		" f    d3           ", Styles,
		"     //////////////",
	)
	f.TTY.TestBuffer(t, d1Buf)

	// Test scrolling of preview.
	w.ScrollPreview(1)
	f.App.Redraw()
	d1Buf2 := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1            line 2             │\n", Styles,
		"     ++++++++++++++                    -",
		" d    d2                               │\n", Styles,
		"#### //////////////                    -",
		" f    d3                                ", Styles,
		"     //////////////                    X",
	)
	f.TTY.TestBuffer(t, d1Buf2)

	// Test handling of selection change and directory preview. Also test
	// LS_COLORS.
	w.Select(tk.Next)
	f.App.Redraw()
	d2Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1             d21                \n", Styles,
		"                    ++++++++++++++++++++",
		" d    d2             d22                \n", Styles,
		"#### ##############",
		" f    d3             other.png          ", Styles,
		"     ////////////// !!!!!!!!!!!!!!!!!!!!",
	)
	f.TTY.TestBuffer(t, d2Buf)

	// Test handling of Descend.
	w.Descend()
	f.App.Redraw()
	d21Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" d1   d21           content d21\n", Styles,
		"     ++++++++++++++",
		" d2   d22          \n", Styles,
		"####",
		" d3   other.png    ", Styles,
		"//// !!!!!!!!!!!!!!",
	)
	f.TTY.TestBuffer(t, d21Buf)

	// Test handling of Ascend, and that the current column selects the
	// directory we just ascended from, thus reverting to wantBuf1.
	w.Ascend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d2Buf)

	// Test handling of Descend on a regular file, i.e. do nothing. First move
	// the cursor to d1, which is a regular file.
	w.Select(tk.Prev)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d1Buf)
	// Now descend, and verify that the buffer has not changed.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d1Buf)

	// Test showing hidden.
	w.MutateShowHidden(func(bool) bool { return true })
	f.App.Redraw()
	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING (show hidden)  \n", Styles,
		"************************** ",
		" a    .dh           content    d1\n",
		" d    d1            line 2\n", Styles,
		"#### ++++++++++++++",
		" f    d2           \n", Styles,
		"     //////////////",
		"      d3           ", Styles,
		"     //////////////",
	)
	w.MutateShowHidden(func(bool) bool { return false })

	// Test filtering; current column shows d1, d2, d3 before filtering, and
	// only shows d2 after filtering.
	w.MutateFiltering(func(bool) bool { return true })
	f.TTY.Inject(term.K('2'))
	f.TestTTY(t,
		"\n",
		" NAVIGATING  2", Styles,
		"************  ", term.DotHere, "\n",
		" a    d2             d21                \n", Styles,
		"     ############## ++++++++++++++++++++",
		" d                   d22                \n", Styles,
		"####",
		" f                   other.png          ", Styles,
		"                    !!!!!!!!!!!!!!!!!!!!",
	)
	w.MutateFiltering(func(bool) bool { return false })

	// Now move into d2, and test that the filter has been cleared when
	// descending.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d21Buf)

	// Apply a filter within d2.
	w.MutateFiltering(func(bool) bool { return true })
	f.TTY.Inject(term.K('2'))
	f.TestTTY(t,
		"\n",
		" NAVIGATING  2", Styles,
		"************  ", term.DotHere, "\n",
		" d1   d21           content d21\n", Styles,
		"     ++++++++++++++",
		" d2   d22          \n", Styles,
		"####",
		" d3 ", Styles,
		"////",
	)
	w.MutateFiltering(func(bool) bool { return false })

	// Ascend, and test that the filter has been cleared again when ascending.
	w.Ascend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d2Buf)

	// Now move into d3, an empty directory.
	w.Select(tk.Next)
	w.Descend()
	f.App.Redraw()
	d3NoneBuf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" d1                 \n",
		" d2 \n", Styles,
		"////",
		" d3 ", Styles,
		"####",
	)
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that selecting the previous does nothing in an empty directory.
	w.Select(tk.Prev)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that selecting the next does nothing in an empty directory.
	w.Select(tk.Next)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that Descend does nothing in an empty directory.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
}

func TestNewNavigation_FocusedWidgetNotCodeArea(t *testing.T) {
	testFocusedWidgetNotCodeArea(t, func(app cli.App) error {
		_, err := NewNavigation(app, NavigationSpec{})
		return err
	})
}

func setupNav(c testutil.Cleanuper) *Fixture {
	lscolors.SetTestLsColors(c)
	// Use a small TTY size to make the test buffer easier to build.
	return Setup(WithTTY(func(tty TTYCtrl) { tty.SetSize(6, 40) }))
}

func startNavigation(app cli.App, spec NavigationSpec) Navigation {
	w, _ := NewNavigation(app, spec)
	startMode(app, w, nil)
	return w
}

func getTestCursor() *testCursor {
	return &testCursor{root: testDir, pwd: []string{"d"}}
}
<commit_msg>pkg/cli/modes: Improve nav mode test coverage.<commit_after>package modes

import (
	"errors"
	"testing"

	"src.elv.sh/pkg/cli"
	. "src.elv.sh/pkg/cli/clitest"
	"src.elv.sh/pkg/cli/lscolors"
	"src.elv.sh/pkg/cli/term"
	"src.elv.sh/pkg/cli/tk"
	"src.elv.sh/pkg/must"
	"src.elv.sh/pkg/testutil"
	"src.elv.sh/pkg/ui"
)

var testDir = testutil.Dir{
	"a": "",
	"d": testutil.Dir{
		"d1": "content\td1\nline 2",
		"d2": testutil.Dir{
			"d21":       "content d21",
			"d22":       "content d22",
			"other.png": "",
		},
		"d3":  testutil.Dir{},
		".dh": "hidden",
	},
	"f": "",
}

func TestErrorInAscend(t *testing.T) {
	f := Setup()
	defer f.Stop()

	c := getTestCursor()
	c.ascendErr = errors.New("cannot ascend")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TTY.Inject(term.K(ui.Left))
	f.TestTTYNotes(t,
		"error: cannot ascend", Styles,
		"!!!!!!")
}

func TestErrorInDescend(t *testing.T) {
	f := Setup()
	defer f.Stop()

	c := getTestCursor()
	c.descendErr = errors.New("cannot descend")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TTY.Inject(term.K(ui.Down))
	f.TTY.Inject(term.K(ui.Right))
	f.TestTTYNotes(t,
		"error: cannot descend", Styles,
		"!!!!!!")
}

func TestErrorInCurrent(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	c.currentErr = errors.New("ERR")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a   ERR            \n", Styles,
		"     !!!",
		" d  \n", Styles,
		"////",
		" f  ",
	)

	// Test that Right does nothing.
	f.TTY.Inject(term.K(ui.Right))
	// We can't just test that the buffer hasn't changed, because that might
	// capture the state of the buffer before the Right key is handled. Instead
	// we inject a key and test the result of that instead, to ensure that the
	// Right key had no effect.
	f.TTY.Inject(term.K('x'))
	f.TestTTY(t,
		"x", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a   ERR            \n", Styles,
		"     !!!",
		" d  \n", Styles,
		"////",
		" f  ",
	)
}

func TestErrorInParent(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	c.parentErr = errors.New("ERR")
	startNavigation(f.App, NavigationSpec{Cursor: c})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		"ERR   d1            content    d1\n", Styles,
		"!!!  ++++++++++++++",
		"      d2            line 2\n", Styles,
		"     //////////////",
		"      d3           ", Styles,
		"     //////////////",
	)
}

func TestWidthRatio(t *testing.T) {
	f := setupNav(t)
	defer f.Stop()

	c := getTestCursor()
	startNavigation(f.App, NavigationSpec{
		Cursor:     c,
		WidthRatio: func() [3]int { return [3]int{1, 1, 1} },
	})

	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a            d1           content    d1\n", Styles,
		"             +++++++++++++",
		" d            d2           line 2\n", Styles,
		"############ /////////////",
		" f            d3          ", Styles,
		"             /////////////",
	)
}

func TestNavigation_SelectedName(t *testing.T) {
	f := Setup()
	defer f.Stop()

	w := startNavigation(f.App, NavigationSpec{Cursor: getTestCursor()})

	wantName := "d1"
	if name := w.SelectedName(); name != wantName {
		t.Errorf("Got name %q, want %q", name, wantName)
	}
}

func TestNavigation_SelectedName_EmptyDirectory(t *testing.T) {
	f := Setup()
	defer f.Stop()

	cursor := &testCursor{
		root: testutil.Dir{"d": testutil.Dir{}},
		pwd:  []string{"d"}}
	w := startNavigation(f.App, NavigationSpec{Cursor: cursor})

	wantName := ""
	if name := w.SelectedName(); name != wantName {
		t.Errorf("Got name %q, want %q", name, wantName)
	}
}

func TestNavigation_FakeFS(t *testing.T) {
	cursor := getTestCursor()
	testNavigation(t, cursor)
}

func TestNavigation_RealFS(t *testing.T) {
	testutil.InTempDir(t)
	testutil.ApplyDir(testDir)

	must.Chdir("d")
	testNavigation(t, nil)
}

func testNavigation(t *testing.T, c NavigationCursor) {
	f := setupNav(t)
	defer f.Stop()

	w := startNavigation(f.App, NavigationSpec{Cursor: c})

	// Test initial UI and file preview.
	// NOTE: Buffers are named after the file that is now being selected.
	d1Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1            content    d1\n", Styles,
		"     ++++++++++++++",
		" d    d2            line 2\n", Styles,
		"#### //////////////",
		" f    d3           ", Styles,
		"     //////////////",
	)
	f.TTY.TestBuffer(t, d1Buf)

	// Test scrolling of preview.
	w.ScrollPreview(1)
	f.App.Redraw()
	d1Buf2 := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1            line 2             │\n", Styles,
		"     ++++++++++++++                    -",
		" d    d2                               │\n", Styles,
		"#### //////////////                    -",
		" f    d3                                ", Styles,
		"     //////////////                    X",
	)
	f.TTY.TestBuffer(t, d1Buf2)

	// Test handling of selection change and directory preview. Also test
	// LS_COLORS.
	w.Select(tk.Next)
	f.App.Redraw()
	d2Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" a    d1             d21                \n", Styles,
		"                    ++++++++++++++++++++",
		" d    d2             d22                \n", Styles,
		"#### ##############",
		" f    d3             other.png          ", Styles,
		"     ////////////// !!!!!!!!!!!!!!!!!!!!",
	)
	f.TTY.TestBuffer(t, d2Buf)

	// Test handling of Descend.
	w.Descend()
	f.App.Redraw()
	d21Buf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" d1   d21           content d21\n", Styles,
		"     ++++++++++++++",
		" d2   d22          \n", Styles,
		"####",
		" d3   other.png    ", Styles,
		"//// !!!!!!!!!!!!!!",
	)
	f.TTY.TestBuffer(t, d21Buf)

	// Test handling of Ascend, and that the current column selects the
	// directory we just ascended from, thus reverting to wantBuf1.
	w.Ascend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d2Buf)

	// Test handling of Descend on a regular file, i.e. do nothing. First move
	// the cursor to d1, which is a regular file.
	w.Select(tk.Prev)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d1Buf)
	// Now descend, and verify that the buffer has not changed.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d1Buf)

	// Test showing hidden.
	w.MutateShowHidden(func(bool) bool { return true })
	f.App.Redraw()
	f.TestTTY(t,
		"", term.DotHere, "\n",
		" NAVIGATING (show hidden)  \n", Styles,
		"************************** ",
		" a    .dh           content    d1\n",
		" d    d1            line 2\n", Styles,
		"#### ++++++++++++++",
		" f    d2           \n", Styles,
		"     //////////////",
		"      d3           ", Styles,
		"     //////////////",
	)
	w.MutateShowHidden(func(bool) bool { return false })

	// Test filtering; current column shows d1, d2, d3 before filtering, and
	// only shows d2 after filtering.
	w.MutateFiltering(func(bool) bool { return true })
	f.TTY.Inject(term.K('2'))
	dFilter2Buf := f.MakeBuffer(
		"\n",
		" NAVIGATING  2", Styles,
		"************  ", term.DotHere, "\n",
		" a    d2             d21                \n", Styles,
		"     ############## ++++++++++++++++++++",
		" d                   d22                \n", Styles,
		"####",
		" f                   other.png          ", Styles,
		"                    !!!!!!!!!!!!!!!!!!!!",
	)
	f.TTY.TestBuffer(t, dFilter2Buf)

	// Unbound key while filtering is ignored.
	f.TTY.Inject(term.K('a', ui.Alt))
	f.TTY.TestBuffer(t, dFilter2Buf)
	w.MutateFiltering(func(bool) bool { return false })

	// Now move into d2, and test that the filter has been cleared when
	// descending.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d21Buf)

	// Apply a filter within d2.
	w.MutateFiltering(func(bool) bool { return true })
	f.TTY.Inject(term.K('2'))
	f.TestTTY(t,
		"\n",
		" NAVIGATING  2", Styles,
		"************  ", term.DotHere, "\n",
		" d1   d21           content d21\n", Styles,
		"     ++++++++++++++",
		" d2   d22          \n", Styles,
		"####",
		" d3 ", Styles,
		"////",
	)
	w.MutateFiltering(func(bool) bool { return false })

	// Ascend, and test that the filter has been cleared again when ascending.
	w.Ascend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d2Buf)

	// Now move into d3, an empty directory.
	w.Select(tk.Next)
	w.Descend()
	f.App.Redraw()
	d3NoneBuf := f.MakeBuffer(
		"", term.DotHere, "\n",
		" NAVIGATING  \n", Styles,
		"************ ",
		" d1                 \n",
		" d2 \n", Styles,
		"////",
		" d3 ", Styles,
		"####",
	)
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that selecting the previous does nothing in an empty directory.
	w.Select(tk.Prev)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that selecting the next does nothing in an empty directory.
	w.Select(tk.Next)
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
	// Test that Descend does nothing in an empty directory.
	w.Descend()
	f.App.Redraw()
	f.TTY.TestBuffer(t, d3NoneBuf)
}

func TestNewNavigation_FocusedWidgetNotCodeArea(t *testing.T) {
	testFocusedWidgetNotCodeArea(t, func(app cli.App) error {
		_, err := NewNavigation(app, NavigationSpec{})
		return err
	})
}

func setupNav(c testutil.Cleanuper) *Fixture {
	lscolors.SetTestLsColors(c)
	// Use a small TTY size to make the test buffer easier to build.
	return Setup(WithTTY(func(tty TTYCtrl) { tty.SetSize(6, 40) }))
}

func startNavigation(app cli.App, spec NavigationSpec) Navigation {
	w, _ := NewNavigation(app, spec)
	startMode(app, w, nil)
	return w
}

func getTestCursor() *testCursor {
	return &testCursor{root: testDir, pwd: []string{"d"}}
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package feature

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

	"github.com/golang/glog"
	"github.com/spf13/pflag"
)

type Feature string

const (
	flagName = "feature-gates"

	// allAlphaGate is a global toggle for alpha features. Per-feature key
	// values override the default set by allAlphaGate. Examples:
	//   AllAlpha=false,NewFeature=true  will result in newFeature=true
	//   AllAlpha=true,NewFeature=false  will result in newFeature=false
	allAlphaGate Feature = "AllAlpha"
)

var (
	// The generic features.
	defaultFeatures = map[Feature]FeatureSpec{
		allAlphaGate: {Default: false, PreRelease: Alpha},
	}

	// Special handling for a few gates.
	specialFeatures = map[Feature]func(f *featureGate, val bool){
		allAlphaGate: setUnsetAlphaGates,
	}

	// DefaultFeatureGate is a shared global FeatureGate.
	DefaultFeatureGate FeatureGate = NewFeatureGate()
)

type FeatureSpec struct {
	Default    bool
	PreRelease prerelease
}

type prerelease string

const (
	// Values for PreRelease.
	Alpha = prerelease("ALPHA")
	Beta  = prerelease("BETA")
	GA    = prerelease("")
)

// FeatureGate parses and stores flag gates for known features from
// a string like feature1=true,feature2=false,...
type FeatureGate interface {
	// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
	AddFlag(fs *pflag.FlagSet)
	// Set parses and stores flag gates for known features
	// from a string like feature1=true,feature2=false,...
	Set(value string) error
	// Enabled returns true if the key is enabled.
	Enabled(key Feature) bool
	// Add adds features to the featureGate.
	Add(features map[Feature]FeatureSpec) error
	// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
	KnownFeatures() []string
}

// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
type featureGate struct {
	known   map[Feature]FeatureSpec
	special map[Feature]func(*featureGate, bool)
	enabled map[Feature]bool

	// is set to true when AddFlag is called. Note: initialization is not go-routine safe, lookup is
	closed bool
}

func setUnsetAlphaGates(f *featureGate, val bool) {
	for k, v := range f.known {
		if v.PreRelease == Alpha {
			if _, found := f.enabled[k]; !found {
				f.enabled[k] = val
			}
		}
	}
}

// Set, String, and Type implement pflag.Value
var _ pflag.Value = &featureGate{}

func NewFeatureGate() *featureGate {
	f := &featureGate{
		known:   map[Feature]FeatureSpec{},
		special: specialFeatures,
		enabled: map[Feature]bool{},
	}
	for k, v := range defaultFeatures {
		f.known[k] = v
	}
	return f
}

// Set Parses a string of the form "key1=value1,key2=value2,..." into a
// map[string]bool of known keys or returns an error.
func (f *featureGate) Set(value string) error {
	for _, s := range strings.Split(value, ",") {
		if len(s) == 0 {
			continue
		}
		arr := strings.SplitN(s, "=", 2)
		k := Feature(strings.TrimSpace(arr[0]))
		_, ok := f.known[Feature(k)]
		if !ok {
			return fmt.Errorf("unrecognized key: %s", k)
		}
		if len(arr) != 2 {
			return fmt.Errorf("missing bool value for %s", k)
		}
		v := strings.TrimSpace(arr[1])
		boolValue, err := strconv.ParseBool(v)
		if err != nil {
			return fmt.Errorf("invalid value of %s: %s, err: %v", k, v, err)
		}
		f.enabled[k] = boolValue

		// Handle "special" features like "all alpha gates"
		if fn, found := f.special[k]; found {
			fn(f, boolValue)
		}
	}

	glog.Infof("feature gates: %v", f.enabled)
	return nil
}

// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...".
func (f *featureGate) String() string {
	pairs := []string{}
	for k, v := range f.enabled {
		pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
	}
	sort.Strings(pairs)
	return strings.Join(pairs, ",")
}

func (f *featureGate) Type() string {
	return "mapStringBool"
}

// Add adds features to the featureGate.
func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
	if f.closed {
		return fmt.Errorf("cannot add a feature gate after adding it to the flag set")
	}

	for name, spec := range features {
		if existingSpec, found := f.known[name]; found {
			if existingSpec == spec {
				continue
			}
			return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec)
		}

		f.known[name] = spec
	}
	return nil
}

// Enabled returns true if the key is enabled.
func (f *featureGate) Enabled(key Feature) bool {
	defaultValue := f.known[key].Default
	if f.enabled != nil {
		if v, ok := f.enabled[key]; ok {
			return v
		}
	}
	return defaultValue
}

// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
	f.closed = true

	known := f.KnownFeatures()
	fs.Var(f, flagName, ""+
		"A set of key=value pairs that describe feature gates for alpha/experimental features. "+
		"Options are:\n"+strings.Join(known, "\n"))
}

// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
func (f *featureGate) KnownFeatures() []string {
	var known []string
	for k, v := range f.known {
		pre := ""
		if v.PreRelease != GA {
			pre = fmt.Sprintf("%s - ", v.PreRelease)
		}
		known = append(known, fmt.Sprintf("%s=true|false (%sdefault=%t)", k, pre, v.Default))
	}
	sort.Strings(known)
	return known
}
<commit_msg>Make feature gate threadsafe<commit_after>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package feature

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

	"github.com/golang/glog"
	"github.com/spf13/pflag"
)

type Feature string

const (
	flagName = "feature-gates"

	// allAlphaGate is a global toggle for alpha features. Per-feature key
	// values override the default set by allAlphaGate. Examples:
	//   AllAlpha=false,NewFeature=true  will result in newFeature=true
	//   AllAlpha=true,NewFeature=false  will result in newFeature=false
	allAlphaGate Feature = "AllAlpha"
)

var (
	// The generic features.
	defaultFeatures = map[Feature]FeatureSpec{
		allAlphaGate: {Default: false, PreRelease: Alpha},
	}

	// Special handling for a few gates.
	specialFeatures = map[Feature]func(f *featureGate, val bool){
		allAlphaGate: setUnsetAlphaGates,
	}

	// DefaultFeatureGate is a shared global FeatureGate.
	DefaultFeatureGate FeatureGate = NewFeatureGate()
)

type FeatureSpec struct {
	Default    bool
	PreRelease prerelease
}

type prerelease string

const (
	// Values for PreRelease.
	Alpha = prerelease("ALPHA")
	Beta  = prerelease("BETA")
	GA    = prerelease("")
)

// FeatureGate parses and stores flag gates for known features from
// a string like feature1=true,feature2=false,...
type FeatureGate interface {
	// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
	AddFlag(fs *pflag.FlagSet)
	// Set parses and stores flag gates for known features
	// from a string like feature1=true,feature2=false,...
	Set(value string) error
	// Enabled returns true if the key is enabled.
	Enabled(key Feature) bool
	// Add adds features to the featureGate.
	Add(features map[Feature]FeatureSpec) error
	// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
	KnownFeatures() []string
}

// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
type featureGate struct {
	lock sync.RWMutex

	known   map[Feature]FeatureSpec
	special map[Feature]func(*featureGate, bool)
	enabled map[Feature]bool

	// is set to true when AddFlag is called. Note: initialization is not go-routine safe, lookup is
	closed bool
}

func setUnsetAlphaGates(f *featureGate, val bool) {
	for k, v := range f.known {
		if v.PreRelease == Alpha {
			if _, found := f.enabled[k]; !found {
				f.enabled[k] = val
			}
		}
	}
}

// Set, String, and Type implement pflag.Value
var _ pflag.Value = &featureGate{}

func NewFeatureGate() *featureGate {
	f := &featureGate{
		known:   map[Feature]FeatureSpec{},
		special: specialFeatures,
		enabled: map[Feature]bool{},
	}
	for k, v := range defaultFeatures {
		f.known[k] = v
	}
	return f
}

// Set Parses a string of the form "key1=value1,key2=value2,..." into a
// map[string]bool of known keys or returns an error.
func (f *featureGate) Set(value string) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	for _, s := range strings.Split(value, ",") {
		if len(s) == 0 {
			continue
		}
		arr := strings.SplitN(s, "=", 2)
		k := Feature(strings.TrimSpace(arr[0]))
		_, ok := f.known[Feature(k)]
		if !ok {
			return fmt.Errorf("unrecognized key: %s", k)
		}
		if len(arr) != 2 {
			return fmt.Errorf("missing bool value for %s", k)
		}
		v := strings.TrimSpace(arr[1])
		boolValue, err := strconv.ParseBool(v)
		if err != nil {
			return fmt.Errorf("invalid value of %s: %s, err: %v", k, v, err)
		}
		f.enabled[k] = boolValue

		// Handle "special" features like "all alpha gates"
		if fn, found := f.special[k]; found {
			fn(f, boolValue)
		}
	}

	glog.Infof("feature gates: %v", f.enabled)
	return nil
}

// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...".
func (f *featureGate) String() string {
	f.lock.RLock()
	defer f.lock.RUnlock()

	pairs := []string{}
	for k, v := range f.enabled {
		pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
	}
	sort.Strings(pairs)
	return strings.Join(pairs, ",")
}

func (f *featureGate) Type() string {
	return "mapStringBool"
}

// Add adds features to the featureGate.
func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	if f.closed {
		return fmt.Errorf("cannot add a feature gate after adding it to the flag set")
	}

	for name, spec := range features {
		if existingSpec, found := f.known[name]; found {
			if existingSpec == spec {
				continue
			}
			return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec)
		}

		f.known[name] = spec
	}
	return nil
}

// Enabled returns true if the key is enabled.
func (f *featureGate) Enabled(key Feature) bool {
	f.lock.RLock()
	defer f.lock.RUnlock()

	defaultValue := f.known[key].Default
	if f.enabled != nil {
		if v, ok := f.enabled[key]; ok {
			return v
		}
	}
	return defaultValue
}

// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
	f.lock.Lock()
	f.closed = true
	f.lock.Unlock()

	known := f.KnownFeatures()
	fs.Var(f, flagName, ""+
		"A set of key=value pairs that describe feature gates for alpha/experimental features. "+
		"Options are:\n"+strings.Join(known, "\n"))
}

// KnownFeatures returns a slice of strings describing the FeatureGate's known features.
func (f *featureGate) KnownFeatures() []string {
	f.lock.RLock()
	defer f.lock.RUnlock()
	var known []string
	for k, v := range f.known {
		pre := ""
		if v.PreRelease != GA {
			pre = fmt.Sprintf("%s - ", v.PreRelease)
		}
		known = append(known, fmt.Sprintf("%s=true|false (%sdefault=%t)", k, pre, v.Default))
	}
	sort.Strings(known)
	return known
}
<|endoftext|>


<commit_before>package server

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/bouk/httprouter"
	"github.com/influxdata/chronograf"
	"github.com/influxdata/chronograf/uuid"
)

// ValidTemplateRequest checks if the request sent to the server is the correct format.
func ValidTemplateRequest(template *chronograf.Template) error {
	switch template.Type {
	default:
		return fmt.Errorf("Unknown template type %s", template.Type)
	case "query", "constant", "csv", "fieldKeys", "tagKeys", "tagValues":
	}

	for _, v := range template.Values {
		switch v.Type {
		default:
			return fmt.Errorf("Unknown template variable type %s", v.Type)
		case "csv", "fieldKey", "tagKey", "tagValue":
		}
	}

	if template.Type == "query" && template.Query == nil {
		return fmt.Errorf("No query set for template of type 'query'")
	}
	return nil
}

type templateLinks struct {
	Self string `json:"self"` // Self link mapping to this resource
}

type templateResponse struct {
	chronograf.Template
	Links templateLinks `json:"links"`
}

func newTemplateResponses(dID chronograf.DashboardID, tmps []chronograf.Template) []templateResponse {
	res := make([]templateResponse, len(tmps))
	for i, t := range tmps {
		res[i] = newTemplateResponse(dID, t)
	}
	return res
}

type templatesResponses struct {
	Templates []templateResponse `json:"templates"`
}

func newTemplateResponse(dID chronograf.DashboardID, tmp chronograf.Template) templateResponse {
	base := "/chronograf/v1/dashboards"
	return templateResponse{
		Template: tmp,
		Links: templateLinks{
			Self: fmt.Sprintf("%s/%d/templates/%s", base, dID, tmp.ID),
		},
	}
}

// Templates returns all templates from a dashboard within the store
func (s *Service) Templates(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	d, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	res := templatesResponses{
		Templates: newTemplateResponses(chronograf.DashboardID(id), d.Templates),
	}
	encodeJSON(w, http.StatusOK, res, s.Logger)
}

// NewTemplate adds a template to an existing dashboard
func (s *Service) NewTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	var template chronograf.Template
	if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
		invalidJSON(w, s.Logger)
		return
	}

	if err := ValidTemplateRequest(&template); err != nil {
		invalidData(w, err, s.Logger)
		return
	}

	ids := uuid.V4{}
	tid, err := ids.Generate()
	if err != nil {
		msg := fmt.Sprintf("Error creating template ID for dashboard %d: %v", id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}
	template.ID = chronograf.TemplateID(tid)

	dash.Templates = append(dash.Templates, template)
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error adding template %s to dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	res := newTemplateResponse(dash.ID, template)
	encodeJSON(w, http.StatusOK, res, s.Logger)
}

// TemplateID retrieves a specific template from a dashboard
func (s *Service) TemplateID(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	for _, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			res := newTemplateResponse(chronograf.DashboardID(id), t)
			encodeJSON(w, http.StatusOK, res, s.Logger)
		}
	}

	notFound(w, id, s.Logger)
}

// RemoveTemplate removes a specific template from an existing dashboard
func (s *Service) RemoveTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	pos := -1
	for i, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			pos = i
			break
		}
	}
	if pos == -1 {
		notFound(w, id, s.Logger)
		return
	}

	dash.Templates = append(dash.Templates[:pos], dash.Templates[pos+1:]...)
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error removing template %s from dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// ReplaceTemplate replaces a template entirely within an existing dashboard
func (s *Service) ReplaceTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	pos := -1
	for i, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			pos = i
			break
		}
	}
	if pos == -1 {
		notFound(w, id, s.Logger)
		return
	}

	var template chronograf.Template
	if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
		invalidJSON(w, s.Logger)
		return
	}

	if err := ValidTemplateRequest(&template); err != nil {
		invalidData(w, err, s.Logger)
		return
	}
	template.ID = chronograf.TemplateID(tid)

	dash.Templates[pos] = template
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error updating template %s in dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	res := newTemplateResponse(chronograf.DashboardID(id), template)
	encodeJSON(w, http.StatusOK, res, s.Logger)
}
<commit_msg>Add measurements and databases to template variables<commit_after>package server

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/bouk/httprouter"
	"github.com/influxdata/chronograf"
	"github.com/influxdata/chronograf/uuid"
)

// ValidTemplateRequest checks if the request sent to the server is the correct format.
func ValidTemplateRequest(template *chronograf.Template) error {
	switch template.Type {
	default:
		return fmt.Errorf("Unknown template type %s", template.Type)
	case "query", "constant", "csv", "fieldKeys", "tagKeys", "tagValues", "measurements", "databases":
	}

	for _, v := range template.Values {
		switch v.Type {
		default:
			return fmt.Errorf("Unknown template variable type %s", v.Type)
		case "csv", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant":
		}
	}

	if template.Type == "query" && template.Query == nil {
		return fmt.Errorf("No query set for template of type 'query'")
	}

	return nil
}

type templateLinks struct {
	Self string `json:"self"` // Self link mapping to this resource
}

type templateResponse struct {
	chronograf.Template
	Links templateLinks `json:"links"`
}

func newTemplateResponses(dID chronograf.DashboardID, tmps []chronograf.Template) []templateResponse {
	res := make([]templateResponse, len(tmps))
	for i, t := range tmps {
		res[i] = newTemplateResponse(dID, t)
	}
	return res
}

type templatesResponses struct {
	Templates []templateResponse `json:"templates"`
}

func newTemplateResponse(dID chronograf.DashboardID, tmp chronograf.Template) templateResponse {
	base := "/chronograf/v1/dashboards"
	return templateResponse{
		Template: tmp,
		Links: templateLinks{
			Self: fmt.Sprintf("%s/%d/templates/%s", base, dID, tmp.ID),
		},
	}
}

// Templates returns all templates from a dashboard within the store
func (s *Service) Templates(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	d, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	res := templatesResponses{
		Templates: newTemplateResponses(chronograf.DashboardID(id), d.Templates),
	}
	encodeJSON(w, http.StatusOK, res, s.Logger)
}

// NewTemplate adds a template to an existing dashboard
func (s *Service) NewTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	var template chronograf.Template
	if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
		invalidJSON(w, s.Logger)
		return
	}

	if err := ValidTemplateRequest(&template); err != nil {
		invalidData(w, err, s.Logger)
		return
	}

	ids := uuid.V4{}
	tid, err := ids.Generate()
	if err != nil {
		msg := fmt.Sprintf("Error creating template ID for dashboard %d: %v", id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}
	template.ID = chronograf.TemplateID(tid)

	dash.Templates = append(dash.Templates, template)
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error adding template %s to dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	res := newTemplateResponse(dash.ID, template)
	encodeJSON(w, http.StatusOK, res, s.Logger)
}

// TemplateID retrieves a specific template from a dashboard
func (s *Service) TemplateID(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	for _, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			res := newTemplateResponse(chronograf.DashboardID(id), t)
			encodeJSON(w, http.StatusOK, res, s.Logger)
			return
		}
	}

	notFound(w, id, s.Logger)
}

// RemoveTemplate removes a specific template from an existing dashboard
func (s *Service) RemoveTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	pos := -1
	for i, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			pos = i
			break
		}
	}
	if pos == -1 {
		notFound(w, id, s.Logger)
		return
	}

	dash.Templates = append(dash.Templates[:pos], dash.Templates[pos+1:]...)
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error removing template %s from dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// ReplaceTemplate replaces a template entirely within an existing dashboard
func (s *Service) ReplaceTemplate(w http.ResponseWriter, r *http.Request) {
	id, err := paramID("id", r)
	if err != nil {
		Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
		return
	}

	ctx := r.Context()
	dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
	if err != nil {
		notFound(w, id, s.Logger)
		return
	}

	tid := httprouter.GetParamFromContext(ctx, "tid")
	pos := -1
	for i, t := range dash.Templates {
		if t.ID == chronograf.TemplateID(tid) {
			pos = i
			break
		}
	}
	if pos == -1 {
		notFound(w, id, s.Logger)
		return
	}

	var template chronograf.Template
	if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
		invalidJSON(w, s.Logger)
		return
	}

	if err := ValidTemplateRequest(&template); err != nil {
		invalidData(w, err, s.Logger)
		return
	}
	template.ID = chronograf.TemplateID(tid)

	dash.Templates[pos] = template
	if err := s.DashboardsStore.Update(ctx, dash); err != nil {
		msg := fmt.Sprintf("Error updating template %s in dashboard %d: %v", tid, id, err)
		Error(w, http.StatusInternalServerError, msg, s.Logger)
		return
	}

	res := newTemplateResponse(chronograf.DashboardID(id), template)
	encodeJSON(w, http.StatusOK, res, s.Logger)
}
<|endoftext|>


<commit_before>package main

import (
	"github.com/phzfi/RIC/server/ops"
	"github.com/phzfi/RIC/server/config"
	"github.com/phzfi/RIC/server/logging"
	"github.com/valyala/fasthttp"
	"path/filepath"
	"strings"
)

func ExtToFormat(ext string) string {
	ext = strings.ToUpper(strings.TrimLeft(ext, "."))
	if ext == "JPG" {
		return "JPEG"
	}
	return ext
}

func ParseURI(uri *fasthttp.URI, source ops.ImageSource, marker ops.Watermarker, conf config.Conf) (operations []ops.Operation, err error) {
	args := uri.QueryArgs()
	filename := string(uri.Path())
	w, werr := args.GetUint("width")
	h, herr := args.GetUint("height")
	ow, oh, err := source.ImageSize(filename)
	if err != nil {
		return
	}
	mode := string(args.Peek("mode"))

	operations = []ops.Operation{source.LoadImageOp(filename)}

	adjustWidth := func() {
		w = int(float32(h*ow)/float32(oh) + 0.5)
	}

	adjustHeight := func() {
		h = int(float32(w*oh)/float32(ow) + 0.5)
	}

	adjustSize := func() {
		if herr != nil && werr == nil {
			adjustHeight()
		} else if herr == nil && werr != nil {
			adjustWidth()
		} else if werr != nil && herr != nil {
			w, h = ow, oh
		}
	}

	denyUpscale := func() {
		if w > ow {
			w = ow
			adjustHeight()
		}
		if h > oh {
			h = oh
			adjustWidth()
		}
	}

	resize := func() {
		denyUpscale()
		adjustSize()
		operations = append(operations, ops.Resize{w, h})
	}

	liquid := func() {
		denyUpscale()
		adjustSize()
		operations = append(operations, ops.LiquidRescale{w, h})
	}

	fit := func() {
		if werr == nil && herr == nil {
			denyUpscale()
			if ow*h > w*oh {
				adjustHeight()
			} else {
				adjustWidth()
			}
			operations = append(operations, ops.Resize{w, h})
		} else {
			resize()
		}
	}

	watermark := func() {
		heightOK := h > marker.Minheight && h < marker.Maxheight
		widthOK := w > marker.Minwidth && w < marker.Maxwidth
		if marker.AddMark && heightOK && widthOK {
			logging.Debug("Adding watermarkOp")
			operations = append(operations, ops.WatermarkOp(marker.WatermarkImage, marker.Horizontal, marker.Vertical))
		}
	}

	switch mode {
	case "resize":
		resize()
	case "fit":
		fit()
	case "liquid":
		liquid()
	default:
		resize()
	}
	watermark()

	ext := filepath.Ext(filename)
	if ext != "" {
		operations = append(operations, ops.Convert{ExtToFormat(ext)})
	}

	return
}
<commit_msg>No FPU for integer math anymore<commit_after>package main

import (
	"github.com/phzfi/RIC/server/config"
	"github.com/phzfi/RIC/server/logging"
	"github.com/phzfi/RIC/server/ops"
	"github.com/valyala/fasthttp"
	"path/filepath"
	"strings"
)

func ExtToFormat(ext string) string {
	ext = strings.ToUpper(strings.TrimLeft(ext, "."))
	if ext == "JPG" {
		return "JPEG"
	}
	return ext
}

func ParseURI(uri *fasthttp.URI, source ops.ImageSource, marker ops.Watermarker, conf config.Conf) (operations []ops.Operation, err error) {
	args := uri.QueryArgs()
	filename := string(uri.Path())
	w, werr := args.GetUint("width")
	h, herr := args.GetUint("height")
	ow, oh, err := source.ImageSize(filename)
	if err != nil {
		return
	}
	mode := string(args.Peek("mode"))

	operations = []ops.Operation{source.LoadImageOp(filename)}

	adjustWidth := func() {
		w = roundedIntegerDivision(h*ow, oh)
	}

	adjustHeight := func() {
		h = roundedIntegerDivision(w*oh, ow)
	}

	adjustSize := func() {
		if herr != nil && werr == nil {
			adjustHeight()
		} else if herr == nil && werr != nil {
			adjustWidth()
		} else if werr != nil && herr != nil {
			w, h = ow, oh
		}
	}

	denyUpscale := func() {
		if w > ow {
			w = ow
			adjustHeight()
		}
		if h > oh {
			h = oh
			adjustWidth()
		}
	}

	resize := func() {
		denyUpscale()
		adjustSize()
		operations = append(operations, ops.Resize{w, h})
	}

	liquid := func() {
		denyUpscale()
		adjustSize()
		operations = append(operations, ops.LiquidRescale{w, h})
	}

	fit := func() {
		if werr == nil && herr == nil {
			denyUpscale()
			if ow*h > w*oh {
				adjustHeight()
			} else {
				adjustWidth()
			}
			operations = append(operations, ops.Resize{w, h})
		} else {
			resize()
		}
	}

	watermark := func() {
		heightOK := h > marker.Minheight && h < marker.Maxheight
		widthOK := w > marker.Minwidth && w < marker.Maxwidth
		if marker.AddMark && heightOK && widthOK {
			logging.Debug("Adding watermarkOp")
			operations = append(operations, ops.WatermarkOp(marker.WatermarkImage, marker.Horizontal, marker.Vertical))
		}
	}

	switch mode {
	case "resize":
		resize()
	case "fit":
		fit()
	case "liquid":
		liquid()
	default:
		resize()
	}
	watermark()

	ext := filepath.Ext(filename)
	if ext != "" {
		operations = append(operations, ops.Convert{ExtToFormat(ext)})
	}

	return
}

func roundedIntegerDivision(n, m int) int {
	if n < 0 == m < 0 {
		return (n + m/2) / m
	} else { // -5 / 6 should round to -1
		return (n - m/2) / m
	}
}
<|endoftext|>


<commit_before>// Package bitcoind is a package
package method

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

type PeerInfoArgs struct {
}

type PeerInfo struct {
	Id             int               `json:"id"`
	Addr           string            `json:"addr"`
	AddrLocal      string            `json:"addrlocal"`
	Services       string            `json:"services"`
	RelayTxes      bool              `json:"relaytxes"`
	LastSend       timestamp.Unix    `json:"lastsend"`
	LastRecv       timestamp.Unix    `json:"lastrecv"`
	BytesSent      int               `json:"bytessent"`
	BytesRecv      int               `json:"bytesrecv"`
	ConnTime       timestamp.Unix    `json:"conntime"`
	TimeOffset     int               `json:"timeoffset"`
	PingTime       float32           `json:"pingtime"`
	MinPing        float64           `json:"minping"`
	Version        uint              `json:"version"`
	Subver         string            `json:"subver"`
	Inbout         bool              `json:"inbound"`
	AddNode        bool              `json:"addnode"`
	StartHeight    uint              `json:"startingheight"`
	BanScore       uint              `json:"banscore"`
	SynchedHeaders int               `json:"synced_headers"`
	SynchedBlocks  int               `json:"synced_blocks"`
	Whitelisted    bool              `json:"whitelisted"`
	BytesReceived  PeerInfoBytesSent `json:"bytessent_per_msg"`
	BytesSend      PeerInfoBytesSent `json:"bytesrecv_per_msg"`
}

type PeerInfoBytesSent struct {
	Addr        uint `json:"addr"`
	FeeFilter   uint `json:"feefilter"`
	Inv         uint `json:"inv"`
	Ping        uint `json:"ping"`
	Pont        uint `json:"pong"`
	SendCmpct   uint `json:"sendcmpct"`
	SendHeaders uint `json:"sendheaders"`
	VerAck      uint `json:"verack"`
	Version     uint `json:"version"`
}

type PeerInfoBytesReceived struct {
	Addr    uint `json:"addr"`
	Inv     uint `json:"inv"`
	Ping    uint `json:"ping"`
	Pong    uint `json:"pong"`
	VerAck  uint `json:"verack"`
	Version uint `json:"version"`
}

func GetPeerInfo(client *rpc.RPCClient) ([]PeerInfo, error) {
	response, err := client.Post("getpeerinfo", PeerInfoArgs{})

	if err != nil {
		return nil, err
	}

	defer response.Body.Close()

	var result []PeerInfo
	err = json2.DecodeClientResponse(response.Body, &result)

	if err != nil {
		return nil, err
	}

	return result, nil
}
<commit_msg>StartHeight might be negative sometimes so we cannot use uint<commit_after>// Package bitcoind is a package
package method

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

type PeerInfoArgs struct {
}

type PeerInfo struct {
	Id             int               `json:"id"`
	Addr           string            `json:"addr"`
	AddrLocal      string            `json:"addrlocal"`
	Services       string            `json:"services"`
	RelayTxes      bool              `json:"relaytxes"`
	LastSend       timestamp.Unix    `json:"lastsend"`
	LastRecv       timestamp.Unix    `json:"lastrecv"`
	BytesSent      int               `json:"bytessent"`
	BytesRecv      int               `json:"bytesrecv"`
	ConnTime       timestamp.Unix    `json:"conntime"`
	TimeOffset     int               `json:"timeoffset"`
	PingTime       float32           `json:"pingtime"`
	MinPing        float64           `json:"minping"`
	Version        uint              `json:"version"`
	Subver         string            `json:"subver"`
	Inbout         bool              `json:"inbound"`
	AddNode        bool              `json:"addnode"`
	StartHeight    int               `json:"startingheight"`
	BanScore       uint              `json:"banscore"`
	SynchedHeaders int               `json:"synced_headers"`
	SynchedBlocks  int               `json:"synced_blocks"`
	Whitelisted    bool              `json:"whitelisted"`
	BytesReceived  PeerInfoBytesSent `json:"bytessent_per_msg"`
	BytesSend      PeerInfoBytesSent `json:"bytesrecv_per_msg"`
}

type PeerInfoBytesSent struct {
	Addr        uint `json:"addr"`
	FeeFilter   uint `json:"feefilter"`
	Inv         uint `json:"inv"`
	Ping        uint `json:"ping"`
	Pont        uint `json:"pong"`
	SendCmpct   uint `json:"sendcmpct"`
	SendHeaders uint `json:"sendheaders"`
	VerAck      uint `json:"verack"`
	Version     uint `json:"version"`
}

type PeerInfoBytesReceived struct {
	Addr    uint `json:"addr"`
	Inv     uint `json:"inv"`
	Ping    uint `json:"ping"`
	Pong    uint `json:"pong"`
	VerAck  uint `json:"verack"`
	Version uint `json:"version"`
}

func GetPeerInfo(client *rpc.RPCClient) ([]PeerInfo, error) {
	response, err := client.Post("getpeerinfo", PeerInfoArgs{})

	if err != nil {
		return nil, err
	}

	defer response.Body.Close()

	var result []PeerInfo
	err = json2.DecodeClientResponse(response.Body, &result)

	if err != nil {
		return nil, err
	}

	return result, nil
}
<|endoftext|>


<commit_before>package build

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/jkomoros/boardgame/boardgame-util/lib/config"
	"github.com/jkomoros/boardgame/boardgame-util/lib/path"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const staticSubFolder = "static"
const configJsFileName = "config.js"
const gameSrcSubFolder = "game-src"
const clientSubFolder = "client"

//The path, relative to goPath, where all of the files are to copy
const staticServerPackage = "github.com/jkomoros/boardgame/server/static/webapp"

var filesToLink []string = []string{
	"bower.json",
	"firebase.json",
	"polymer.json",
	"manifest.json",
	"index.html",
	"src",
	"bower_components",
}

//SimpleStaticServer creates and runs a simple static server. directory is the
//folder that the `static` folder is contained within. If no error is
//returned, Runs until the program exits.
func SimpleStaticServer(directory string, port string) error {

	staticPath := filepath.Join(directory, staticSubFolder)

	if _, err := os.Stat(staticPath); os.IsNotExist(err) {
		return errors.New(staticPath + " does not exist")
	}

	fs := http.FileServer(http.Dir(staticPath))

	infos, err := ioutil.ReadDir(staticPath)

	if err != nil {
		return errors.New("Couldn't enumerate items in serving path")
	}

	//Install specific handlers for each existing file or directory in the
	//path to serve.
	for _, info := range infos {
		if info.Name() == "index.html" {
			continue
		}
		name := "/" + info.Name()

		if info.IsDir() {
			name += "/"
		} else {

			//Need to check if the file is a symlink to a directory, and symnlinks to directory
			//don't report as a directory in info.
			resolvedPath, err := filepath.EvalSymlinks(filepath.Join(staticPath, info.Name()))
			if err == nil {
				if info, err := os.Stat(resolvedPath); err == nil {
					if info.IsDir() {
						name += "/"
					}
				}
			}
		}

		http.Handle(name, http.StripPrefix(name, fs))
	}

	//This pattern will match as fallback (it's the shortest), and should
	//return "index.html" for everythign that doesn't match one of the ones
	//already returned.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		//Safe to use since "index.html" is not provided by user but is a constant
		http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
	})

	return http.ListenAndServe(":"+port, nil)

}

//Static creates a folder of static resources for serving within the static
//subfolder of directory. It symlinks necessary resources in. The return value
//is the directory where the assets can be served from, and an error if there
//was an error. You can clean up the created folder structure with CleanStatic.
func Static(directory string, managers []string, c *config.Config) (assetRoot string, err error) {

	if err := ensureBowerComponents(); err != nil {
		return "", errors.New("bower_components couldn't be created: " + err.Error())
	}

	if _, err := os.Stat(directory); os.IsNotExist(err) {
		return "", errors.New(directory + " did not already exist.")
	}

	staticDir := filepath.Join(directory, staticSubFolder)

	if _, err := os.Stat(staticDir); os.IsNotExist(err) {
		if err := os.Mkdir(staticDir, 0700); err != nil {
			return "", errors.New("Couldn't create static directory: " + err.Error())
		}
	}

	fullPkgPath, err := path.AbsoluteGoPkgPath(staticServerPackage)

	if err != nil {
		return "", errors.New("Couldn't get full package path: " + err.Error())
	}

	//TODO: some of the config files should be copied not symlinked; some of
	//these folders will stay around. Maybe take a temp parameter about
	//whehter it should do copying or not.

	workingDirectory, err := os.Getwd()

	if err != nil {
		return "", errors.New("Can't get working directory: " + err.Error())
	}

	for _, name := range filesToLink {
		localPath := filepath.Join(staticDir, name)
		absLocalDirPath := filepath.Join(workingDirectory, staticDir) + string(filepath.Separator)
		absRemotePath := filepath.Join(fullPkgPath, name)

		relRemotePath, err := path.RelativizePaths(absLocalDirPath, absRemotePath)

		rejoinedPath := filepath.Join(absLocalDirPath, relRemotePath)

		if _, err := os.Stat(rejoinedPath); os.IsNotExist(err) {

			if strings.Contains(name, "bower") {
				return "", errors.New("bower_components doesn't appear to exist. You may need to run `bower update` from within `boardgame/server/static/webapp`.")
			}

			return "", errors.New("Unexpected error: relRemotePath of " + relRemotePath + " doesn't exist " + absLocalDirPath + " : " + absRemotePath + "(" + rejoinedPath + ")")
		}

		if err != nil {
			return "", errors.New("Couldn't relativize paths: " + err.Error())
		}

		if _, err := os.Stat(localPath); err == nil {
			//Must already exist, so can skip
			continue
		}
		fmt.Println("Linking " + localPath + " to " + relRemotePath)
		if err := os.Symlink(relRemotePath, localPath); err != nil {
			return "", errors.New("Couldn't link " + name + ": " + err.Error())
		}

	}

	fmt.Println("Creating " + configJsFileName)
	if err := createConfigJs(filepath.Join(staticDir, configJsFileName), c); err != nil {
		return "", errors.New("Couldn't create " + configJsFileName + ": " + err.Error())
	}

	fmt.Println("Creating " + gameSrcSubFolder)
	if err := linkGameClientFolders(staticDir, managers); err != nil {
		return "", errors.New("Couldn't create " + gameSrcSubFolder + ": " + err.Error())
	}

	return staticDir, nil

}

//ensureBowerComoonents ensures that
//`$GOPATH/src/github.com/jkomoros/boardgame/server/static/webapp` has bower
//components.
func ensureBowerComponents() error {

	p, err := path.AbsoluteGoPkgPath(staticServerPackage)

	if err != nil {
		return err
	}

	if _, err := os.Stat(filepath.Join(p, "bower_components")); err == nil {
		//It appears to exist, we're fine!
		return nil
	}

	_, err = exec.LookPath("bower")

	if err != nil {
		return errors.New("bower_components didn't exist and bower didn't appear to be installed. You need to install bower.")
	}

	cmd := exec.Command("bower", "update")
	cmd.Dir = p
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	fmt.Println("bower_components didn't exist, running `bower update`...")
	if err := cmd.Run(); err != nil {
		return errors.New("Couldn't `bower update`: " + err.Error())
	}

	return nil

}

//linkGameClientFolders creates a game-src within basePath and then links the
//client folders for each one.
func linkGameClientFolders(basePath string, managers []string) error {

	if _, err := os.Stat(basePath); os.IsNotExist(err) {
		return errors.New(basePath + " doesn't exist")
	}

	gameSrcDir := filepath.Join(basePath, gameSrcSubFolder)

	if _, err := os.Stat(gameSrcDir); os.IsNotExist(err) {
		if err := os.Mkdir(gameSrcDir, 0700); err != nil {
			return errors.New("Couldn't create game-src directory: " + err.Error())
		}
	}

	workingDirectory, err := os.Getwd()

	if err != nil {
		return errors.New("Can't get working directory: " + err.Error())
	}

	for _, manager := range managers {
		absPkgPath, err := path.AbsoluteGoPkgPath(manager)

		if err != nil {
			return errors.New("Couldn't generate absPkgPath for " + manager + ": " + err.Error())
		}

		pkgShortName := filepath.Base(manager)

		absClientPath := filepath.Join(absPkgPath, clientSubFolder, pkgShortName)

		if _, err := os.Stat(absClientPath); os.IsNotExist(err) {
			fmt.Println("Skipping " + manager + " because it doesn't appear to have a client sub-directory")
			continue
		}

		relLocalPath := filepath.Join(gameSrcDir, pkgShortName)

		//This feels like it should be relLocalPath, but it needs to be
		//gameSrcDir, otherwise there's an extra ".." in the path. Not really
		//sure why. :-/
		absLocalPath := filepath.Join(workingDirectory, gameSrcDir)

		relPath, err := path.RelativizePaths(absLocalPath, absClientPath)

		if err != nil {
			return errors.New("Couldn't relativize path: " + err.Error())
		}

		rejoinedPath := filepath.Join(absLocalPath, relPath)

		if _, err := os.Stat(rejoinedPath); os.IsNotExist(err) {
			return errors.New("Unexpected error: relPath of " + relPath + " doesn't exist " + absLocalPath + " : " + absClientPath + "(" + rejoinedPath + ")")
		}

		if _, err := os.Stat(relLocalPath); err == nil {
			//Must already exist, so can skip
			continue
		}

		fmt.Println("Linking " + relLocalPath + " to " + relPath)
		if err := os.Symlink(relPath, relLocalPath); err != nil {
			return errors.New("Couldn't create sym lnk for " + manager + ": " + relPath + ":: " + relLocalPath)
		}

	}

	return nil

}

func createConfigJs(path string, c *config.Config) error {
	client := c.Client(false)

	clientBlob, err := json.MarshalIndent(client, "", "\t")

	if err != nil {
		return errors.New("Couldn't create blob: " + err.Error())
	}

	fileContents := "var CONFIG = " + string(clientBlob)

	if err := ioutil.WriteFile(path, []byte(fileContents), 0644); err != nil {
		return errors.New("Couldn't create file: " + err.Error())
	}

	return nil

}

//CleanStatic removes all of the things created in the static subfolder within
//directory.
func CleanStatic(directory string) error {
	return os.RemoveAll(filepath.Join(directory, staticSubFolder))
}
<commit_msg>Switched to an exclusion model for which files to copy from build.Static(). This gets images, serviceworker.js, etc. But SERVER STILL DOESN'T WORK for some reason. Part of #655.<commit_after>package build

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/jkomoros/boardgame/boardgame-util/lib/config"
	"github.com/jkomoros/boardgame/boardgame-util/lib/path"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const staticSubFolder = "static"
const configJsFileName = "config.js"
const gameSrcSubFolder = "game-src"
const clientSubFolder = "client"

//The path, relative to goPath, where all of the files are to copy
const staticServerPackage = "github.com/jkomoros/boardgame/server/static/webapp"

var filesToExclude map[string]bool = map[string]bool{
	".gitignore": true,
	"README.md":  true,
	//Don't copy over because we'll generate our own; if we copy over and
	//generate our own we'll overwrite original.
	"config.js": true,
}

//SimpleStaticServer creates and runs a simple static server. directory is the
//folder that the `static` folder is contained within. If no error is
//returned, Runs until the program exits.
func SimpleStaticServer(directory string, port string) error {

	staticPath := filepath.Join(directory, staticSubFolder)

	if _, err := os.Stat(staticPath); os.IsNotExist(err) {
		return errors.New(staticPath + " does not exist")
	}

	fs := http.FileServer(http.Dir(staticPath))

	infos, err := ioutil.ReadDir(staticPath)

	if err != nil {
		return errors.New("Couldn't enumerate items in serving path")
	}

	//Install specific handlers for each existing file or directory in the
	//path to serve.
	for _, info := range infos {
		if info.Name() == "index.html" {
			continue
		}
		name := "/" + info.Name()

		if info.IsDir() {
			name += "/"
		} else {

			//Need to check if the file is a symlink to a directory, and symnlinks to directory
			//don't report as a directory in info.
			resolvedPath, err := filepath.EvalSymlinks(filepath.Join(staticPath, info.Name()))
			if err == nil {
				if info, err := os.Stat(resolvedPath); err == nil {
					if info.IsDir() {
						name += "/"
					}
				}
			}
		}

		http.Handle(name, http.StripPrefix(name, fs))
	}

	//This pattern will match as fallback (it's the shortest), and should
	//return "index.html" for everythign that doesn't match one of the ones
	//already returned.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		//Safe to use since "index.html" is not provided by user but is a constant
		http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
	})

	return http.ListenAndServe(":"+port, nil)

}

//Static creates a folder of static resources for serving within the static
//subfolder of directory. It symlinks necessary resources in. The return value
//is the directory where the assets can be served from, and an error if there
//was an error. You can clean up the created folder structure with CleanStatic.
func Static(directory string, managers []string, c *config.Config) (assetRoot string, err error) {

	if err := ensureBowerComponents(); err != nil {
		return "", errors.New("bower_components couldn't be created: " + err.Error())
	}

	if _, err := os.Stat(directory); os.IsNotExist(err) {
		return "", errors.New(directory + " did not already exist.")
	}

	staticDir := filepath.Join(directory, staticSubFolder)

	if _, err := os.Stat(staticDir); os.IsNotExist(err) {
		if err := os.Mkdir(staticDir, 0700); err != nil {
			return "", errors.New("Couldn't create static directory: " + err.Error())
		}
	}

	fullPkgPath, err := path.AbsoluteGoPkgPath(staticServerPackage)

	if err != nil {
		return "", errors.New("Couldn't get full package path: " + err.Error())
	}

	//TODO: some of the config files should be copied not symlinked; some of
	//these folders will stay around. Maybe take a temp parameter about
	//whehter it should do copying or not.

	workingDirectory, err := os.Getwd()

	if err != nil {
		return "", errors.New("Can't get working directory: " + err.Error())
	}

	infos, err := ioutil.ReadDir(fullPkgPath)

	if err != nil {
		return "", errors.New("Couldn't list files in remote directory: " + err.Error())
	}

	for _, info := range infos {

		name := info.Name()

		if filesToExclude[name] {
			continue
		}

		localPath := filepath.Join(staticDir, name)
		absLocalDirPath := filepath.Join(workingDirectory, staticDir) + string(filepath.Separator)
		absRemotePath := filepath.Join(fullPkgPath, name)

		relRemotePath, err := path.RelativizePaths(absLocalDirPath, absRemotePath)

		rejoinedPath := filepath.Join(absLocalDirPath, relRemotePath)

		if _, err := os.Stat(rejoinedPath); os.IsNotExist(err) {

			if strings.Contains(name, "bower") {
				return "", errors.New("bower_components doesn't appear to exist. You may need to run `bower update` from within `boardgame/server/static/webapp`.")
			}

			return "", errors.New("Unexpected error: relRemotePath of " + relRemotePath + " doesn't exist " + absLocalDirPath + " : " + absRemotePath + "(" + rejoinedPath + ")")
		}

		if err != nil {
			return "", errors.New("Couldn't relativize paths: " + err.Error())
		}

		if _, err := os.Stat(localPath); err == nil {
			//Must already exist, so can skip
			continue
		}
		fmt.Println("Linking " + localPath + " to " + relRemotePath)
		if err := os.Symlink(relRemotePath, localPath); err != nil {
			return "", errors.New("Couldn't link " + name + ": " + err.Error())
		}

	}

	fmt.Println("Creating " + configJsFileName)
	if err := createConfigJs(filepath.Join(staticDir, configJsFileName), c); err != nil {
		return "", errors.New("Couldn't create " + configJsFileName + ": " + err.Error())
	}

	fmt.Println("Creating " + gameSrcSubFolder)
	if err := linkGameClientFolders(staticDir, managers); err != nil {
		return "", errors.New("Couldn't create " + gameSrcSubFolder + ": " + err.Error())
	}

	return staticDir, nil

}

//ensureBowerComoonents ensures that
//`$GOPATH/src/github.com/jkomoros/boardgame/server/static/webapp` has bower
//components.
func ensureBowerComponents() error {

	p, err := path.AbsoluteGoPkgPath(staticServerPackage)

	if err != nil {
		return err
	}

	if _, err := os.Stat(filepath.Join(p, "bower_components")); err == nil {
		//It appears to exist, we're fine!
		return nil
	}

	_, err = exec.LookPath("bower")

	if err != nil {
		return errors.New("bower_components didn't exist and bower didn't appear to be installed. You need to install bower.")
	}

	cmd := exec.Command("bower", "update")
	cmd.Dir = p
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	fmt.Println("bower_components didn't exist, running `bower update`...")
	if err := cmd.Run(); err != nil {
		return errors.New("Couldn't `bower update`: " + err.Error())
	}

	return nil

}

//linkGameClientFolders creates a game-src within basePath and then links the
//client folders for each one.
func linkGameClientFolders(basePath string, managers []string) error {

	if _, err := os.Stat(basePath); os.IsNotExist(err) {
		return errors.New(basePath + " doesn't exist")
	}

	gameSrcDir := filepath.Join(basePath, gameSrcSubFolder)

	if _, err := os.Stat(gameSrcDir); os.IsNotExist(err) {
		if err := os.Mkdir(gameSrcDir, 0700); err != nil {
			return errors.New("Couldn't create game-src directory: " + err.Error())
		}
	}

	workingDirectory, err := os.Getwd()

	if err != nil {
		return errors.New("Can't get working directory: " + err.Error())
	}

	for _, manager := range managers {
		absPkgPath, err := path.AbsoluteGoPkgPath(manager)

		if err != nil {
			return errors.New("Couldn't generate absPkgPath for " + manager + ": " + err.Error())
		}

		pkgShortName := filepath.Base(manager)

		absClientPath := filepath.Join(absPkgPath, clientSubFolder, pkgShortName)

		if _, err := os.Stat(absClientPath); os.IsNotExist(err) {
			fmt.Println("Skipping " + manager + " because it doesn't appear to have a client sub-directory")
			continue
		}

		relLocalPath := filepath.Join(gameSrcDir, pkgShortName)

		//This feels like it should be relLocalPath, but it needs to be
		//gameSrcDir, otherwise there's an extra ".." in the path. Not really
		//sure why. :-/
		absLocalPath := filepath.Join(workingDirectory, gameSrcDir)

		relPath, err := path.RelativizePaths(absLocalPath, absClientPath)

		if err != nil {
			return errors.New("Couldn't relativize path: " + err.Error())
		}

		rejoinedPath := filepath.Join(absLocalPath, relPath)

		if _, err := os.Stat(rejoinedPath); os.IsNotExist(err) {
			return errors.New("Unexpected error: relPath of " + relPath + " doesn't exist " + absLocalPath + " : " + absClientPath + "(" + rejoinedPath + ")")
		}

		if _, err := os.Stat(relLocalPath); err == nil {
			//Must already exist, so can skip
			continue
		}

		fmt.Println("Linking " + relLocalPath + " to " + relPath)
		if err := os.Symlink(relPath, relLocalPath); err != nil {
			return errors.New("Couldn't create sym lnk for " + manager + ": " + relPath + ":: " + relLocalPath)
		}

	}

	return nil

}

func createConfigJs(path string, c *config.Config) error {
	client := c.Client(false)

	clientBlob, err := json.MarshalIndent(client, "", "\t")

	if err != nil {
		return errors.New("Couldn't create blob: " + err.Error())
	}

	fileContents := "var CONFIG = " + string(clientBlob)

	if err := ioutil.WriteFile(path, []byte(fileContents), 0644); err != nil {
		return errors.New("Couldn't create file: " + err.Error())
	}

	return nil

}

//CleanStatic removes all of the things created in the static subfolder within
//directory.
func CleanStatic(directory string) error {
	return os.RemoveAll(filepath.Join(directory, staticSubFolder))
}
<|endoftext|>