<commit_before>/*
Copyright 2014 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 cmd

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

	"github.com/golang/glog"
	"github.com/spf13/cobra"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	restclient "k8s.io/client-go/rest"
	"k8s.io/client-go/tools/remotecommand"
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	api "k8s.io/kubernetes/pkg/apis/core"
	coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
	"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/kubectl/util/i18n"
)

var (
	attachExample = templates.Examples(i18n.T(`
		# Get output from running pod 123456-7890, using the first container by default
		kubectl attach 123456-7890

		# Get output from ruby-container from pod 123456-7890
		kubectl attach 123456-7890 -c ruby-container

		# Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
		# and sends stdout/stderr from 'bash' back to the client
		kubectl attach 123456-7890 -c ruby-container -i -t

		# Get output from the first pod of a ReplicaSet named nginx
		kubectl attach rs/nginx
		`))
)

const (
	defaultPodAttachTimeout = 60 * time.Second
	defaultPodLogsTimeout   = 20 * time.Second
)

func NewCmdAttach(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
	options := &AttachOptions{
		StreamOptions: StreamOptions{
			In:  cmdIn,
			Out: cmdOut,
			Err: cmdErr,
		},

		Attach: &DefaultRemoteAttach{},
	}
	cmd := &cobra.Command{
		Use: "attach (POD | TYPE/NAME) -c CONTAINER",
		DisableFlagsInUseLine: true,
		Short:   i18n.T("Attach to a running container"),
		Long:    "Attach to a process that is already running inside an existing container.",
		Example: attachExample,
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(options.Complete(f, cmd, args))
			cmdutil.CheckErr(options.Validate())
			cmdutil.CheckErr(options.Run())
		},
	}
	cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
	cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", "Container name. If omitted, the first container in the pod will be chosen")
	cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", false, "Pass stdin to the container")
	cmd.Flags().BoolVarP(&options.TTY, "tty", "t", false, "Stdin is a TTY")
	return cmd
}

// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
type RemoteAttach interface {
	Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
}

// DefaultRemoteAttach is the standard implementation of attaching
type DefaultRemoteAttach struct{}

func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
	exec, err := remotecommand.NewSPDYExecutor(config, method, url)
	if err != nil {
		return err
	}
	return exec.Stream(remotecommand.StreamOptions{
		Stdin:             stdin,
		Stdout:            stdout,
		Stderr:            stderr,
		Tty:               tty,
		TerminalSizeQueue: terminalSizeQueue,
	})
}

// AttachOptions declare the arguments accepted by the Exec command
type AttachOptions struct {
	StreamOptions

	CommandName string

	Pod *api.Pod

	Attach        RemoteAttach
	PodClient     coreclient.PodsGetter
	GetPodTimeout time.Duration
	Config        *restclient.Config
}

// Complete verifies command line arguments and loads data from the command environment
func (p *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []string) error {
	if len(argsIn) == 0 {
		return cmdutil.UsageErrorf(cmd, "at least 1 argument is required for attach")
	}
	if len(argsIn) > 2 {
		return cmdutil.UsageErrorf(cmd, "expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(argsIn), argsIn)
	}

	namespace, _, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
	if err != nil {
		return cmdutil.UsageErrorf(cmd, err.Error())
	}

	builder := f.NewBuilder().
		Internal().
		NamespaceParam(namespace).DefaultNamespace()

	switch len(argsIn) {
	case 1:
		builder.ResourceNames("pods", argsIn[0])
	case 2:
		builder.ResourceNames(argsIn[0], argsIn[1])
	}

	obj, err := builder.Do().Object()
	if err != nil {
		return err
	}

	attachablePod, err := f.AttachablePodForObject(obj, p.GetPodTimeout)
	if err != nil {
		return err
	}

	p.PodName = attachablePod.Name
	p.Namespace = namespace

	config, err := f.ClientConfig()
	if err != nil {
		return err
	}
	p.Config = config

	clientset, err := f.ClientSet()
	if err != nil {
		return err
	}
	p.PodClient = clientset.Core()

	if p.CommandName == "" {
		p.CommandName = cmd.CommandPath()
	}

	return nil
}

// Validate checks that the provided attach options are specified.
func (p *AttachOptions) Validate() error {
	allErrs := []error{}
	if len(p.PodName) == 0 {
		allErrs = append(allErrs, errors.New("pod name must be specified"))
	}
	if p.Out == nil || p.Err == nil {
		allErrs = append(allErrs, errors.New("both output and error output must be provided"))
	}
	if p.Attach == nil || p.PodClient == nil || p.Config == nil {
		allErrs = append(allErrs, errors.New("client, client config, and attach must be provided"))
	}
	return utilerrors.NewAggregate(allErrs)
}

// Run executes a validated remote execution against a pod.
func (p *AttachOptions) Run() error {
	if p.Pod == nil {
		pod, err := p.PodClient.Pods(p.Namespace).Get(p.PodName, metav1.GetOptions{})
		if err != nil {
			return err
		}

		if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {
			return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", pod.Status.Phase)
		}

		p.Pod = pod
		// TODO: convert this to a clean "wait" behavior
	}
	pod := p.Pod

	// check for TTY
	containerToAttach, err := p.containerToAttachTo(pod)
	if err != nil {
		return fmt.Errorf("cannot attach to the container: %v", err)
	}
	if p.TTY && !containerToAttach.TTY {
		p.TTY = false
		if p.Err != nil {
			fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
		}
	} else if !p.TTY && containerToAttach.TTY {
		// the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
		// an error "Unrecognized input header"
		p.TTY = true
	}

	// ensure we can recover the terminal while attached
	t := p.setupTTY()

	// save p.Err so we can print the command prompt message below
	stderr := p.Err

	var sizeQueue remotecommand.TerminalSizeQueue
	if t.Raw {
		if size := t.GetSize(); size != nil {
			// fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
			// screen being redrawn
			sizePlusOne := *size
			sizePlusOne.Width++
			sizePlusOne.Height++

			// this call spawns a goroutine to monitor/update the terminal size
			sizeQueue = t.MonitorSize(&sizePlusOne, size)
		}

		// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
		// true
		p.Err = nil
	}

	fn := func() error {
		restClient, err := restclient.RESTClientFor(p.Config)
		if err != nil {
			return err
		}
		// TODO: consider abstracting into a client invocation or client helper
		req := restClient.Post().
			Resource("pods").
			Name(pod.Name).
			Namespace(pod.Namespace).
			SubResource("attach")
		req.VersionedParams(&api.PodAttachOptions{
			Container: containerToAttach.Name,
			Stdin:     p.Stdin,
			Stdout:    p.Out != nil,
			Stderr:    p.Err != nil,
			TTY:       t.Raw,
		}, legacyscheme.ParameterCodec)

		return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
	}

	if !p.Quiet && stderr != nil {
		fmt.Fprintln(stderr, "If you don't see a command prompt, try pressing enter.")
	}
	if err := t.Safe(fn); err != nil {
		return err
	}

	if p.Stdin && t.Raw && pod.Spec.RestartPolicy == api.RestartPolicyAlways {
		fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name)
	}
	return nil
}

// containerToAttach returns a reference to the container to attach to, given
// by name or the first container if name is empty.
func (p *AttachOptions) containerToAttachTo(pod *api.Pod) (*api.Container, error) {
	if len(p.ContainerName) > 0 {
		for i := range pod.Spec.Containers {
			if pod.Spec.Containers[i].Name == p.ContainerName {
				return &pod.Spec.Containers[i], nil
			}
		}
		for i := range pod.Spec.InitContainers {
			if pod.Spec.InitContainers[i].Name == p.ContainerName {
				return &pod.Spec.InitContainers[i], nil
			}
		}
		return nil, fmt.Errorf("container not found (%s)", p.ContainerName)
	}

	glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
	return &pod.Spec.Containers[0], nil
}

// GetContainerName returns the name of the container to attach to, with a fallback.
func (p *AttachOptions) GetContainerName(pod *api.Pod) (string, error) {
	c, err := p.containerToAttachTo(pod)
	if err != nil {
		return "", err
	}
	return c.Name, nil
}
<commit_msg>suggest using describe cmd to list pod containers<commit_after>/*
Copyright 2014 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 cmd

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

	"github.com/golang/glog"
	"github.com/spf13/cobra"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	restclient "k8s.io/client-go/rest"
	"k8s.io/client-go/tools/remotecommand"
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	api "k8s.io/kubernetes/pkg/apis/core"
	coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
	"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/kubectl/util/i18n"
)

var (
	attachExample = templates.Examples(i18n.T(`
		# Get output from running pod 123456-7890, using the first container by default
		kubectl attach 123456-7890

		# Get output from ruby-container from pod 123456-7890
		kubectl attach 123456-7890 -c ruby-container

		# Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
		# and sends stdout/stderr from 'bash' back to the client
		kubectl attach 123456-7890 -c ruby-container -i -t

		# Get output from the first pod of a ReplicaSet named nginx
		kubectl attach rs/nginx
		`))
)

const (
	defaultPodAttachTimeout = 60 * time.Second
	defaultPodLogsTimeout   = 20 * time.Second
)

func NewCmdAttach(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
	options := &AttachOptions{
		StreamOptions: StreamOptions{
			In:  cmdIn,
			Out: cmdOut,
			Err: cmdErr,
		},

		Attach: &DefaultRemoteAttach{},
	}
	cmd := &cobra.Command{
		Use: "attach (POD | TYPE/NAME) -c CONTAINER",
		DisableFlagsInUseLine: true,
		Short:   i18n.T("Attach to a running container"),
		Long:    "Attach to a process that is already running inside an existing container.",
		Example: attachExample,
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(options.Complete(f, cmd, args))
			cmdutil.CheckErr(options.Validate())
			cmdutil.CheckErr(options.Run())
		},
	}
	cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
	cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", "Container name. If omitted, the first container in the pod will be chosen")
	cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", false, "Pass stdin to the container")
	cmd.Flags().BoolVarP(&options.TTY, "tty", "t", false, "Stdin is a TTY")
	return cmd
}

// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
type RemoteAttach interface {
	Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
}

// DefaultRemoteAttach is the standard implementation of attaching
type DefaultRemoteAttach struct{}

func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
	exec, err := remotecommand.NewSPDYExecutor(config, method, url)
	if err != nil {
		return err
	}
	return exec.Stream(remotecommand.StreamOptions{
		Stdin:             stdin,
		Stdout:            stdout,
		Stderr:            stderr,
		Tty:               tty,
		TerminalSizeQueue: terminalSizeQueue,
	})
}

// AttachOptions declare the arguments accepted by the Exec command
type AttachOptions struct {
	StreamOptions

	CommandName       string
	SuggestedCmdUsage string

	Pod *api.Pod

	Attach        RemoteAttach
	PodClient     coreclient.PodsGetter
	GetPodTimeout time.Duration
	Config        *restclient.Config
}

// Complete verifies command line arguments and loads data from the command environment
func (p *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []string) error {
	if len(argsIn) == 0 {
		return cmdutil.UsageErrorf(cmd, "at least 1 argument is required for attach")
	}
	if len(argsIn) > 2 {
		return cmdutil.UsageErrorf(cmd, "expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(argsIn), argsIn)
	}

	namespace, _, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
	if err != nil {
		return cmdutil.UsageErrorf(cmd, err.Error())
	}

	builder := f.NewBuilder().
		Internal().
		NamespaceParam(namespace).DefaultNamespace()

	switch len(argsIn) {
	case 1:
		builder.ResourceNames("pods", argsIn[0])
	case 2:
		builder.ResourceNames(argsIn[0], argsIn[1])
	}

	obj, err := builder.Do().Object()
	if err != nil {
		return err
	}

	attachablePod, err := f.AttachablePodForObject(obj, p.GetPodTimeout)
	if err != nil {
		return err
	}

	p.PodName = attachablePod.Name
	p.Namespace = namespace

	fullCmdName := ""
	cmdParent := cmd.Parent()
	if cmdParent != nil {
		fullCmdName = cmdParent.CommandPath()
	}
	if len(fullCmdName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "describe") {
		p.SuggestedCmdUsage = fmt.Sprintf("Use '%s describe pod/%s -n %s' to see all of the containers in this pod.", fullCmdName, p.PodName, p.Namespace)
	}

	config, err := f.ClientConfig()
	if err != nil {
		return err
	}
	p.Config = config

	clientset, err := f.ClientSet()
	if err != nil {
		return err
	}
	p.PodClient = clientset.Core()

	if p.CommandName == "" {
		p.CommandName = cmd.CommandPath()
	}

	return nil
}

// Validate checks that the provided attach options are specified.
func (p *AttachOptions) Validate() error {
	allErrs := []error{}
	if len(p.PodName) == 0 {
		allErrs = append(allErrs, errors.New("pod name must be specified"))
	}
	if p.Out == nil || p.Err == nil {
		allErrs = append(allErrs, errors.New("both output and error output must be provided"))
	}
	if p.Attach == nil || p.PodClient == nil || p.Config == nil {
		allErrs = append(allErrs, errors.New("client, client config, and attach must be provided"))
	}
	return utilerrors.NewAggregate(allErrs)
}

// Run executes a validated remote execution against a pod.
func (p *AttachOptions) Run() error {
	if p.Pod == nil {
		pod, err := p.PodClient.Pods(p.Namespace).Get(p.PodName, metav1.GetOptions{})
		if err != nil {
			return err
		}

		if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {
			return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", pod.Status.Phase)
		}

		p.Pod = pod
		// TODO: convert this to a clean "wait" behavior
	}
	pod := p.Pod

	// check for TTY
	containerToAttach, err := p.containerToAttachTo(pod)
	if err != nil {
		return fmt.Errorf("cannot attach to the container: %v", err)
	}
	if p.TTY && !containerToAttach.TTY {
		p.TTY = false
		if p.Err != nil {
			fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
		}
	} else if !p.TTY && containerToAttach.TTY {
		// the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
		// an error "Unrecognized input header"
		p.TTY = true
	}

	// ensure we can recover the terminal while attached
	t := p.setupTTY()

	// save p.Err so we can print the command prompt message below
	stderr := p.Err

	var sizeQueue remotecommand.TerminalSizeQueue
	if t.Raw {
		if size := t.GetSize(); size != nil {
			// fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
			// screen being redrawn
			sizePlusOne := *size
			sizePlusOne.Width++
			sizePlusOne.Height++

			// this call spawns a goroutine to monitor/update the terminal size
			sizeQueue = t.MonitorSize(&sizePlusOne, size)
		}

		// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
		// true
		p.Err = nil
	}

	fn := func() error {
		restClient, err := restclient.RESTClientFor(p.Config)
		if err != nil {
			return err
		}
		// TODO: consider abstracting into a client invocation or client helper
		req := restClient.Post().
			Resource("pods").
			Name(pod.Name).
			Namespace(pod.Namespace).
			SubResource("attach")
		req.VersionedParams(&api.PodAttachOptions{
			Container: containerToAttach.Name,
			Stdin:     p.Stdin,
			Stdout:    p.Out != nil,
			Stderr:    p.Err != nil,
			TTY:       t.Raw,
		}, legacyscheme.ParameterCodec)

		return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
	}

	if !p.Quiet && stderr != nil {
		fmt.Fprintln(stderr, "If you don't see a command prompt, try pressing enter.")
	}
	if err := t.Safe(fn); err != nil {
		return err
	}

	if p.Stdin && t.Raw && pod.Spec.RestartPolicy == api.RestartPolicyAlways {
		fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name)
	}
	return nil
}

// containerToAttach returns a reference to the container to attach to, given
// by name or the first container if name is empty.
func (p *AttachOptions) containerToAttachTo(pod *api.Pod) (*api.Container, error) {
	if len(p.ContainerName) > 0 {
		for i := range pod.Spec.Containers {
			if pod.Spec.Containers[i].Name == p.ContainerName {
				return &pod.Spec.Containers[i], nil
			}
		}
		for i := range pod.Spec.InitContainers {
			if pod.Spec.InitContainers[i].Name == p.ContainerName {
				return &pod.Spec.InitContainers[i], nil
			}
		}
		return nil, fmt.Errorf("container not found (%s)", p.ContainerName)
	}

	if len(p.SuggestedCmdUsage) > 0 {
		fmt.Fprintf(p.Err, "Defaulting container name to %s.\n", pod.Spec.Containers[0].Name)
		fmt.Fprintf(p.Err, "%s\n", p.SuggestedCmdUsage)
	}

	glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
	return &pod.Spec.Containers[0], nil
}

// GetContainerName returns the name of the container to attach to, with a fallback.
func (p *AttachOptions) GetContainerName(pod *api.Pod) (string, error) {
	c, err := p.containerToAttachTo(pod)
	if err != nil {
		return "", err
	}
	return c.Name, nil
}
<|endoftext|>


<commit_before>package plugin

//go:generate goderive .

import (
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"hash"
	"io"
	"mime"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"

	"github.com/dcos/dcos-cli/pkg/config"
	"github.com/dcos/dcos-cli/pkg/fsutil"
	"github.com/dcos/dcos-cli/pkg/httpclient"
	"github.com/pelletier/go-toml"
	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
)

// Manager retrieves the plugins available for the current cluster
// by navigating into the filesystem.
type Manager struct {
	fs      afero.Fs
	logger  *logrus.Logger
	cluster *config.Cluster
}

// NewManager returns a new plugin manager.
func NewManager(fs afero.Fs, logger *logrus.Logger) *Manager {
	return &Manager{
		fs:     fs,
		logger: logger,
	}
}

// InstallOpts are installation options for plugin resources.
type InstallOpts struct {
	// Name specify the name of the plugin.
	Name string

	// Update allows to potentially overwrite an already existing plugin of the same name.
	Update bool

	// Checksum represents a CLI plugin resource content hash.
	Checksum Checksum

	// PostInstall is a hook which can be invoked after plugin installation.
	// It is invoked right before the plugin is moved to its final location.
	PostInstall func(fs afero.Fs, pluginDir string) error

	path       string
	stagingDir string
}

// Checksum contains the hash function and the checksum we expect from a plugin.
type Checksum struct {
	Hasher hash.Hash
	Value  string
}

// Install installs a plugin from a resource.
func (m *Manager) Install(resource string, installOpts *InstallOpts) (err error) {
	// If it's a remote resource, download it first.
	m.logger.Infof("Installing plugin from %s...", resource)
	if strings.HasPrefix(resource, "https://") || strings.HasPrefix(resource, "http://") {
		installOpts.path, err = m.downloadPlugin(resource, installOpts.Checksum)
		if err != nil {
			return err
		}
	} else {
		installOpts.path = resource
	}

	// The staging dir is where the plugin will be constructed
	// before eventually getting moved to its final location.
	installOpts.stagingDir, err = afero.TempDir(m.fs, os.TempDir(), "dcos-cli")
	if err != nil {
		return err
	}

	// Build the plugin into the staging directory.
	err = m.buildPlugin(installOpts)
	if err != nil {
		return err
	}
	return m.installPlugin(installOpts)
}

// SetCluster sets the plugin manager's target cluster.
func (m *Manager) SetCluster(cluster *config.Cluster) {
	m.cluster = cluster
}

// Remove removes a plugin from the filesystem.
func (m *Manager) Remove(name string) error {
	pluginDir := filepath.Join(m.pluginsDir(), name)
	pluginDirExists, err := afero.DirExists(m.fs, pluginDir)
	if err != nil {
		return err
	}
	if !pluginDirExists {
		return fmt.Errorf("'%s' is not a plugin directory", pluginDir)
	}
	err = m.fs.RemoveAll(pluginDir)
	if err != nil {
		return err
	}
	m.logger.Infof("Removed %s as a plugin from the CLI", name)
	return nil
}

// Plugins returns the plugins associated with the current cluster.
func (m *Manager) Plugins() (plugins []*Plugin) {
	pluginDirs, err := afero.ReadDir(m.fs, m.pluginsDir())
	if err != nil {
		m.logger.Debugf("Couldn't open plugin dir: %s", err)
		return plugins
	}

	for _, pluginDir := range pluginDirs {
		if !pluginDir.IsDir() {
			continue
		}
		plugin, err := m.loadPlugin(pluginDir.Name())
		if err != nil {
			// We don't want to see the CLI failing if a single plugin is malformed.
			// We thus log the error but continue if there is an issue at that step.
			m.logger.Debugf("Couldn't load plugin: %s", err)
			continue
		}
		plugins = append(plugins, plugin)
	}
	return plugins
}

// loadPlugin loads a plugin based on its name.
func (m *Manager) loadPlugin(name string) (*Plugin, error) {
	m.logger.Infof("Loading plugin '%s'...", name)

	plugin := &Plugin{Name: name}
	pluginPath := filepath.Join(m.pluginsDir(), name, "env")
	pluginFilePath := filepath.Join(pluginPath, "plugin.toml")

	if err := m.unmarshalPlugin(plugin, pluginFilePath); err != nil {
		return nil, err
	}

	// Save a deep copy of the plugin once it's loaded from the plugin.toml file.
	persistedPlugin := &Plugin{}
	deriveDeepCopy(persistedPlugin, plugin)

	if len(plugin.Commands) == 0 {
		plugin.Commands = m.findCommands(pluginPath)
	}

	// Normalize plugin commands by putting binary full paths and description summaries.
	for i, cmd := range plugin.Commands {
		if !filepath.IsAbs(cmd.Path) {
			cmd.Path = filepath.Join(pluginPath, cmd.Path)
		}
		if cmd.Description == "" {
			cmd.Description = m.commandDescription(cmd)
		}
		plugin.Commands[i] = cmd
	}

	// Compare the normalized plugin with the saved copy to know whether or not the file should be updated.
	if !reflect.DeepEqual(persistedPlugin, plugin) {
		m.persistPlugin(plugin, pluginFilePath)
	}
	return plugin, nil
}

// findCommands discovers commands in a given directory according to conventions.
// Each command should be contained in a dedicated binary named `dcos-{command}`.
// On Windows it must have the `.exe`` extension.
func (m *Manager) findCommands(pluginDir string) (commands []Command) {
	binDir := filepath.Join(pluginDir, "bin")
	if runtime.GOOS == "windows" {
		// On Windows we check for the `bin` dir. If it doesn't exist we use the legacy `Scripts` dir.
		binDirExists, err := afero.DirExists(m.fs, binDir)
		if err != nil || !binDirExists {
			binDir = filepath.Join(pluginDir, "Scripts")
		}
	}

	m.logger.Debugf("Discovering commands in '%s'...", binDir)

	binaries, err := afero.ReadDir(m.fs, binDir)
	if err != nil {
		m.logger.Debug(err)
		return nil
	}

	for _, binary := range binaries {
		if !strings.HasPrefix(binary.Name(), "dcos-") {
			continue
		}
		cmd := Command{
			Path: filepath.Join(binDir, binary.Name()),
			Name: strings.TrimPrefix(binary.Name(), "dcos-"),
		}
		if runtime.GOOS == "windows" {
			cmd.Name = strings.TrimSuffix(cmd.Name, ".exe")
		}
		m.logger.Debugf("Discovered '%s' command", cmd.Name)
		commands = append(commands, cmd)
	}
	return commands
}

// commandDescription gets the command info summary by invoking the binary with the `--info` flag.
func (m *Manager) commandDescription(cmd Command) (desc string) {
	infoCmd, err := exec.Command(cmd.Path, cmd.Name, "--info").Output()
	if err != nil {
		m.logger.Debugf("Couldn't get info summary for the '%s' command: %s", cmd.Name, err)
	} else {
		desc = strings.TrimSpace(string(infoCmd))
	}
	return desc
}

// unmarshalPlugin unmarshals a `plugin.toml` file into a Plugin structure.
func (m *Manager) unmarshalPlugin(plugin *Plugin, path string) error {
	data, err := afero.ReadFile(m.fs, path)
	if err != nil {
		m.logger.Debugf("Couldn't open plugin.toml: %s", err)
		return nil
	}
	return toml.Unmarshal(data, plugin)
}

// persistPlugin saves a `plugin.toml` file representing the plugin.
func (m *Manager) persistPlugin(plugin *Plugin, path string) {
	pluginTOML, err := toml.Marshal(*plugin)
	if err == nil {
		err = afero.WriteFile(m.fs, path, pluginTOML, 0644)
	}
	if err != nil {
		m.logger.Debug(err)
	}
}

// pluginsDir returns the path to the plugins directory.
func (m *Manager) pluginsDir() string {
	return filepath.Join(m.cluster.Dir(), "subcommands")
}

// downloadPlugin downloads a plugin and returns the path to the temporary file it stored it to.
func (m *Manager) downloadPlugin(url string, checksum Checksum) (string, error) {
	tmpDir, err := afero.TempDir(m.fs, os.TempDir(), "dcos-cli")
	if err != nil {
		return "", err
	}

	resp, err := m.httpClient(url).Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	downloadedFilePath := filepath.Join(tmpDir, m.downloadFilename(resp))

	var respReader io.Reader
	if checksum.Hasher != nil {
		respReader = io.TeeReader(resp.Body, checksum.Hasher)
	} else {
		respReader = resp.Body
	}
	if err := fsutil.CopyReader(m.fs, respReader, downloadedFilePath, 0644); err != nil {
		return "", err
	}

	if checksum.Hasher != nil {
		m.logger.Debugf("Verifying checksum for %s...", url)
		computedChecksum := hex.EncodeToString(checksum.Hasher.Sum(nil))
		if computedChecksum != checksum.Value {
			return "", fmt.Errorf("computed checksum %s for %s, expected %s", computedChecksum, url, checksum.Value)
		}
	}

	return downloadedFilePath, nil
}

// downloadFilename picks a filename for the resource to download. It first reads the
// `Content-Disposition` header, when not set it defaults to the URL path basename.
func (m *Manager) downloadFilename(resp *http.Response) string {
	contentDisposition := resp.Header.Get("Content-Disposition")
	_, dispositionParams, err := mime.ParseMediaType(contentDisposition)
	if err != nil {
		m.logger.Debug(err)
	} else if filename, ok := dispositionParams["filename"]; ok {
		return filename
	}
	return path.Base(resp.Request.URL.Path)
}

// buildPlugin constructs the plugin structure within a target directory, based on a path to a plugin resource.
func (m *Manager) buildPlugin(installOpts *InstallOpts) error {
	plugin := &Plugin{}

	// Detect the plugin resource media type.
	contentType, err := fsutil.DetectMediaType(m.fs, installOpts.path)
	if err != nil {
		return err
	}

	envDir := filepath.Join(installOpts.stagingDir, "env")

	switch contentType {
	case "application/zip":
		// Unzip the plugin into the staging dir and validate its plugin.toml, if any.
		if err := fsutil.Unzip(m.fs, installOpts.path, envDir); err != nil {
			return err
		}
		pluginFilePath := filepath.Join(envDir, "plugin.toml")
		if err := m.unmarshalPlugin(plugin, pluginFilePath); err != nil {
			return err
		}
		if plugin.Name != "" {
			installOpts.Name = plugin.Name
		}

	// The current media type detection mechanism (based on the stdlib) cannot
	// detect binary executables. Thus, we assume the resource is an actual binary
	// when it's not a ZIP archive.
	default:
		// Copy the binary into the staging dir's bin folder,
		binDir := filepath.Join(envDir, "bin")
		if err := m.fs.MkdirAll(binDir, 0755); err != nil {
			return err
		}
		binPath := filepath.Join(binDir, filepath.Base(installOpts.path))
		err := fsutil.CopyFile(m.fs, installOpts.path, binPath, 0751)
		if err != nil {
			return err
		}
	}

	// If there is no plugin name, use the resource file basename.
	if installOpts.Name == "" {
		basename := filepath.Base(installOpts.path)
		installOpts.Name = strings.TrimSuffix(basename, filepath.Ext(basename))
	}
	if installOpts.PostInstall != nil {
		return installOpts.PostInstall(m.fs, installOpts.stagingDir)
	}
	return nil
}

// installPlugin installs a plugin from a staging dir into its final location.
// "update" indicates whether an already existing plugin can be overwritten.
func (m *Manager) installPlugin(installOpts *InstallOpts) error {
	dest := filepath.Join(m.pluginsDir(), installOpts.Name)

	if installOpts.Update {
		if err := m.fs.RemoveAll(dest); err != nil {
			return err
		}
	} else {
		pluginDirExists, err := afero.DirExists(m.fs, dest)
		if err != nil {
			m.logger.Debug(err)
		}
		if pluginDirExists {
			return fmt.Errorf("'%s' is already installed", installOpts.Name)
		}
	}

	if err := m.fs.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return err
	}

	// Copy the plugin folder to its final location. We don't move it as this causes
	// issues when the system's temp dir and the DC/OS dir are on different devices.
	// See https://groups.google.com/forum/m/#!topic/golang-dev/5w7Jmg_iCJQ.
	defer m.fs.RemoveAll(installOpts.stagingDir)
	return fsutil.CopyDir(m.fs, installOpts.stagingDir, dest)
}

// httpClient returns the appropriate HTTP client for a given resource.
func (m *Manager) httpClient(url string) *httpclient.Client {
	httpOpts := []httpclient.Option{
		httpclient.Logger(m.logger),
		httpclient.FailOnErrStatus(true),
	}
	if strings.HasPrefix(url, m.cluster.URL()) {
		httpOpts = append(
			httpOpts,
			httpclient.ACSToken(m.cluster.ACSToken()),
			httpclient.TLS(&tls.Config{
				InsecureSkipVerify: m.cluster.TLS().Insecure,
				RootCAs:            m.cluster.TLS().RootCAs,
			}),
		)
	}
	return httpclient.New("", httpOpts...)
}
<commit_msg>Remove downloaded plugins after installation<commit_after>package plugin

//go:generate goderive .

import (
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"hash"
	"io"
	"mime"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"

	"github.com/dcos/dcos-cli/pkg/config"
	"github.com/dcos/dcos-cli/pkg/fsutil"
	"github.com/dcos/dcos-cli/pkg/httpclient"
	"github.com/pelletier/go-toml"
	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
)

// Manager retrieves the plugins available for the current cluster
// by navigating into the filesystem.
type Manager struct {
	fs      afero.Fs
	logger  *logrus.Logger
	cluster *config.Cluster
}

// NewManager returns a new plugin manager.
func NewManager(fs afero.Fs, logger *logrus.Logger) *Manager {
	return &Manager{
		fs:     fs,
		logger: logger,
	}
}

// InstallOpts are installation options for plugin resources.
type InstallOpts struct {
	// Name specify the name of the plugin.
	Name string

	// Update allows to potentially overwrite an already existing plugin of the same name.
	Update bool

	// Checksum represents a CLI plugin resource content hash.
	Checksum Checksum

	// PostInstall is a hook which can be invoked after plugin installation.
	// It is invoked right before the plugin is moved to its final location.
	PostInstall func(fs afero.Fs, pluginDir string) error

	path       string
	stagingDir string
}

// Checksum contains the hash function and the checksum we expect from a plugin.
type Checksum struct {
	Hasher hash.Hash
	Value  string
}

// Install installs a plugin from a resource.
func (m *Manager) Install(resource string, installOpts *InstallOpts) (err error) {
	// If it's a remote resource, download it first.
	m.logger.Infof("Installing plugin from %s...", resource)
	if strings.HasPrefix(resource, "https://") || strings.HasPrefix(resource, "http://") {
		installOpts.path, err = m.downloadPlugin(resource, installOpts.Checksum)
		if err != nil {
			return err
		}
		// Remove the downloaded resource from the temp dir at the end of installation.
		defer m.fs.RemoveAll(filepath.Dir(installOpts.path))
	} else {
		installOpts.path = resource
	}

	// The staging dir is where the plugin will be constructed
	// before eventually getting moved to its final location.
	installOpts.stagingDir, err = afero.TempDir(m.fs, os.TempDir(), "dcos-cli")
	if err != nil {
		return err
	}

	// Build the plugin into the staging directory.
	err = m.buildPlugin(installOpts)
	if err != nil {
		return err
	}
	return m.installPlugin(installOpts)
}

// SetCluster sets the plugin manager's target cluster.
func (m *Manager) SetCluster(cluster *config.Cluster) {
	m.cluster = cluster
}

// Remove removes a plugin from the filesystem.
func (m *Manager) Remove(name string) error {
	pluginDir := filepath.Join(m.pluginsDir(), name)
	pluginDirExists, err := afero.DirExists(m.fs, pluginDir)
	if err != nil {
		return err
	}
	if !pluginDirExists {
		return fmt.Errorf("'%s' is not a plugin directory", pluginDir)
	}
	err = m.fs.RemoveAll(pluginDir)
	if err != nil {
		return err
	}
	m.logger.Infof("Removed %s as a plugin from the CLI", name)
	return nil
}

// Plugins returns the plugins associated with the current cluster.
func (m *Manager) Plugins() (plugins []*Plugin) {
	pluginDirs, err := afero.ReadDir(m.fs, m.pluginsDir())
	if err != nil {
		m.logger.Debugf("Couldn't open plugin dir: %s", err)
		return plugins
	}

	for _, pluginDir := range pluginDirs {
		if !pluginDir.IsDir() {
			continue
		}
		plugin, err := m.loadPlugin(pluginDir.Name())
		if err != nil {
			// We don't want to see the CLI failing if a single plugin is malformed.
			// We thus log the error but continue if there is an issue at that step.
			m.logger.Debugf("Couldn't load plugin: %s", err)
			continue
		}
		plugins = append(plugins, plugin)
	}
	return plugins
}

// loadPlugin loads a plugin based on its name.
func (m *Manager) loadPlugin(name string) (*Plugin, error) {
	m.logger.Infof("Loading plugin '%s'...", name)

	plugin := &Plugin{Name: name}
	pluginPath := filepath.Join(m.pluginsDir(), name, "env")
	pluginFilePath := filepath.Join(pluginPath, "plugin.toml")

	if err := m.unmarshalPlugin(plugin, pluginFilePath); err != nil {
		return nil, err
	}

	// Save a deep copy of the plugin once it's loaded from the plugin.toml file.
	persistedPlugin := &Plugin{}
	deriveDeepCopy(persistedPlugin, plugin)

	if len(plugin.Commands) == 0 {
		plugin.Commands = m.findCommands(pluginPath)
	}

	// Normalize plugin commands by putting binary full paths and description summaries.
	for i, cmd := range plugin.Commands {
		if !filepath.IsAbs(cmd.Path) {
			cmd.Path = filepath.Join(pluginPath, cmd.Path)
		}
		if cmd.Description == "" {
			cmd.Description = m.commandDescription(cmd)
		}
		plugin.Commands[i] = cmd
	}

	// Compare the normalized plugin with the saved copy to know whether or not the file should be updated.
	if !reflect.DeepEqual(persistedPlugin, plugin) {
		m.persistPlugin(plugin, pluginFilePath)
	}
	return plugin, nil
}

// findCommands discovers commands in a given directory according to conventions.
// Each command should be contained in a dedicated binary named `dcos-{command}`.
// On Windows it must have the `.exe`` extension.
func (m *Manager) findCommands(pluginDir string) (commands []Command) {
	binDir := filepath.Join(pluginDir, "bin")
	if runtime.GOOS == "windows" {
		// On Windows we check for the `bin` dir. If it doesn't exist we use the legacy `Scripts` dir.
		binDirExists, err := afero.DirExists(m.fs, binDir)
		if err != nil || !binDirExists {
			binDir = filepath.Join(pluginDir, "Scripts")
		}
	}

	m.logger.Debugf("Discovering commands in '%s'...", binDir)

	binaries, err := afero.ReadDir(m.fs, binDir)
	if err != nil {
		m.logger.Debug(err)
		return nil
	}

	for _, binary := range binaries {
		if !strings.HasPrefix(binary.Name(), "dcos-") {
			continue
		}
		cmd := Command{
			Path: filepath.Join(binDir, binary.Name()),
			Name: strings.TrimPrefix(binary.Name(), "dcos-"),
		}
		if runtime.GOOS == "windows" {
			cmd.Name = strings.TrimSuffix(cmd.Name, ".exe")
		}
		m.logger.Debugf("Discovered '%s' command", cmd.Name)
		commands = append(commands, cmd)
	}
	return commands
}

// commandDescription gets the command info summary by invoking the binary with the `--info` flag.
func (m *Manager) commandDescription(cmd Command) (desc string) {
	infoCmd, err := exec.Command(cmd.Path, cmd.Name, "--info").Output()
	if err != nil {
		m.logger.Debugf("Couldn't get info summary for the '%s' command: %s", cmd.Name, err)
	} else {
		desc = strings.TrimSpace(string(infoCmd))
	}
	return desc
}

// unmarshalPlugin unmarshals a `plugin.toml` file into a Plugin structure.
func (m *Manager) unmarshalPlugin(plugin *Plugin, path string) error {
	data, err := afero.ReadFile(m.fs, path)
	if err != nil {
		m.logger.Debugf("Couldn't open plugin.toml: %s", err)
		return nil
	}
	return toml.Unmarshal(data, plugin)
}

// persistPlugin saves a `plugin.toml` file representing the plugin.
func (m *Manager) persistPlugin(plugin *Plugin, path string) {
	pluginTOML, err := toml.Marshal(*plugin)
	if err == nil {
		err = afero.WriteFile(m.fs, path, pluginTOML, 0644)
	}
	if err != nil {
		m.logger.Debug(err)
	}
}

// pluginsDir returns the path to the plugins directory.
func (m *Manager) pluginsDir() string {
	return filepath.Join(m.cluster.Dir(), "subcommands")
}

// downloadPlugin downloads a plugin and returns the path to the temporary file it stored it to.
func (m *Manager) downloadPlugin(url string, checksum Checksum) (string, error) {
	tmpDir, err := afero.TempDir(m.fs, os.TempDir(), "dcos-cli")
	if err != nil {
		return "", err
	}

	resp, err := m.httpClient(url).Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	downloadedFilePath := filepath.Join(tmpDir, m.downloadFilename(resp))

	var respReader io.Reader
	if checksum.Hasher != nil {
		respReader = io.TeeReader(resp.Body, checksum.Hasher)
	} else {
		respReader = resp.Body
	}
	if err := fsutil.CopyReader(m.fs, respReader, downloadedFilePath, 0644); err != nil {
		return "", err
	}

	if checksum.Hasher != nil {
		m.logger.Debugf("Verifying checksum for %s...", url)
		computedChecksum := hex.EncodeToString(checksum.Hasher.Sum(nil))
		if computedChecksum != checksum.Value {
			return "", fmt.Errorf("computed checksum %s for %s, expected %s", computedChecksum, url, checksum.Value)
		}
	}

	return downloadedFilePath, nil
}

// downloadFilename picks a filename for the resource to download. It first reads the
// `Content-Disposition` header, when not set it defaults to the URL path basename.
func (m *Manager) downloadFilename(resp *http.Response) string {
	contentDisposition := resp.Header.Get("Content-Disposition")
	_, dispositionParams, err := mime.ParseMediaType(contentDisposition)
	if err != nil {
		m.logger.Debug(err)
	} else if filename, ok := dispositionParams["filename"]; ok {
		return filename
	}
	return path.Base(resp.Request.URL.Path)
}

// buildPlugin constructs the plugin structure within a target directory, based on a path to a plugin resource.
func (m *Manager) buildPlugin(installOpts *InstallOpts) error {
	plugin := &Plugin{}

	// Detect the plugin resource media type.
	contentType, err := fsutil.DetectMediaType(m.fs, installOpts.path)
	if err != nil {
		return err
	}

	envDir := filepath.Join(installOpts.stagingDir, "env")

	switch contentType {
	case "application/zip":
		// Unzip the plugin into the staging dir and validate its plugin.toml, if any.
		if err := fsutil.Unzip(m.fs, installOpts.path, envDir); err != nil {
			return err
		}
		pluginFilePath := filepath.Join(envDir, "plugin.toml")
		if err := m.unmarshalPlugin(plugin, pluginFilePath); err != nil {
			return err
		}
		if plugin.Name != "" {
			installOpts.Name = plugin.Name
		}

	// The current media type detection mechanism (based on the stdlib) cannot
	// detect binary executables. Thus, we assume the resource is an actual binary
	// when it's not a ZIP archive.
	default:
		// Copy the binary into the staging dir's bin folder,
		binDir := filepath.Join(envDir, "bin")
		if err := m.fs.MkdirAll(binDir, 0755); err != nil {
			return err
		}
		binPath := filepath.Join(binDir, filepath.Base(installOpts.path))
		err := fsutil.CopyFile(m.fs, installOpts.path, binPath, 0751)
		if err != nil {
			return err
		}
	}

	// If there is no plugin name, use the resource file basename.
	if installOpts.Name == "" {
		basename := filepath.Base(installOpts.path)
		installOpts.Name = strings.TrimSuffix(basename, filepath.Ext(basename))
	}
	if installOpts.PostInstall != nil {
		return installOpts.PostInstall(m.fs, installOpts.stagingDir)
	}
	return nil
}

// installPlugin installs a plugin from a staging dir into its final location.
// "update" indicates whether an already existing plugin can be overwritten.
func (m *Manager) installPlugin(installOpts *InstallOpts) error {
	dest := filepath.Join(m.pluginsDir(), installOpts.Name)

	if installOpts.Update {
		if err := m.fs.RemoveAll(dest); err != nil {
			return err
		}
	} else {
		pluginDirExists, err := afero.DirExists(m.fs, dest)
		if err != nil {
			m.logger.Debug(err)
		}
		if pluginDirExists {
			return fmt.Errorf("'%s' is already installed", installOpts.Name)
		}
	}

	if err := m.fs.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return err
	}

	// Copy the plugin folder to its final location. We don't move it as this causes
	// issues when the system's temp dir and the DC/OS dir are on different devices.
	// See https://groups.google.com/forum/m/#!topic/golang-dev/5w7Jmg_iCJQ.
	defer m.fs.RemoveAll(installOpts.stagingDir)
	return fsutil.CopyDir(m.fs, installOpts.stagingDir, dest)
}

// httpClient returns the appropriate HTTP client for a given resource.
func (m *Manager) httpClient(url string) *httpclient.Client {
	httpOpts := []httpclient.Option{
		httpclient.Logger(m.logger),
		httpclient.FailOnErrStatus(true),
	}
	if strings.HasPrefix(url, m.cluster.URL()) {
		httpOpts = append(
			httpOpts,
			httpclient.ACSToken(m.cluster.ACSToken()),
			httpclient.TLS(&tls.Config{
				InsecureSkipVerify: m.cluster.TLS().Insecure,
				RootCAs:            m.cluster.TLS().RootCAs,
			}),
		)
	}
	return httpclient.New("", httpOpts...)
}
<|endoftext|>


<commit_before>package helm

import (
	"github.com/Aptomi/aptomi/pkg/config"
	"sync"
)

// Plugin uses Helm for deployment of apps on kubernetes
type Plugin struct {
	cache *sync.Map
	cfg   config.Helm
}

// NewPlugin creates a new helm plugin
func NewPlugin(cfg config.Helm) *Plugin {
	return &Plugin{
		cache: new(sync.Map),
		cfg:   cfg,
	}
}
<commit_msg>Ensure that helm plugin implements all needed interfaces<commit_after>package helm

import (
	"github.com/Aptomi/aptomi/pkg/config"
	"github.com/Aptomi/aptomi/pkg/plugin"
	"sync"
)

// Plugin uses Helm for deployment of apps on kubernetes
type Plugin struct {
	cache *sync.Map
	cfg   config.Helm
}

var _ plugin.Plugin = &Plugin{}
var _ plugin.DeployPlugin = &Plugin{}
var _ plugin.PostProcessPlugin = &Plugin{}

// NewPlugin creates a new helm plugin
func NewPlugin(cfg config.Helm) *Plugin {
	return &Plugin{
		cache: new(sync.Map),
		cfg:   cfg,
	}
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Spec 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 dao

import (
	"context"

	"github.com/apigee/registry/server/models"
	"github.com/apigee/registry/server/names"
	"github.com/apigee/registry/server/storage"
	"github.com/apigee/registry/server/storage/filtering"
	"google.golang.org/api/iterator"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// SpecList contains a page of spec resources.
type SpecList struct {
	Specs []models.Spec
	Token string
}

var specFields = []filtering.Field{
	{Name: "name", Type: filtering.String},
	{Name: "project_id", Type: filtering.String},
	{Name: "api_id", Type: filtering.String},
	{Name: "version_id", Type: filtering.String},
	{Name: "spec_id", Type: filtering.String},
	{Name: "filename", Type: filtering.String},
	{Name: "description", Type: filtering.String},
	{Name: "create_time", Type: filtering.Timestamp},
	{Name: "revision_create_time", Type: filtering.Timestamp},
	{Name: "revision_update_time", Type: filtering.Timestamp},
	{Name: "mime_type", Type: filtering.Timestamp},
	{Name: "size_bytes", Type: filtering.Int},
	{Name: "source_uri", Type: filtering.String},
	{Name: "labels", Type: filtering.StringMap},
}

func (d *DAO) ListSpecs(ctx context.Context, parent names.Version, opts PageOptions) (SpecList, error) {
	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("Currency", models.IsCurrent)
	q, err := q.ApplyCursor(opts.Token)
	if err != nil {
		return SpecList{}, status.Errorf(codes.InvalidArgument, "invalid page token %q: %s", opts.Token, err.Error())
	}

	if id := parent.ProjectID; id != "-" {
		q = q.Require("ProjectID", id)
	}
	if id := parent.ApiID; id != "-" {
		q = q.Require("ApiID", id)
	}
	if id := parent.VersionID; id != "-" {
		q = q.Require("VersionID", id)
	}

	if parent.ProjectID != "-" && parent.ApiID != "-" && parent.VersionID != "-" {
		if _, err := d.GetVersion(ctx, parent); err != nil {
			return SpecList{}, err
		}
	} else if parent.ProjectID != "-" && parent.ApiID != "-" && parent.VersionID == "-" {
		if _, err := d.GetApi(ctx, parent.Api()); err != nil {
			return SpecList{}, err
		}
	} else if parent.ProjectID != "-" && parent.ApiID == "-" && parent.VersionID == "-" {
		if _, err := d.GetProject(ctx, parent.Project()); err != nil {
			return SpecList{}, err
		}
	}

	filter, err := filtering.NewFilter(opts.Filter, specFields)
	if err != nil {
		return SpecList{}, err
	}

	it := d.Run(ctx, q)
	response := SpecList{
		Specs: make([]models.Spec, 0, opts.Size),
	}

	spec := new(models.Spec)
	for _, err = it.Next(spec); err == nil; _, err = it.Next(spec) {
		specMap, err := specMap(*spec)
		if err != nil {
			return response, status.Error(codes.Internal, err.Error())
		}

		match, err := filter.Matches(specMap)
		if err != nil {
			return response, err
		} else if !match {
			continue
		}

		response.Specs = append(response.Specs, *spec)
		if len(response.Specs) == int(opts.Size) {
			break
		}
	}
	if err != nil && err != iterator.Done {
		return response, status.Error(codes.Internal, err.Error())
	}

	if err == nil {
		response.Token, err = it.GetCursor()
		if err != nil {
			return response, status.Error(codes.Internal, err.Error())
		}
	}

	return response, nil
}

func specMap(spec models.Spec) (map[string]interface{}, error) {
	labels, err := spec.LabelsMap()
	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"name":                 spec.Name(),
		"project_id":           spec.ProjectID,
		"api_id":               spec.ApiID,
		"version_id":           spec.VersionID,
		"spec_id":              spec.SpecID,
		"filename":             spec.FileName,
		"description":          spec.Description,
		"create_time":          spec.CreateTime,
		"revision_create_time": spec.RevisionCreateTime,
		"revision_update_time": spec.RevisionUpdateTime,
		"mime_type":            spec.MimeType,
		"size_bytes":           spec.SizeInBytes,
		"source_uri":           spec.SourceURI,
		"labels":               labels,
	}, nil
}

func (d *DAO) GetSpec(ctx context.Context, name names.Spec) (*models.Spec, error) {
	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("ProjectID", name.ProjectID)
	q = q.Require("ApiID", name.ApiID)
	q = q.Require("VersionID", name.VersionID)
	q = q.Require("SpecID", name.SpecID)
	q = q.Require("Currency", models.IsCurrent)

	it := d.Run(ctx, q)
	spec := &models.Spec{}
	if _, err := it.Next(spec); err != nil {
		return nil, status.Error(codes.NotFound, err.Error())
	}

	return spec, nil
}

func (d *DAO) SaveSpec(ctx context.Context, spec *models.Spec) error {
	k := d.NewKey(storage.SpecEntityName, spec.Name())
	if _, err := d.Put(ctx, k, spec); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	return nil
}

func (d *DAO) DeleteSpec(ctx context.Context, name names.Spec) error {
	if err := d.DeleteChildrenOfSpec(ctx, name); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	k := d.NewKey(storage.SpecEntityName, name.String())
	if err := d.Delete(ctx, k); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("ProjectID", name.ProjectID)
	q = q.Require("ApiID", name.ApiID)
	q = q.Require("VersionID", name.VersionID)
	q = q.Require("SpecID", name.SpecID)
	if err := d.DeleteAllMatches(ctx, q); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	return nil
}
<commit_msg>Set the filtering type of the spec mime_type field to String (#118)<commit_after>// Copyright 2020 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Spec 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 dao

import (
	"context"

	"github.com/apigee/registry/server/models"
	"github.com/apigee/registry/server/names"
	"github.com/apigee/registry/server/storage"
	"github.com/apigee/registry/server/storage/filtering"
	"google.golang.org/api/iterator"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// SpecList contains a page of spec resources.
type SpecList struct {
	Specs []models.Spec
	Token string
}

var specFields = []filtering.Field{
	{Name: "name", Type: filtering.String},
	{Name: "project_id", Type: filtering.String},
	{Name: "api_id", Type: filtering.String},
	{Name: "version_id", Type: filtering.String},
	{Name: "spec_id", Type: filtering.String},
	{Name: "filename", Type: filtering.String},
	{Name: "description", Type: filtering.String},
	{Name: "create_time", Type: filtering.Timestamp},
	{Name: "revision_create_time", Type: filtering.Timestamp},
	{Name: "revision_update_time", Type: filtering.Timestamp},
	{Name: "mime_type", Type: filtering.String},
	{Name: "size_bytes", Type: filtering.Int},
	{Name: "source_uri", Type: filtering.String},
	{Name: "labels", Type: filtering.StringMap},
}

func (d *DAO) ListSpecs(ctx context.Context, parent names.Version, opts PageOptions) (SpecList, error) {
	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("Currency", models.IsCurrent)
	q, err := q.ApplyCursor(opts.Token)
	if err != nil {
		return SpecList{}, status.Errorf(codes.InvalidArgument, "invalid page token %q: %s", opts.Token, err.Error())
	}

	if id := parent.ProjectID; id != "-" {
		q = q.Require("ProjectID", id)
	}
	if id := parent.ApiID; id != "-" {
		q = q.Require("ApiID", id)
	}
	if id := parent.VersionID; id != "-" {
		q = q.Require("VersionID", id)
	}

	if parent.ProjectID != "-" && parent.ApiID != "-" && parent.VersionID != "-" {
		if _, err := d.GetVersion(ctx, parent); err != nil {
			return SpecList{}, err
		}
	} else if parent.ProjectID != "-" && parent.ApiID != "-" && parent.VersionID == "-" {
		if _, err := d.GetApi(ctx, parent.Api()); err != nil {
			return SpecList{}, err
		}
	} else if parent.ProjectID != "-" && parent.ApiID == "-" && parent.VersionID == "-" {
		if _, err := d.GetProject(ctx, parent.Project()); err != nil {
			return SpecList{}, err
		}
	}

	filter, err := filtering.NewFilter(opts.Filter, specFields)
	if err != nil {
		return SpecList{}, err
	}

	it := d.Run(ctx, q)
	response := SpecList{
		Specs: make([]models.Spec, 0, opts.Size),
	}

	spec := new(models.Spec)
	for _, err = it.Next(spec); err == nil; _, err = it.Next(spec) {
		specMap, err := specMap(*spec)
		if err != nil {
			return response, status.Error(codes.Internal, err.Error())
		}

		match, err := filter.Matches(specMap)
		if err != nil {
			return response, err
		} else if !match {
			continue
		}

		response.Specs = append(response.Specs, *spec)
		if len(response.Specs) == int(opts.Size) {
			break
		}
	}
	if err != nil && err != iterator.Done {
		return response, status.Error(codes.Internal, err.Error())
	}

	if err == nil {
		response.Token, err = it.GetCursor()
		if err != nil {
			return response, status.Error(codes.Internal, err.Error())
		}
	}

	return response, nil
}

func specMap(spec models.Spec) (map[string]interface{}, error) {
	labels, err := spec.LabelsMap()
	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"name":                 spec.Name(),
		"project_id":           spec.ProjectID,
		"api_id":               spec.ApiID,
		"version_id":           spec.VersionID,
		"spec_id":              spec.SpecID,
		"filename":             spec.FileName,
		"description":          spec.Description,
		"create_time":          spec.CreateTime,
		"revision_create_time": spec.RevisionCreateTime,
		"revision_update_time": spec.RevisionUpdateTime,
		"mime_type":            spec.MimeType,
		"size_bytes":           spec.SizeInBytes,
		"source_uri":           spec.SourceURI,
		"labels":               labels,
	}, nil
}

func (d *DAO) GetSpec(ctx context.Context, name names.Spec) (*models.Spec, error) {
	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("ProjectID", name.ProjectID)
	q = q.Require("ApiID", name.ApiID)
	q = q.Require("VersionID", name.VersionID)
	q = q.Require("SpecID", name.SpecID)
	q = q.Require("Currency", models.IsCurrent)

	it := d.Run(ctx, q)
	spec := &models.Spec{}
	if _, err := it.Next(spec); err != nil {
		return nil, status.Error(codes.NotFound, err.Error())
	}

	return spec, nil
}

func (d *DAO) SaveSpec(ctx context.Context, spec *models.Spec) error {
	k := d.NewKey(storage.SpecEntityName, spec.Name())
	if _, err := d.Put(ctx, k, spec); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	return nil
}

func (d *DAO) DeleteSpec(ctx context.Context, name names.Spec) error {
	if err := d.DeleteChildrenOfSpec(ctx, name); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	k := d.NewKey(storage.SpecEntityName, name.String())
	if err := d.Delete(ctx, k); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	q := d.NewQuery(storage.SpecEntityName)
	q = q.Require("ProjectID", name.ProjectID)
	q = q.Require("ApiID", name.ApiID)
	q = q.Require("VersionID", name.VersionID)
	q = q.Require("SpecID", name.SpecID)
	if err := d.DeleteAllMatches(ctx, q); err != nil {
		return status.Error(codes.Internal, err.Error())
	}

	return nil
}
<|endoftext|>


<commit_before>package redlot

import (
	"strconv"

	"github.com/syndtr/goleveldb/leveldb/util"
)

func encodeHashKey(name, key []byte) (buf []byte) {
	buf = append(buf, typeHASH)
	buf = append(buf, uint32ToBytes(uint32(len(name)))...)
	buf = append(buf, name...)
	buf = append(buf, key...)
	return
}

func decodeHashKey(b []byte) (name, key []byte) {
	nameLen := bytesToUint32(b[1:5])
	name = b[5 : 5+nameLen]
	key = b[5+nameLen:]
	return
}

func encodeHsizeKey(name []byte) (buf []byte) {
	buf = append(buf, typeHSIZE)
	buf = append(buf, name...)
	return
}

func decodeHsizeKey(b []byte) (key []byte) {
	return b[1:]
}

func hashSizeIncr(name []byte, incr int) {
	hsize := encodeHsizeKey(name)

	var size uint32
	if b, err := db.Get(hsize, nil); err == nil {
		size = bytesToUint32(b)
	}

	if incr > 0 {
		size += uint32(incr)
	}
	if incr < 0 && size > 0 {
		size = size - uint32(0-incr)
	}

	if size == 0 {
		db.Delete(hsize, nil)
	}
	if size > 0 {
		db.Put(hsize, uint32ToBytes(size), nil)
	}
}

// Hset will set a hashmap value by the key.
// Args: name string, key string, value any
func Hset(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}
	key := encodeHashKey(args[0], args[1])

	if exists, _ := db.Has(key, nil); !exists {
		hashSizeIncr(args[0], 1)
	}

	err = db.Put(key, args[2], nil)
	if err != nil {
		hashSizeIncr(args[0], -1)
		return nil, err
	}

	return
}

// Hset will return a hashmap value by the key.
// Args: name string, key string
func Hget(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}
	var b []byte
	b, err = db.Get(encodeHashKey(args[0], args[1]), nil)
	return string(b), err
}

// Hdel will delete a hashmap value by the key.
// Args: name string, key string
func Hdel(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}

func hincr(name, key []byte, increment int) (r int64, err error) {
	hash := encodeHashKey(name, key)
	v, _ := db.Get(hash, nil)
	var number int
	var exists bool
	if len(v) != 0 {
		number, err = strconv.Atoi(string(v))
		exists = true
		if err != nil {
			return -1, errNotInt
		}
	}
	number += increment
	r = int64(number)
	err = db.Put(hash, []byte(strconv.Itoa(number)), nil)
	if err == nil && !exists {
		hashSizeIncr(name, 1)
	}
	return
}

// Hincr will incr a hashmap value by the key.
// Args: name string, key string
func Hincr(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return hincr(args[0], args[1], 1)
}

// Hincrby will incr number a hashmap value by the key.
// Args: name string, key string, value int
func Hincrby(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}
	i, e := strconv.Atoi(string(args[2]))
	if e != nil {
		return -1, errNotInt
	}

	return hincr(args[0], args[1], i)
}

// Hexists will check the hashmap key is exists.
// Args: name string, key string
func Hexists(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}

// Hsize will return the hashmap size.
// Args: name string
func Hsize(args [][]byte) (r interface{}, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	var size int64
	if b, e := db.Get(encodeHsizeKey(args[0]), nil); e != nil {
		size = -1
	} else {
		size = int64(bytesToUint32(b))
	}

	return size, nil
}

// Hlist will list all hashmap in the range.
// Args: start string, end string, limit int
func Hlist(args [][]byte) (r []string, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}

	return
}

// Hrlist will reverse list all hashmap in the range.
// Args: start string, end string, limit int
func Hrlist(args [][]byte) (r []string, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}

	return
}

func hscan(args [][]byte, kv, reverse bool) (r []string, err error) {
	if _, err = db.Get(encodeHsizeKey(args[0]), nil); err != nil {
		return
	}

	if len(args[1]) != 0 && string(args[1]) >= string(args[2]) {
		return []string{""}, nil
	}

	var ks, ke []byte
	if len(args[1]) == 0 {
		ks = append(ks, typeHASH)
		ks = append(ks, uint32ToBytes(uint32(len(args[0])))...)
		ks = append(ks, args[0]...)
	} else {
		ks = encodeHashKey(args[0], args[1])
	}

	if len(args[2]) == 0 {
		ke = append(ks, []byte{0xff}...)
	} else {
		ke = encodeHashKey(args[0], args[2])
	}

	limit, _ := strconv.Atoi(string(args[3]))

	iter := db.NewIterator(&util.Range{Start: ks, Limit: ke}, nil)
	var iters func() bool
	if reverse {
		iter.Seek(ke)
		iters = iter.Prev
	} else {
		iters = iter.Next
	}
	for iters() {
		_, key := decodeHashKey(iter.Key())
		r = append(r, string(key))
		if kv {
			r = append(r, string(iter.Value()))
		}
		limit--
		if limit <= 0 {
			break
		}
	}

	iter.Release()
	err = iter.Error()

	return
}

// Hkeys will list the hashmap keys in the range.
// Args: name string, start string, end string, limit int
func Hkeys(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, false, false)

	return
}

// Hrkeys will reverse list the hashmap keys in the range.
// Args: name string, start string, end string, limit int
func Hrkeys(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, false, true)

	return
}

// Hgetall will list all keys/value in the hashmap.
// Args: name string
func Hgetall(args [][]byte) (r []string, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	if _, err = db.Get(encodeHsizeKey(args[0]), nil); err != nil {
		return
	}

	var buf []byte
	buf = append(buf, typeHASH)
	buf = append(buf, uint32ToBytes(uint32(len(args[0])))...)
	buf = append(buf, args[0]...)
	ke := append(buf, []byte{0xff}...)

	iter := db.NewIterator(&util.Range{Start: buf, Limit: ke}, nil)
	for iter.Next() {
		_, key := decodeHashKey(iter.Key())
		r = append(r, string(key))
		r = append(r, string(iter.Value()))
	}
	iter.Release()
	err = iter.Error()
	return
}

// Hscan will list keys/value of the hashmap in the range.
// Args: name string, start string, end string, limit int
func Hscan(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, true, false)

	return
}

// Hrscan will reverse list keys/value of the hashmap in the range.
// Args: name string, start string, end string, limit int
func Hrscan(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, true, true)

	return
}

// Hclear will remove all value in the hashmap.
// Args: name string
func Hclear(args [][]byte) (r interface{}, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	return
}

// MultiHget will return multi hashmap value by keys.
func MultiHget(args [][]byte) (r []string, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}

// MultiHset will set multi hashmap value by keys.
func MultiHset(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 && len(args)%2 == 0 {
		return nil, errNosArgs
	}

	return
}

// MultiHdel will delete multi hashmap value by keys.
func MultiHdel(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}
<commit_msg>Only increment -1 when delete hash field success.<commit_after>package redlot

import (
	"strconv"

	"github.com/syndtr/goleveldb/leveldb/util"
)

func encodeHashKey(name, key []byte) (buf []byte) {
	buf = append(buf, typeHASH)
	buf = append(buf, uint32ToBytes(uint32(len(name)))...)
	buf = append(buf, name...)
	buf = append(buf, key...)
	return
}

func decodeHashKey(b []byte) (name, key []byte) {
	nameLen := bytesToUint32(b[1:5])
	name = b[5 : 5+nameLen]
	key = b[5+nameLen:]
	return
}

func encodeHsizeKey(name []byte) (buf []byte) {
	buf = append(buf, typeHSIZE)
	buf = append(buf, name...)
	return
}

func decodeHsizeKey(b []byte) (key []byte) {
	return b[1:]
}

func hashSizeIncr(name []byte, incr int) {
	hsize := encodeHsizeKey(name)

	var size uint32
	if b, err := db.Get(hsize, nil); err == nil {
		size = bytesToUint32(b)
	}

	if incr > 0 {
		size += uint32(incr)
	}
	if incr < 0 && size > 0 {
		size = size - uint32(0-incr)
	}

	if size == 0 {
		db.Delete(hsize, nil)
	}
	if size > 0 {
		db.Put(hsize, uint32ToBytes(size), nil)
	}
}

// Hset will set a hashmap value by the key.
// Args: name string, key string, value any
func Hset(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}
	key := encodeHashKey(args[0], args[1])

	if exists, _ := db.Has(key, nil); !exists {
		hashSizeIncr(args[0], 1)
	}

	err = db.Put(key, args[2], nil)
	if err != nil {
		hashSizeIncr(args[0], -1)
		return nil, err
	}

	return
}

// Hset will return a hashmap value by the key.
// Args: name string, key string
func Hget(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}
	var b []byte
	b, err = db.Get(encodeHashKey(args[0], args[1]), nil)
	return string(b), err
}

// Hdel will delete a hashmap value by the key.
// Args: name string, key string
func Hdel(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}
	err = db.Delete(encodeHashKey(args[0], args[1]), nil)
	if err == nil {
		hashSizeIncr(args[0], -1)
	}
	return
}

func hincr(name, key []byte, increment int) (r int64, err error) {
	hash := encodeHashKey(name, key)
	v, _ := db.Get(hash, nil)
	var number int
	var exists bool
	if len(v) != 0 {
		number, err = strconv.Atoi(string(v))
		exists = true
		if err != nil {
			return -1, errNotInt
		}
	}
	number += increment
	r = int64(number)
	err = db.Put(hash, []byte(strconv.Itoa(number)), nil)
	if err == nil && !exists {
		hashSizeIncr(name, 1)
	}
	return
}

// Hincr will incr a hashmap value by the key.
// Args: name string, key string
func Hincr(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return hincr(args[0], args[1], 1)
}

// Hincrby will incr number a hashmap value by the key.
// Args: name string, key string, value int
func Hincrby(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}
	i, e := strconv.Atoi(string(args[2]))
	if e != nil {
		return -1, errNotInt
	}

	return hincr(args[0], args[1], i)
}

// Hexists will check the hashmap key is exists.
// Args: name string, key string
func Hexists(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}

// Hsize will return the hashmap size.
// Args: name string
func Hsize(args [][]byte) (r interface{}, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	var size int64
	if b, e := db.Get(encodeHsizeKey(args[0]), nil); e != nil {
		size = -1
	} else {
		size = int64(bytesToUint32(b))
	}

	return size, nil
}

// Hlist will list all hashmap in the range.
// Args: start string, end string, limit int
func Hlist(args [][]byte) (r []string, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}

	return
}

// Hrlist will reverse list all hashmap in the range.
// Args: start string, end string, limit int
func Hrlist(args [][]byte) (r []string, err error) {
	if len(args) < 3 {
		return nil, errNosArgs
	}

	return
}

func hscan(args [][]byte, kv, reverse bool) (r []string, err error) {
	if _, err = db.Get(encodeHsizeKey(args[0]), nil); err != nil {
		return
	}

	if len(args[1]) != 0 && string(args[1]) >= string(args[2]) {
		return []string{""}, nil
	}

	var ks, ke []byte
	if len(args[1]) == 0 {
		ks = append(ks, typeHASH)
		ks = append(ks, uint32ToBytes(uint32(len(args[0])))...)
		ks = append(ks, args[0]...)
	} else {
		ks = encodeHashKey(args[0], args[1])
	}

	if len(args[2]) == 0 {
		ke = append(ks, []byte{0xff}...)
	} else {
		ke = encodeHashKey(args[0], args[2])
	}

	limit, _ := strconv.Atoi(string(args[3]))

	iter := db.NewIterator(&util.Range{Start: ks, Limit: ke}, nil)
	var iters func() bool
	if reverse {
		iter.Seek(ke)
		iters = iter.Prev
	} else {
		iters = iter.Next
	}
	for iters() {
		_, key := decodeHashKey(iter.Key())
		r = append(r, string(key))
		if kv {
			r = append(r, string(iter.Value()))
		}
		limit--
		if limit <= 0 {
			break
		}
	}

	iter.Release()
	err = iter.Error()

	return
}

// Hkeys will list the hashmap keys in the range.
// Args: name string, start string, end string, limit int
func Hkeys(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, false, false)

	return
}

// Hrkeys will reverse list the hashmap keys in the range.
// Args: name string, start string, end string, limit int
func Hrkeys(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, false, true)

	return
}

// Hgetall will list all keys/value in the hashmap.
// Args: name string
func Hgetall(args [][]byte) (r []string, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	if _, err = db.Get(encodeHsizeKey(args[0]), nil); err != nil {
		return
	}

	var buf []byte
	buf = append(buf, typeHASH)
	buf = append(buf, uint32ToBytes(uint32(len(args[0])))...)
	buf = append(buf, args[0]...)
	ke := append(buf, []byte{0xff}...)

	iter := db.NewIterator(&util.Range{Start: buf, Limit: ke}, nil)
	for iter.Next() {
		_, key := decodeHashKey(iter.Key())
		r = append(r, string(key))
		r = append(r, string(iter.Value()))
	}
	iter.Release()
	err = iter.Error()
	return
}

// Hscan will list keys/value of the hashmap in the range.
// Args: name string, start string, end string, limit int
func Hscan(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, true, false)

	return
}

// Hrscan will reverse list keys/value of the hashmap in the range.
// Args: name string, start string, end string, limit int
func Hrscan(args [][]byte) (r []string, err error) {
	if len(args) < 4 {
		return nil, errNosArgs
	}

	r, err = hscan(args, true, true)

	return
}

// Hclear will remove all value in the hashmap.
// Args: name string
func Hclear(args [][]byte) (r interface{}, err error) {
	if len(args) < 1 {
		return nil, errNosArgs
	}

	return
}

// MultiHget will return multi hashmap value by keys.
func MultiHget(args [][]byte) (r []string, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}

// MultiHset will set multi hashmap value by keys.
func MultiHset(args [][]byte) (r interface{}, err error) {
	if len(args) < 3 && len(args)%2 == 0 {
		return nil, errNosArgs
	}

	return
}

// MultiHdel will delete multi hashmap value by keys.
func MultiHdel(args [][]byte) (r interface{}, err error) {
	if len(args) < 2 {
		return nil, errNosArgs
	}

	return
}
<|endoftext|>


<commit_before>package proxy

import (
	"net/http"
	"strings"

	log "github.com/Sirupsen/logrus"
	"github.com/hellofresh/janus/pkg/router"
)

const (
	methodAll = "ALL"
)

// RegisterChan holds two channels. Many is a channel of many routes
// this is more for a bulk operation, when you create many routes at once
// use this channel.
// One is for a single route, if you create one route and want to register it
// use this channel
type RegisterChan struct {
	Many chan []*Route
	One  chan *Route
}

// NewRegisterChan creates a new instance of RegisterChan
func NewRegisterChan(router router.Router, transport http.RoundTripper) *RegisterChan {
	register := NewRegister(router, transport)

	registerChan := &RegisterChan{}
	registerChan.Many = make(chan []*Route)
	registerChan.One = make(chan *Route)

	go registerChan.listenForRouteChanges(register)
	return registerChan
}

func (rc *RegisterChan) listenForRouteChanges(register *Register) {
	for {
		select {
		case routes := <-rc.Many:
			register.RegisterMany(routes)

		case route := <-rc.One:
			register.Register(route)
		}
	}
}

// Register handles the register of proxies into the choosen router.
// It also handles the conversion from a proxy to an http.HandlerFunc
type Register struct {
	router    router.Router
	transport http.RoundTripper
	proxies   []Proxy
}

// NewRegister creates a new instance of Register
func NewRegister(router router.Router, transport http.RoundTripper) *Register {
	return &Register{router: router, transport: transport}
}

// RegisterMany registers many proxies at once
func (p *Register) RegisterMany(routes []*Route) {
	for _, route := range routes {
		p.Register(route)
	}
}

// Register register a new proxy
func (p *Register) Register(route *Route) {
	proxy := route.proxy

	if false == p.Exists(proxy) {
		handler := p.ToHandler(proxy)
		matcher := router.NewListenPathMatcher()
		if matcher.Match(proxy.ListenPath) {
			p.doRegister(matcher.Extract(proxy.ListenPath), handler, proxy.Methods, route.handlers)
		}

		p.doRegister(proxy.ListenPath, handler, proxy.Methods, route.handlers)
		p.proxies = append(p.proxies, proxy)
	}

}

func (p *Register) doRegister(
	listenPath string,
	handler http.HandlerFunc,
	methods []string,
	handlers []router.Constructor,
) {
	log.WithFields(log.Fields{
		"listen_path": listenPath,
	}).Debug("Registering a proxy")

	for _, method := range methods {
		if strings.ToUpper(method) == methodAll {
			p.router.Any(listenPath, handler, handlers...)
		} else {
			p.router.Handle(strings.ToUpper(method), listenPath, handler, handlers...)
		}
	}
}

// Exists checks if a proxy is already registered in the manager
func (p *Register) Exists(proxy Proxy) bool {
	for _, route := range p.proxies {
		if route.ListenPath == proxy.ListenPath {
			return true
		}
	}

	return false
}

// ToHandler turns a proxy configuration into a handler
func (p *Register) ToHandler(proxy Proxy) http.HandlerFunc {
	return func(rw http.ResponseWriter, r *http.Request) {
		reverseProxy := NewSingleHostReverseProxy(proxy, p.transport)
		reverseProxy.ServeHTTP(rw, r)
	}
}
<commit_msg>Added register interface<commit_after>package proxy

import (
	"net/http"
	"strings"

	log "github.com/Sirupsen/logrus"
	"github.com/hellofresh/janus/pkg/router"
)

const (
	methodAll = "ALL"
)

type Register interface {
	Exists(route *Route) bool
	Get(listenPath string) *Route
	Remove(listenPath string) error
	AddMany(routes []*Route) error
	Add(route *Route) error
}

// Register handles the register of proxies into the choosen router.
// It also handles the conversion from a proxy to an http.HandlerFunc
type InMemoryRegister struct {
	router    router.Router
	transport http.RoundTripper
	proxies   map[string]*Route
}

// NewInMemoryRegister creates a new instance of Register
func NewInMemoryRegister(router router.Router, transport http.RoundTripper) *InMemoryRegister {
	return &InMemoryRegister{router: router, transport: transport, proxies: make(map[string]*Route)}
}

// Exists checks if a proxy is already registered in the manager
func (p *InMemoryRegister) Exists(route *Route) bool {
	_, exists := p.proxies[route.proxy.ListenPath]
	return exists
}

func (p *InMemoryRegister) Get(listenPath string) *Route {
	return p.proxies[listenPath]
}

func (p *InMemoryRegister) Remove(listenPath string) error {
	delete(p.proxies, listenPath)
	return nil
}

// AddMany registers many proxies at once
func (p *InMemoryRegister) AddMany(routes []*Route) error {
	for _, r := range routes {
		err := p.Add(r)
		if nil != err {
			return err
		}
	}

	return nil
}

// Register register a new proxy
func (p *InMemoryRegister) Add(route *Route) error {
	if p.Exists(route) {
		return p.replace(route)
	} else {
		return p.add(route)
	}
}

func (p *InMemoryRegister) add(route *Route) error {
	if false == p.Exists(route) {
		proxy := route.proxy

		handler := p.toHandler(proxy)
		matcher := router.NewListenPathMatcher()
		if matcher.Match(proxy.ListenPath) {
			p.doRegister(matcher.Extract(proxy.ListenPath), handler, proxy.Methods, route.handlers)
		}

		p.doRegister(proxy.ListenPath, handler, proxy.Methods, route.handlers)
		p.proxies[proxy.ListenPath] = route
	}

	return nil
}

func (p *InMemoryRegister) replace(r *Route) error {
	log.WithFields(log.Fields{
		"listen_path": r.proxy.ListenPath,
		"target_url":  r.proxy.TargetURL,
	}).Debug("Replacing a route")

	currentRoute := p.Get(r.proxy.ListenPath)
	*currentRoute.proxy = *r.proxy
	currentRoute.handlers = r.handlers

	return nil
}

func (p *InMemoryRegister) doRegister(listenPath string, handler http.HandlerFunc, methods []string, handlers []router.Constructor) {
	log.WithFields(log.Fields{
		"listen_path": listenPath,
	}).Debug("Registering a route")

	for _, method := range methods {
		if strings.ToUpper(method) == methodAll {
			p.router.Any(listenPath, handler, handlers...)
		} else {
			p.router.Handle(strings.ToUpper(method), listenPath, handler, handlers...)
		}
	}
}

// ToHandler turns a proxy configuration into a handler
func (p *InMemoryRegister) toHandler(proxy *Proxy) http.HandlerFunc {
	return func(rw http.ResponseWriter, r *http.Request) {
		reverseProxy := NewSingleHostReverseProxy(proxy, p.transport)
		reverseProxy.ServeHTTP(rw, r)
	}
}
<|endoftext|>


<commit_before>package server

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
)

type RedditIndexerConfiguration struct {
	Database *Database
}

type RedditIndexer struct {
	configuration RedditIndexerConfiguration
	closeSignal   chan bool
}

func NewRedditIndexer(configuration RedditIndexerConfiguration) (*RedditIndexer, error) {
	ret := &RedditIndexer{
		configuration: configuration,
		closeSignal:   make(chan bool),
	}
	go ret.run()
	return ret, nil
}

func (indexer *RedditIndexer) Close() {
	indexer.closeSignal <- true
}

func (indexer *RedditIndexer) run() {
	log.Info("starting reddit indexer")

	users := []string{
		"chris_wilson", "Bex_GGG", "Negitivefrags", "Omnitect", "qarldev", "BrianWeissman_GGG",
		"Mark_GGG", "RhysGGG", "Dan_GGG", "Rory_Rackham", "Blake_GGG", "Fitzy_GGG", "Hartlin_GGG",
		"Hrishi_GGG", "Baltic_GGG",
	}
	next := 0

	for {
		select {
		case <-indexer.closeSignal:
			return
		default:
			indexer.index(users[next])
			next += 1
			if next >= len(users) {
				next = 0
			}
			time.Sleep(time.Second * 2)
		}
	}
}

func ParseRedditActivity(b []byte) ([]Activity, string, error) {
	activity := []Activity(nil)

	var root struct {
		Data struct {
			After    string `json:"after"`
			Children []struct {
				Kind string `json:"kind"`
				Data struct {
					Id           string  `json:"id"`
					Author       string  `json:"author"`
					BodyHTML     string  `json:"body_html"`
					SelftextHTML string  `json:"selftext_html"`
					SubredditId  string  `json:"subreddit_id"`
					Permalink    string  `json:"permalink"`
					URL          string  `json:"url"`
					Title        string  `json:"title"`
					CreatedUTC   float64 `json:"created_utc"`
					LinkId       string  `json:"link_id"`
					LinkTitle    string  `json:"link_title"`
				} `json:"data"`
			} `json:"children"`
		} `json:"data"`
	}

	if err := json.Unmarshal(b, &root); err != nil {
		return nil, "", err
	}

	for _, thing := range root.Data.Children {
		if thing.Data.SubredditId != "t5_2sf6m" {
			continue
		}
		switch thing.Kind {
		case "t1":
			activity = append(activity, &RedditComment{
				Id:        thing.Data.Id,
				Author:    thing.Data.Author,
				BodyHTML:  thing.Data.BodyHTML,
				PostId:    strings.TrimPrefix(thing.Data.LinkId, "t3_"),
				PostTitle: thing.Data.LinkTitle,
				Time:      time.Unix(int64(thing.Data.CreatedUTC), 0),
			})
		case "t3":
			activity = append(activity, &RedditPost{
				Id:        thing.Data.Id,
				Author:    thing.Data.Author,
				BodyHTML:  thing.Data.SelftextHTML,
				Permalink: thing.Data.Permalink,
				Title:     thing.Data.Title,
				URL:       thing.Data.URL,
				Time:      time.Unix(int64(thing.Data.CreatedUTC), 0),
			})
		}
	}

	return activity, root.Data.After, nil
}

func (indexer *RedditIndexer) redditActivity(user string, page string) ([]Activity, string, error) {
	url := fmt.Sprintf("https://www.reddit.com/user/%v.json?count=25&after=%v&raw_json=1", user, page)
	client := http.Client{
		Timeout: time.Second * 10,
	}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, "", err
	}
	req.Header.Add("User-Agent", "GGG Tracker (https://github.com/ccbrown/gggtracker) by /u/rz2yoj")

	resp, err := client.Do(req)
	if err != nil {
		return nil, "", err
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, "", err
	}

	return ParseRedditActivity(body)
}

func (indexer *RedditIndexer) index(user string) {
	logger := log.WithFields(log.Fields{
		"user": user,
	})

	cutoff := time.Now().Add(time.Hour * -12)
	activity := []Activity(nil)

	for page := ""; ; {
		things, next, err := indexer.redditActivity(user, page)
		page = next
		if err != nil {
			logger.WithError(err).Error("error requesting reddit activity")
		}

		done := len(things) == 0
		for _, thing := range things {
			if thing.ActivityTime().Before(cutoff) {
				done = true
			}
			activity = append(activity, thing)
		}

		logger.WithFields(log.Fields{
			"count": len(things),
		}).Info("received reddit activity")

		if done {
			break
		}
		time.Sleep(time.Second * 2)
	}

	if len(activity) > 0 {
		indexer.configuration.Database.AddActivity(activity)
	}
}
<commit_msg>added KamilOrmanJanowski to tracked reddit users (#11)<commit_after>package server

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
)

type RedditIndexerConfiguration struct {
	Database *Database
}

type RedditIndexer struct {
	configuration RedditIndexerConfiguration
	closeSignal   chan bool
}

func NewRedditIndexer(configuration RedditIndexerConfiguration) (*RedditIndexer, error) {
	ret := &RedditIndexer{
		configuration: configuration,
		closeSignal:   make(chan bool),
	}
	go ret.run()
	return ret, nil
}

func (indexer *RedditIndexer) Close() {
	indexer.closeSignal <- true
}

func (indexer *RedditIndexer) run() {
	log.Info("starting reddit indexer")

	users := []string{
		"chris_wilson", "Bex_GGG", "Negitivefrags", "Omnitect", "qarldev", "BrianWeissman_GGG",
		"Mark_GGG", "RhysGGG", "Dan_GGG", "Rory_Rackham", "Blake_GGG", "Fitzy_GGG", "Hartlin_GGG",
		"Hrishi_GGG", "Baltic_GGG", "KamilOrmanJanowski",
	}
	next := 0

	for {
		select {
		case <-indexer.closeSignal:
			return
		default:
			indexer.index(users[next])
			next += 1
			if next >= len(users) {
				next = 0
			}
			time.Sleep(time.Second * 2)
		}
	}
}

func ParseRedditActivity(b []byte) ([]Activity, string, error) {
	activity := []Activity(nil)

	var root struct {
		Data struct {
			After    string `json:"after"`
			Children []struct {
				Kind string `json:"kind"`
				Data struct {
					Id           string  `json:"id"`
					Author       string  `json:"author"`
					BodyHTML     string  `json:"body_html"`
					SelftextHTML string  `json:"selftext_html"`
					SubredditId  string  `json:"subreddit_id"`
					Permalink    string  `json:"permalink"`
					URL          string  `json:"url"`
					Title        string  `json:"title"`
					CreatedUTC   float64 `json:"created_utc"`
					LinkId       string  `json:"link_id"`
					LinkTitle    string  `json:"link_title"`
				} `json:"data"`
			} `json:"children"`
		} `json:"data"`
	}

	if err := json.Unmarshal(b, &root); err != nil {
		return nil, "", err
	}

	for _, thing := range root.Data.Children {
		if thing.Data.SubredditId != "t5_2sf6m" {
			continue
		}
		switch thing.Kind {
		case "t1":
			activity = append(activity, &RedditComment{
				Id:        thing.Data.Id,
				Author:    thing.Data.Author,
				BodyHTML:  thing.Data.BodyHTML,
				PostId:    strings.TrimPrefix(thing.Data.LinkId, "t3_"),
				PostTitle: thing.Data.LinkTitle,
				Time:      time.Unix(int64(thing.Data.CreatedUTC), 0),
			})
		case "t3":
			activity = append(activity, &RedditPost{
				Id:        thing.Data.Id,
				Author:    thing.Data.Author,
				BodyHTML:  thing.Data.SelftextHTML,
				Permalink: thing.Data.Permalink,
				Title:     thing.Data.Title,
				URL:       thing.Data.URL,
				Time:      time.Unix(int64(thing.Data.CreatedUTC), 0),
			})
		}
	}

	return activity, root.Data.After, nil
}

func (indexer *RedditIndexer) redditActivity(user string, page string) ([]Activity, string, error) {
	url := fmt.Sprintf("https://www.reddit.com/user/%v.json?count=25&after=%v&raw_json=1", user, page)
	client := http.Client{
		Timeout: time.Second * 10,
	}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, "", err
	}
	req.Header.Add("User-Agent", "GGG Tracker (https://github.com/ccbrown/gggtracker) by /u/rz2yoj")

	resp, err := client.Do(req)
	if err != nil {
		return nil, "", err
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, "", err
	}

	return ParseRedditActivity(body)
}

func (indexer *RedditIndexer) index(user string) {
	logger := log.WithFields(log.Fields{
		"user": user,
	})

	cutoff := time.Now().Add(time.Hour * -12)
	activity := []Activity(nil)

	for page := ""; ; {
		things, next, err := indexer.redditActivity(user, page)
		page = next
		if err != nil {
			logger.WithError(err).Error("error requesting reddit activity")
		}

		done := len(things) == 0
		for _, thing := range things {
			if thing.ActivityTime().Before(cutoff) {
				done = true
			}
			activity = append(activity, thing)
		}

		logger.WithFields(log.Fields{
			"count": len(things),
		}).Info("received reddit activity")

		if done {
			break
		}
		time.Sleep(time.Second * 2)
	}

	if len(activity) > 0 {
		indexer.configuration.Database.AddActivity(activity)
	}
}
<|endoftext|>


<commit_before>package proxy

import (
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"

	"github.com/hellofresh/janus/pkg/router"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
)

const (
	methodAll = "ALL"
)

// Register handles the register of proxies into the chosen router.
// It also handles the conversion from a proxy to an http.HandlerFunc
type Register struct {
	Router router.Router
	params Params
}

// NewRegister creates a new instance of Register
func NewRegister(router router.Router, params Params) *Register {
	return &Register{router, params}
}

// AddMany registers many proxies at once
func (p *Register) AddMany(routes []*Route) error {
	for _, r := range routes {
		err := p.Add(r)
		if nil != err {
			return err
		}
	}

	return nil
}

// Add register a new route
func (p *Register) Add(route *Route) error {
	definition := route.Proxy

	var balancer Balancer
	if definition.IsBalancerDefined() {
		log.WithField("balancing_alg", definition.Upstreams.Balancing).Debug("Using a load balancing algorithm")

		var err error
		balancer, err = NewBalancer(definition.Upstreams.Balancing)
		if err != nil {
			msg := "Could not create a balancer"
			log.WithError(err).Error(msg)
			return errors.Wrap(err, msg)
		}
	}

	p.params.Outbound = route.Outbound
	p.params.InsecureSkipVerify = definition.InsecureSkipVerify
	handler := &httputil.ReverseProxy{
		Director:  p.createDirector(definition, balancer),
		Transport: NewTransportWithParams(p.params),
	}

	matcher := router.NewListenPathMatcher()
	if matcher.Match(definition.ListenPath) {
		p.doRegister(matcher.Extract(definition.ListenPath), handler.ServeHTTP, definition.Methods, route.Inbound)
	}

	p.doRegister(definition.ListenPath, handler.ServeHTTP, definition.Methods, route.Inbound)
	return nil
}

func (p *Register) createDirector(proxyDefinition *Definition, balancer Balancer) func(req *http.Request) {
	return func(req *http.Request) {
		var upstreamURL string
		if proxyDefinition.IsBalancerDefined() && balancer != nil {
			upstream, err := balancer.Elect(proxyDefinition.Upstreams.Targets)
			if err != nil {
				log.WithError(err).Error("Could not elect one upstream")
				return
			}
			log.WithField("target", upstream.Target).Debug("Target upstream elected ")
			upstreamURL = upstream.Target
		} else {
			log.Warn("The upstream URL is deprecated. Use Upstreams instead")
			upstreamURL = proxyDefinition.UpstreamURL
		}

		target, err := url.Parse(upstreamURL)
		if err != nil {
			log.WithError(err).Error("Could not parse the target URL")
			return
		}

		targetQuery := target.RawQuery
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		path := target.Path

		if proxyDefinition.AppendPath {
			log.Debug("Appending listen path to the target url")
			path = singleJoiningSlash(target.Path, req.URL.Path)
		}

		if proxyDefinition.StripPath {
			path = singleJoiningSlash(target.Path, req.URL.Path)
			matcher := router.NewListenPathMatcher()
			listenPath := matcher.Extract(proxyDefinition.ListenPath)

			log.WithField("listen_path", listenPath).Debug("Stripping listen path")
			path = strings.Replace(path, listenPath, "", 1)
			if !strings.HasSuffix(target.Path, "/") && strings.HasSuffix(path, "/") {
				path = path[:len(path)-1]
			}
		}

		log.WithField("path", path).Debug("Upstream Path")
		req.URL.Path = path

		// This is very important to avoid problems with ssl verification for the HOST header
		if !proxyDefinition.PreserveHost {
			log.Debug("Preserving the host header")
			req.Host = target.Host
		}

		if targetQuery == "" || req.URL.RawQuery == "" {
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}
	}
}

func (p *Register) doRegister(listenPath string, handler http.HandlerFunc, methods []string, handlers InChain) {
	log.WithFields(log.Fields{
		"listen_path": listenPath,
	}).Debug("Registering a route")

	if strings.Index(listenPath, "/") != 0 {
		log.WithField("listen_path", listenPath).
			Error("Route listen path must begin with '/'.Skipping invalid route.")
	} else {
		for _, method := range methods {
			if strings.ToUpper(method) == methodAll {
				p.Router.Any(listenPath, handler, handlers...)
			} else {
				p.Router.Handle(strings.ToUpper(method), listenPath, handler, handlers...)
			}
		}
	}
}

func cleanSlashes(a string) string {
	endSlash := strings.HasSuffix(a, "//")
	startSlash := strings.HasPrefix(a, "//")

	if startSlash {
		a = "/" + strings.TrimPrefix(a, "//")
	}

	if endSlash {
		a = strings.TrimSuffix(a, "//") + "/"
	}

	return a
}

func singleJoiningSlash(a, b string) string {
	a = cleanSlashes(a)
	b = cleanSlashes(b)

	aSlash := strings.HasSuffix(a, "/")
	bSlash := strings.HasPrefix(b, "/")

	switch {
	case aSlash && bSlash:
		return a + b[1:]
	case !aSlash && !bSlash:
		if len(b) > 0 {
			return a + "/" + b
		}
		return a
	}
	return a + b
}
<commit_msg>fix log error for preserve_host<commit_after>package proxy

import (
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"

	"github.com/hellofresh/janus/pkg/router"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
)

const (
	methodAll = "ALL"
)

// Register handles the register of proxies into the chosen router.
// It also handles the conversion from a proxy to an http.HandlerFunc
type Register struct {
	Router router.Router
	params Params
}

// NewRegister creates a new instance of Register
func NewRegister(router router.Router, params Params) *Register {
	return &Register{router, params}
}

// AddMany registers many proxies at once
func (p *Register) AddMany(routes []*Route) error {
	for _, r := range routes {
		err := p.Add(r)
		if nil != err {
			return err
		}
	}

	return nil
}

// Add register a new route
func (p *Register) Add(route *Route) error {
	definition := route.Proxy

	var balancer Balancer
	if definition.IsBalancerDefined() {
		log.WithField("balancing_alg", definition.Upstreams.Balancing).Debug("Using a load balancing algorithm")

		var err error
		balancer, err = NewBalancer(definition.Upstreams.Balancing)
		if err != nil {
			msg := "Could not create a balancer"
			log.WithError(err).Error(msg)
			return errors.Wrap(err, msg)
		}
	}

	p.params.Outbound = route.Outbound
	p.params.InsecureSkipVerify = definition.InsecureSkipVerify
	handler := &httputil.ReverseProxy{
		Director:  p.createDirector(definition, balancer),
		Transport: NewTransportWithParams(p.params),
	}

	matcher := router.NewListenPathMatcher()
	if matcher.Match(definition.ListenPath) {
		p.doRegister(matcher.Extract(definition.ListenPath), handler.ServeHTTP, definition.Methods, route.Inbound)
	}

	p.doRegister(definition.ListenPath, handler.ServeHTTP, definition.Methods, route.Inbound)
	return nil
}

func (p *Register) createDirector(proxyDefinition *Definition, balancer Balancer) func(req *http.Request) {
	return func(req *http.Request) {
		var upstreamURL string
		if proxyDefinition.IsBalancerDefined() && balancer != nil {
			upstream, err := balancer.Elect(proxyDefinition.Upstreams.Targets)
			if err != nil {
				log.WithError(err).Error("Could not elect one upstream")
				return
			}
			log.WithField("target", upstream.Target).Debug("Target upstream elected ")
			upstreamURL = upstream.Target
		} else {
			log.Warn("The upstream URL is deprecated. Use Upstreams instead")
			upstreamURL = proxyDefinition.UpstreamURL
		}

		target, err := url.Parse(upstreamURL)
		if err != nil {
			log.WithError(err).Error("Could not parse the target URL")
			return
		}

		targetQuery := target.RawQuery
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		path := target.Path

		if proxyDefinition.AppendPath {
			log.Debug("Appending listen path to the target url")
			path = singleJoiningSlash(target.Path, req.URL.Path)
		}

		if proxyDefinition.StripPath {
			path = singleJoiningSlash(target.Path, req.URL.Path)
			matcher := router.NewListenPathMatcher()
			listenPath := matcher.Extract(proxyDefinition.ListenPath)

			log.WithField("listen_path", listenPath).Debug("Stripping listen path")
			path = strings.Replace(path, listenPath, "", 1)
			if !strings.HasSuffix(target.Path, "/") && strings.HasSuffix(path, "/") {
				path = path[:len(path)-1]
			}
		}

		log.WithField("path", path).Debug("Upstream Path")
		req.URL.Path = path

		// This is very important to avoid problems with ssl verification for the HOST header
		if proxyDefinition.PreserveHost {
			log.Debug("Preserving the host header")
		} else {
			req.Host = target.Host
		}

		if targetQuery == "" || req.URL.RawQuery == "" {
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}
	}
}

func (p *Register) doRegister(listenPath string, handler http.HandlerFunc, methods []string, handlers InChain) {
	log.WithFields(log.Fields{
		"listen_path": listenPath,
	}).Debug("Registering a route")

	if strings.Index(listenPath, "/") != 0 {
		log.WithField("listen_path", listenPath).
			Error("Route listen path must begin with '/'.Skipping invalid route.")
	} else {
		for _, method := range methods {
			if strings.ToUpper(method) == methodAll {
				p.Router.Any(listenPath, handler, handlers...)
			} else {
				p.Router.Handle(strings.ToUpper(method), listenPath, handler, handlers...)
			}
		}
	}
}

func cleanSlashes(a string) string {
	endSlash := strings.HasSuffix(a, "//")
	startSlash := strings.HasPrefix(a, "//")

	if startSlash {
		a = "/" + strings.TrimPrefix(a, "//")
	}

	if endSlash {
		a = strings.TrimSuffix(a, "//") + "/"
	}

	return a
}

func singleJoiningSlash(a, b string) string {
	a = cleanSlashes(a)
	b = cleanSlashes(b)

	aSlash := strings.HasSuffix(a, "/")
	bSlash := strings.HasPrefix(b, "/")

	switch {
	case aSlash && bSlash:
		return a + b[1:]
	case !aSlash && !bSlash:
		if len(b) > 0 {
			return a + "/" + b
		}
		return a
	}
	return a + b
}
<|endoftext|>