<commit_before>package authz

import (
    "github.com/jaypipes/procession/pkg/logging"
    pb "github.com/jaypipes/procession/proto"
)

const (
    readPermsStart = pb.Permission_READ_ANY
    readPermsEnd = (pb.Permission_CREATE_ANY - 1)
    createPermsStart = pb.Permission_CREATE_ANY
    createPermsEnd = (pb.Permission_MODIFY_ANY - 1)
    modifyPermsStart = pb.Permission_MODIFY_ANY
    modifyPermsEnd = (pb.Permission_DELETE_ANY - 1)
    deletePermsStart = pb.Permission_DELETE_ANY
    deletePermsEnd = pb.Permission_END_PERMS
)

// Abstraction layer for authorization implementations
type AuthzLookup interface {
    Get(*pb.Session) (*pb.UserPermissions)
}

type Authz struct {
    log *logging.Logs
    lookup AuthzLookup
}

func New(log *logging.Logs, lookup AuthzLookup) (*Authz, error) {
    authz := &Authz{
        log: log,
        lookup: lookup,
    }
    return authz, nil
}

func (a *Authz) sessionPermissions(
    sess *pb.Session,
) (*pb.UserPermissions) {
    if a.lookup == nil {
        return nil
    }
    return a.lookup.Get(sess)
}

// Simple check that the user in the supplied session object has permission to
// perform the action
func (a *Authz) Check(
    sess *pb.Session,
    checked pb.Permission,
) bool {
    sessPerms := a.sessionPermissions(sess)
    if sessPerms == nil {
        return false
    }
    perms := sessPerms.System.Permissions
    find := []pb.Permission{
        pb.Permission_SUPER,  // SUPER permission is allowed to do anything...
        checked,
    }

    if isRead(checked) {
        find = append(find, pb.Permission_READ_ANY)
    } else if isCreate(checked) {
        find = append(find, pb.Permission_CREATE_ANY)
    } else if isModify(checked) {
        find = append(find, pb.Permission_MODIFY_ANY)
    } else if isDelete(checked) {
        find = append(find, pb.Permission_DELETE_ANY)
    }

    return hasAny(perms, find)
}

// Checks that the user in the supplied session object has permission to
// perform all supplied actions
func (a *Authz) CheckAll(
    sess *pb.Session,
    checked ...pb.Permission,
) bool {
    res := true
    for _, check := range checked {
        res = res && a.Check(sess, check)
    }
    return res
}

func isRead(check pb.Permission) bool {
    return check >= readPermsStart && check <= readPermsEnd
}

func isCreate(check pb.Permission) bool {
    return check >= createPermsStart && check <= createPermsEnd
}

func isModify(check pb.Permission) bool {
    return check >= modifyPermsStart && check <= modifyPermsEnd
}

func isDelete(check pb.Permission) bool {
    return check >= deletePermsStart && check <= deletePermsEnd
}

// Returns true if any of the searched-for permissions are contained in the
// subject permissions
func hasAny(perms []pb.Permission, find []pb.Permission) bool {
    for _, p := range perms {
        for _, f := range find {
            if p == f {
                return true
            }
        }
    }
    return false
}
<commit_msg>Quick method/variable name change in authz<commit_after>package authz

import (
    "github.com/jaypipes/procession/pkg/logging"
    pb "github.com/jaypipes/procession/proto"
)

const (
    readPermsStart = pb.Permission_READ_ANY
    readPermsEnd = (pb.Permission_CREATE_ANY - 1)
    createPermsStart = pb.Permission_CREATE_ANY
    createPermsEnd = (pb.Permission_MODIFY_ANY - 1)
    modifyPermsStart = pb.Permission_MODIFY_ANY
    modifyPermsEnd = (pb.Permission_DELETE_ANY - 1)
    deletePermsStart = pb.Permission_DELETE_ANY
    deletePermsEnd = pb.Permission_END_PERMS
)

// Abstraction layer for authorization implementations
type AuthzLookup interface {
    Get(*pb.Session) (*pb.UserPermissions)
}

type Authz struct {
    log *logging.Logs
    lookup AuthzLookup
}

func New(log *logging.Logs, lookup AuthzLookup) (*Authz, error) {
    authz := &Authz{
        log: log,
        lookup: lookup,
    }
    return authz, nil
}

func (a *Authz) getUserPermissions(
    sess *pb.Session,
) (*pb.UserPermissions) {
    if a.lookup == nil {
        return nil
    }
    return a.lookup.Get(sess)
}

// Simple check that the user in the supplied session object has permission to
// perform the action
func (a *Authz) Check(
    sess *pb.Session,
    checked pb.Permission,
) bool {
    perms := a.getUserPermissions(sess)
    if perms == nil {
        return false
    }
    perms := perms.System.Permissions
    find := []pb.Permission{
        pb.Permission_SUPER,  // SUPER permission is allowed to do anything...
        checked,
    }

    if isRead(checked) {
        find = append(find, pb.Permission_READ_ANY)
    } else if isCreate(checked) {
        find = append(find, pb.Permission_CREATE_ANY)
    } else if isModify(checked) {
        find = append(find, pb.Permission_MODIFY_ANY)
    } else if isDelete(checked) {
        find = append(find, pb.Permission_DELETE_ANY)
    }

    return hasAny(perms, find)
}

// Checks that the user in the supplied session object has permission to
// perform all supplied actions
func (a *Authz) CheckAll(
    sess *pb.Session,
    checked ...pb.Permission,
) bool {
    res := true
    for _, check := range checked {
        res = res && a.Check(sess, check)
    }
    return res
}

func isRead(check pb.Permission) bool {
    return check >= readPermsStart && check <= readPermsEnd
}

func isCreate(check pb.Permission) bool {
    return check >= createPermsStart && check <= createPermsEnd
}

func isModify(check pb.Permission) bool {
    return check >= modifyPermsStart && check <= modifyPermsEnd
}

func isDelete(check pb.Permission) bool {
    return check >= deletePermsStart && check <= deletePermsEnd
}

// Returns true if any of the searched-for permissions are contained in the
// subject permissions
func hasAny(perms []pb.Permission, find []pb.Permission) bool {
    for _, p := range perms {
        for _, f := range find {
            if p == f {
                return true
            }
        }
    }
    return false
}
<|endoftext|>


<commit_before>package cmd

import (
	"fmt"
	"io"
	"io/ioutil"
	"path"
	"reflect"
	"strings"

	kyaml "github.com/ghodss/yaml" // we intentionally use a different yaml package here for Kubernetes objects because gopkg.in/yaml.v2 is not meant to serialize k8s objects because of UnmarshalJSON/UnmarshalYAML and `json:""`/`yaml:""` dichotomy resulting in panic when used
	cmdutil "github.com/redhat-developer/opencompose/pkg/cmd/util"
	"github.com/redhat-developer/opencompose/pkg/encoding"
	"github.com/redhat-developer/opencompose/pkg/object"
	"github.com/redhat-developer/opencompose/pkg/transform"
	"github.com/redhat-developer/opencompose/pkg/transform/kubernetes"
	"github.com/redhat-developer/opencompose/pkg/transform/openshift"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"k8s.io/client-go/pkg/api"
	"k8s.io/client-go/pkg/api/meta"
	"k8s.io/client-go/pkg/runtime"
)

var (
	convertExample = `  # Converts file
  opencompose convert -f opencompose.yaml`
)

func NewCmdConvert(v *viper.Viper, out, outerr io.Writer) *cobra.Command {
	cmd := &cobra.Command{
		Use:     "convert",
		Short:   "Converts OpenCompose files into Kubernetes (and OpenShift) artifacts",
		Long:    "Converts OpenCompose files into Kubernetes (and OpenShift) artifacts",
		Example: convertExample,
		RunE: func(cmd *cobra.Command, args []string) error {
			return RunConvert(v, cmd, out, outerr)
		},
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			if cmd.Parent().PersistentPreRunE != nil {
				if err := cmd.Parent().PersistentPreRunE(cmd, args); err != nil {
					return err
				}
			}

			// We have to bind Viper in Run because there is only one instance to avoid collisions between subcommands
			cmdutil.AddIOFlagsViper(v, cmd)

			return nil
		},
	}

	cmdutil.AddIOFlags(cmd)

	return cmd
}

func GetValidatedObject(v *viper.Viper, cmd *cobra.Command, out, outerr io.Writer) (*object.OpenCompose, error) {
	files := v.GetStringSlice(cmdutil.Flag_File_Key)
	if len(files) < 1 {
		return nil, cmdutil.UsageError(cmd, "there has to be at least one file")
	}

	var ocObjects []*object.OpenCompose
	for _, file := range files {
		data, err := ioutil.ReadFile(file)
		if err != nil {
			return nil, fmt.Errorf("unable to read file '%s': %s", file, err)
		}

		decoder, err := encoding.GetDecoderFor(data)
		if err != nil {
			return nil, fmt.Errorf("could not find decoder for file '%s': %s", file, err)
		}

		o, err := decoder.Decode(data)
		if err != nil {
			return nil, fmt.Errorf("could not unmarsha data for file '%s': %s", file, err)
		}

		ocObjects = append(ocObjects, o)
	}

	// FIXME: implement merging OpenCompose obejcts
	openCompose := ocObjects[0]

	openCompose.Validate()

	return openCompose, nil
}

func RunConvert(v *viper.Viper, cmd *cobra.Command, out, outerr io.Writer) error {
	o, err := GetValidatedObject(v, cmd, out, outerr)
	if err != nil {
		return err
	}

	var transformer transform.Transformer
	distro := v.GetString("distro")
	switch d := strings.ToLower(distro); d {
	case "kubernetes":
		transformer = &kubernetes.Transformer{}
	case "openshift":
		transformer = &openshift.Transformer{}
	default:
		return fmt.Errorf("unknown distro '%s'", distro)
	}

	runtimeObjects, err := transformer.Transform(o)
	if err != nil {
		return fmt.Errorf("transformation failed: %s", err)
	}

	var writeObject func(o runtime.Object, data []byte) error
	outputDir := v.GetString(cmdutil.Flag_OutputDir_Key)
	if outputDir == "-" {
		// don't use dir but write it to out (stdout)
		writeObject = func(o runtime.Object, data []byte) error {
			_, err := fmt.Fprintln(out, "---")
			if err != nil {
				return err
			}

			_, err = out.Write(data)
			return err
		}
	} else {
		// write files
		writeObject = func(o runtime.Object, data []byte) error {
			kind := o.GetObjectKind().GroupVersionKind().Kind
			m, ok := o.(meta.Object)
			if !ok {
				return fmt.Errorf("failed to cast runtime.object to meta.object (type is %s): %s", reflect.TypeOf(o).String(), err)
			}

			filename := fmt.Sprintf("%s-%s.yaml", strings.ToLower(kind), m.GetName())
			return ioutil.WriteFile(path.Join(outputDir, filename), data, 0644)
		}
	}

	for _, runtimeObject := range runtimeObjects {
		gvk, isUnversioned, err := api.Scheme.ObjectKind(runtimeObject)
		if err != nil {
			return fmt.Errorf("ConvertToVersion failed: %s", err)
		}
		if isUnversioned {
			return fmt.Errorf("ConvertToVersion failed: can't output unversioned type: %T", runtimeObject)
		}

		runtimeObject.GetObjectKind().SetGroupVersionKind(gvk)

		data, err := kyaml.Marshal(runtimeObject)
		if err != nil {
			return fmt.Errorf("failed to marshal object: %s", err)
		}

		err = writeObject(runtimeObject, data)
		if err != nil {
			return fmt.Errorf("failed to write object: %s", err)
		}
	}

	return nil
}
<commit_msg>Spell correction in one of the errors<commit_after>package cmd

import (
	"fmt"
	"io"
	"io/ioutil"
	"path"
	"reflect"
	"strings"

	kyaml "github.com/ghodss/yaml" // we intentionally use a different yaml package here for Kubernetes objects because gopkg.in/yaml.v2 is not meant to serialize k8s objects because of UnmarshalJSON/UnmarshalYAML and `json:""`/`yaml:""` dichotomy resulting in panic when used
	cmdutil "github.com/redhat-developer/opencompose/pkg/cmd/util"
	"github.com/redhat-developer/opencompose/pkg/encoding"
	"github.com/redhat-developer/opencompose/pkg/object"
	"github.com/redhat-developer/opencompose/pkg/transform"
	"github.com/redhat-developer/opencompose/pkg/transform/kubernetes"
	"github.com/redhat-developer/opencompose/pkg/transform/openshift"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"k8s.io/client-go/pkg/api"
	"k8s.io/client-go/pkg/api/meta"
	"k8s.io/client-go/pkg/runtime"
)

var (
	convertExample = `  # Converts file
  opencompose convert -f opencompose.yaml`
)

func NewCmdConvert(v *viper.Viper, out, outerr io.Writer) *cobra.Command {
	cmd := &cobra.Command{
		Use:     "convert",
		Short:   "Converts OpenCompose files into Kubernetes (and OpenShift) artifacts",
		Long:    "Converts OpenCompose files into Kubernetes (and OpenShift) artifacts",
		Example: convertExample,
		RunE: func(cmd *cobra.Command, args []string) error {
			return RunConvert(v, cmd, out, outerr)
		},
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			if cmd.Parent().PersistentPreRunE != nil {
				if err := cmd.Parent().PersistentPreRunE(cmd, args); err != nil {
					return err
				}
			}

			// We have to bind Viper in Run because there is only one instance to avoid collisions between subcommands
			cmdutil.AddIOFlagsViper(v, cmd)

			return nil
		},
	}

	cmdutil.AddIOFlags(cmd)

	return cmd
}

func GetValidatedObject(v *viper.Viper, cmd *cobra.Command, out, outerr io.Writer) (*object.OpenCompose, error) {
	files := v.GetStringSlice(cmdutil.Flag_File_Key)
	if len(files) < 1 {
		return nil, cmdutil.UsageError(cmd, "there has to be at least one file")
	}

	var ocObjects []*object.OpenCompose
	for _, file := range files {
		data, err := ioutil.ReadFile(file)
		if err != nil {
			return nil, fmt.Errorf("unable to read file '%s': %s", file, err)
		}

		decoder, err := encoding.GetDecoderFor(data)
		if err != nil {
			return nil, fmt.Errorf("could not find decoder for file '%s': %s", file, err)
		}

		o, err := decoder.Decode(data)
		if err != nil {
			return nil, fmt.Errorf("could not unmarshal data for file '%s': %s", file, err)
		}

		ocObjects = append(ocObjects, o)
	}

	// FIXME: implement merging OpenCompose obejcts
	openCompose := ocObjects[0]

	openCompose.Validate()

	return openCompose, nil
}

func RunConvert(v *viper.Viper, cmd *cobra.Command, out, outerr io.Writer) error {
	o, err := GetValidatedObject(v, cmd, out, outerr)
	if err != nil {
		return err
	}

	var transformer transform.Transformer
	distro := v.GetString("distro")
	switch d := strings.ToLower(distro); d {
	case "kubernetes":
		transformer = &kubernetes.Transformer{}
	case "openshift":
		transformer = &openshift.Transformer{}
	default:
		return fmt.Errorf("unknown distro '%s'", distro)
	}

	runtimeObjects, err := transformer.Transform(o)
	if err != nil {
		return fmt.Errorf("transformation failed: %s", err)
	}

	var writeObject func(o runtime.Object, data []byte) error
	outputDir := v.GetString(cmdutil.Flag_OutputDir_Key)
	if outputDir == "-" {
		// don't use dir but write it to out (stdout)
		writeObject = func(o runtime.Object, data []byte) error {
			_, err := fmt.Fprintln(out, "---")
			if err != nil {
				return err
			}

			_, err = out.Write(data)
			return err
		}
	} else {
		// write files
		writeObject = func(o runtime.Object, data []byte) error {
			kind := o.GetObjectKind().GroupVersionKind().Kind
			m, ok := o.(meta.Object)
			if !ok {
				return fmt.Errorf("failed to cast runtime.object to meta.object (type is %s): %s", reflect.TypeOf(o).String(), err)
			}

			filename := fmt.Sprintf("%s-%s.yaml", strings.ToLower(kind), m.GetName())
			return ioutil.WriteFile(path.Join(outputDir, filename), data, 0644)
		}
	}

	for _, runtimeObject := range runtimeObjects {
		gvk, isUnversioned, err := api.Scheme.ObjectKind(runtimeObject)
		if err != nil {
			return fmt.Errorf("ConvertToVersion failed: %s", err)
		}
		if isUnversioned {
			return fmt.Errorf("ConvertToVersion failed: can't output unversioned type: %T", runtimeObject)
		}

		runtimeObject.GetObjectKind().SetGroupVersionKind(gvk)

		data, err := kyaml.Marshal(runtimeObject)
		if err != nil {
			return fmt.Errorf("failed to marshal object: %s", err)
		}

		err = writeObject(runtimeObject, data)
		if err != nil {
			return fmt.Errorf("failed to write object: %s", err)
		}
	}

	return nil
}
<|endoftext|>


<commit_before>/*
   Copyright 2020 Docker Compose CLI 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 compose

import (
	"context"
	"fmt"
	"io"

	"github.com/compose-spec/compose-go/types"
	"github.com/docker/cli/cli/streams"
	"github.com/docker/compose/v2/pkg/api"
	moby "github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/docker/docker/pkg/stringid"
	"github.com/moby/term"
)

func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
	containerID, err := s.prepareRun(ctx, project, opts)
	if err != nil {
		return 0, err
	}

	if opts.Detach {
		err := s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
		if err != nil {
			return 0, err
		}
		fmt.Fprintln(opts.Stdout, containerID)
		return 0, nil
	}

	return s.runInteractive(ctx, containerID, opts)
}

func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
	r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
	if err != nil {
		return 0, err
	}

	stdin, stdout, err := s.getContainerStreams(ctx, containerID)
	if err != nil {
		return 0, err
	}

	in := streams.NewIn(opts.Stdin)
	if in.IsTerminal() {
		state, err := term.SetRawTerminal(in.FD())
		if err != nil {
			return 0, err
		}
		defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
	}

	outputDone := make(chan error)
	inputDone := make(chan error)

	go func() {
		if opts.Tty {
			_, err := io.Copy(opts.Stdout, stdout) //nolint:errcheck
			outputDone <- err
		} else {
			_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout) //nolint:errcheck
			outputDone <- err
		}
		stdout.Close() //nolint:errcheck
	}()

	go func() {
		_, err := io.Copy(stdin, r)
		inputDone <- err
		stdin.Close() //nolint:errcheck
	}()

	err = s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
	if err != nil {
		return 0, err
	}

	s.monitorTTySize(ctx, containerID, s.apiClient.ContainerResize)

	for {
		select {
		case err := <-outputDone:
			if err != nil {
				return 0, err
			}
			return s.terminateRun(ctx, containerID, opts)
		case err := <-inputDone:
			if _, ok := err.(term.EscapeError); ok {
				return 0, nil
			}
			if err != nil {
				return 0, err
			}
			// Wait for output to complete streaming
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}
}

func (s *composeService) terminateRun(ctx context.Context, containerID string, opts api.RunOptions) (exitCode int, err error) {
	exitCh, errCh := s.apiClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
	select {
	case exit := <-exitCh:
		exitCode = int(exit.StatusCode)
	case err = <-errCh:
		return
	}
	if opts.AutoRemove {
		err = s.apiClient.ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
	}
	return
}

func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
	if err := prepareVolumes(project); err != nil { // all dependencies already checked, but might miss service img
		return "", err
	}
	service, err := project.GetService(opts.Service)
	if err != nil {
		return "", err
	}

	applyRunOptions(project, &service, opts)

	slug := stringid.GenerateRandomID()
	if service.ContainerName == "" {
		service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
	}
	service.Scale = 1
	service.StdinOpen = true
	service.Restart = ""
	if service.Deploy != nil {
		service.Deploy.RestartPolicy = nil
	}
	service.Labels = service.Labels.Add(api.SlugLabel, slug)
	service.Labels = service.Labels.Add(api.OneoffLabel, "True")

	if err := s.ensureImagesExists(ctx, project, false); err != nil { // all dependencies already checked, but might miss service img
		return "", err
	}
	if !opts.NoDeps {
		if err := s.waitDependencies(ctx, project, service); err != nil {
			return "", err
		}
	}

	observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
	if err != nil {
		return "", err
	}
	updateServices(&service, observedState)

	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
	if err != nil {
		return "", err
	}
	containerID := created.ID
	return containerID, nil
}

func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.ReadCloser, error) {
	if isTty {
		return r, nil
	}
	var escapeKeys = []byte{16, 17}
	if s.configFile.DetachKeys != "" {
		customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys)
		if err != nil {
			return nil, err
		}
		escapeKeys = customEscapeKeys
	}
	return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil
}

func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
	service.Tty = opts.Tty
	service.StdinOpen = true
	service.ContainerName = opts.Name

	if len(opts.Command) > 0 {
		service.Command = opts.Command
	}
	if len(opts.User) > 0 {
		service.User = opts.User
	}
	if len(opts.WorkingDir) > 0 {
		service.WorkingDir = opts.WorkingDir
	}
	if opts.Entrypoint != nil {
		service.Entrypoint = opts.Entrypoint
	}
	if len(opts.Environment) > 0 {
		env := types.NewMappingWithEquals(opts.Environment)
		projectEnv := env.Resolve(func(s string) (string, bool) {
			v, ok := project.Environment[s]
			return v, ok
		}).RemoveEmpty()
		service.Environment.OverrideBy(projectEnv)
	}
	for k, v := range opts.Labels {
		service.Labels = service.Labels.Add(k, v)
	}
}
<commit_msg>Actually fix #8811<commit_after>/*
   Copyright 2020 Docker Compose CLI 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 compose

import (
	"context"
	"fmt"
	"io"

	"github.com/compose-spec/compose-go/types"
	"github.com/docker/cli/cli/streams"
	"github.com/docker/compose/v2/pkg/api"
	moby "github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/docker/docker/pkg/stringid"
	"github.com/moby/term"
)

func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
	containerID, err := s.prepareRun(ctx, project, opts)
	if err != nil {
		return 0, err
	}

	if opts.Detach {
		err := s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
		if err != nil {
			return 0, err
		}
		fmt.Fprintln(opts.Stdout, containerID)
		return 0, nil
	}

	return s.runInteractive(ctx, containerID, opts)
}

func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
	r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
	if err != nil {
		return 0, err
	}

	stdin, stdout, err := s.getContainerStreams(ctx, containerID)
	if err != nil {
		return 0, err
	}

	in := streams.NewIn(opts.Stdin)
	if in.IsTerminal() {
		state, err := term.SetRawTerminal(in.FD())
		if err != nil {
			return 0, err
		}
		defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
	}

	outputDone := make(chan error)
	inputDone := make(chan error)

	go func() {
		if opts.Tty {
			_, err := io.Copy(opts.Stdout, stdout) //nolint:errcheck
			outputDone <- err
		} else {
			_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout) //nolint:errcheck
			outputDone <- err
		}
		stdout.Close() //nolint:errcheck
	}()

	go func() {
		_, err := io.Copy(stdin, r)
		inputDone <- err
		stdin.Close() //nolint:errcheck
	}()

	err = s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
	if err != nil {
		return 0, err
	}

	s.monitorTTySize(ctx, containerID, s.apiClient.ContainerResize)

	for {
		select {
		case err := <-outputDone:
			if err != nil {
				return 0, err
			}
			return s.terminateRun(ctx, containerID, opts)
		case err := <-inputDone:
			if _, ok := err.(term.EscapeError); ok {
				return 0, nil
			}
			if err != nil {
				return 0, err
			}
			// Wait for output to complete streaming
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}
}

func (s *composeService) terminateRun(ctx context.Context, containerID string, opts api.RunOptions) (exitCode int, err error) {
	exitCh, errCh := s.apiClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
	select {
	case exit := <-exitCh:
		exitCode = int(exit.StatusCode)
	case err = <-errCh:
		return
	}
	if opts.AutoRemove {
		err = s.apiClient.ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
	}
	return
}

func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
	if err := prepareVolumes(project); err != nil { // all dependencies already checked, but might miss service img
		return "", err
	}
	service, err := project.GetService(opts.Service)
	if err != nil {
		return "", err
	}

	applyRunOptions(project, &service, opts)

	slug := stringid.GenerateRandomID()
	if service.ContainerName == "" {
		service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
	}
	service.Scale = 1
	service.StdinOpen = true
	service.Restart = ""
	if service.Deploy != nil {
		service.Deploy.RestartPolicy = nil
	}
	service.Labels = service.Labels.Add(api.SlugLabel, slug)
	service.Labels = service.Labels.Add(api.OneoffLabel, "True")

	if err := s.ensureImagesExists(ctx, project, false); err != nil { // all dependencies already checked, but might miss service img
		return "", err
	}
	if !opts.NoDeps {
		if err := s.waitDependencies(ctx, project, service); err != nil {
			return "", err
		}
	}

	observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
	if err != nil {
		return "", err
	}
	updateServices(&service, observedState)

	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
	if err != nil {
		return "", err
	}
	containerID := created.ID
	return containerID, nil
}

func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.ReadCloser, error) {
	if !isTty {
		return r, nil
	}
	var escapeKeys = []byte{16, 17}
	if s.configFile.DetachKeys != "" {
		customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys)
		if err != nil {
			return nil, err
		}
		escapeKeys = customEscapeKeys
	}
	return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil
}

func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
	service.Tty = opts.Tty
	service.StdinOpen = true
	service.ContainerName = opts.Name

	if len(opts.Command) > 0 {
		service.Command = opts.Command
	}
	if len(opts.User) > 0 {
		service.User = opts.User
	}
	if len(opts.WorkingDir) > 0 {
		service.WorkingDir = opts.WorkingDir
	}
	if opts.Entrypoint != nil {
		service.Entrypoint = opts.Entrypoint
	}
	if len(opts.Environment) > 0 {
		env := types.NewMappingWithEquals(opts.Environment)
		projectEnv := env.Resolve(func(s string) (string, bool) {
			v, ok := project.Environment[s]
			return v, ok
		}).RemoveEmpty()
		service.Environment.OverrideBy(projectEnv)
	}
	for k, v := range opts.Labels {
		service.Labels = service.Labels.Add(k, v)
	}
}
<|endoftext|>


<commit_before>package ec2

//-----------------------------------------------------------------------------
// Package factored import statement:
//-----------------------------------------------------------------------------

import (

	// Stdlib:
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"

	// Community:
	log "github.com/Sirupsen/logrus"
	"github.com/katosys/kato/pkg/tools"
)

//-----------------------------------------------------------------------------
// func: Add
//-----------------------------------------------------------------------------

// Add a new instance to the cluster.
func (d *Data) Add() {

	// Set current command:
	d.command = "add"

	// Load state from state file:
	if err := d.loadState(); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Retrieve the CoreOS AMI ID:
	var err error
	if d.AmiID, err = d.retrieveCoreOSAmiID(); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Execute the udata|run pipeline:
	out, err := tools.ExecutePipeline(d.forgeUdataCommand(), d.forgeRunCommand())
	if err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Publish DNS records:
	if err := d.publishDNSRecords(d.Roles, out); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Warning(err)
	}
}

//-----------------------------------------------------------------------------
// func: retrieveCoreOSAmiID
//-----------------------------------------------------------------------------

func (d *Data) retrieveCoreOSAmiID() (string, error) {

	// Send the request:
	res, err := http.Get("https://coreos.com/dist/aws/aws-" +
		d.CoreOSChannel + ".json")
	if err != nil {
		return "", err
	}

	// Retrieve the data:
	data, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return "", err
	}

	// Close the handler:
	if err = res.Body.Close(); err != nil {
		return "", err
	}

	// Decode JSON into Go values:
	var jsonData map[string]interface{}
	if err := json.Unmarshal(data, &jsonData); err != nil {
		return "", err
	}

	// Store the AMI ID:
	amis := jsonData[d.Region].(map[string]interface{})
	amiID := amis["hvm"].(string)

	// Log this action:
	log.WithFields(log.Fields{"cmd": "ec2:" + d.command, "id": amiID}).
		Info("Latest CoreOS " + d.CoreOSChannel + " AMI located")

	// Return the AMI ID:
	return amiID, nil
}

//-----------------------------------------------------------------------------
// func: forgeUdataCommand
//-----------------------------------------------------------------------------

func (d *Data) forgeUdataCommand() *exec.Cmd {

	// Udata arguments bundle:
	args := []string{"udata",
		"--roles", d.Roles,
		"--cluster-id", d.ClusterID,
		"--cluster-state", d.ClusterState,
		"--quorum-count", strconv.Itoa(d.QuorumCount),
		"--master-count", strconv.Itoa(d.MasterCount),
		"--host-name", d.HostName,
		"--host-id", d.HostID,
		"--domain", d.Domain,
		"--ec2-region", d.Region,
		"--dns-provider", d.DNSProvider,
		"--dns-api-key", d.DNSApiKey,
		"--etcd-token", d.EtcdToken,
		"--calico-ip-pool", d.CalicoIPPool,
		"--rexray-storage-driver", "ebs",
		"--iaas-provider", "ec2",
		"--prometheus",
		"--gzip-udata",
	}

	// Append flags if present:
	if d.SysdigAccessKey != "" {
		args = append(args, "--sysdig-access-key", d.SysdigAccessKey)
	}
	if d.DatadogAPIKey != "" {
		args = append(args, "--datadog-api-key", d.DatadogAPIKey)
	}
	if d.SlackWebhook != "" {
		args = append(args, "--slack-webhook", d.SlackWebhook)
	}
	if d.CaCertPath != "" {
		args = append(args, "--ca-cert-path", d.CaCertPath)
	}
	for _, z := range d.StubZones {
		args = append(args, "--stub-zone", z)
	}
	if d.SMTPURL != "" {
		args = append(args, "--smtp-url", d.SMTPURL)
	}
	if d.AdminEmail != "" {
		args = append(args, "--admin-email", d.AdminEmail)
	}

	// Forge the command and return:
	return exec.Command("katoctl", args...)
}

//-----------------------------------------------------------------------------
// func: forgeRunCommand
//-----------------------------------------------------------------------------

func (d *Data) forgeRunCommand() *exec.Cmd {

	// Ec2 run arguments bundle:
	args := []string{"ec2", "run",
		"--tag-name", d.HostName + "-" + d.HostID + "." + d.Domain,
		"--region", d.Region,
		"--zone", d.Zone,
		"--ami-id", d.AmiID,
		"--instance-type", d.InstanceType,
		"--key-pair", d.KeyPair,
		"--subnet-id", d.ExtSubnetID,
		"--security-group-ids", strings.Join(d.securityGroupIDs(d.Roles), ","),
		"--iam-role", "kato",
		"--source-dest-check", "false",
		"--public-ip", "true",
	}

	// Append flags if present:
	if strings.Contains(d.Roles, "master") {
		i, _ := strconv.Atoi(d.HostID)
		args = append(args, "--private-ip", tools.OffsetIP(d.ExtSubnetCidr, 10+i))
	}
	if strings.Contains(d.Roles, "worker") {
		args = append(args, "--elb-name", d.ClusterID)
	}

	// Forge the command and return:
	return exec.Command("katoctl", args...)
}

//-----------------------------------------------------------------------------
// func: securityGroupIDs
//-----------------------------------------------------------------------------

func (d *Data) securityGroupIDs(roles string) (list []string) {
	for _, role := range strings.Split(roles, ",") {
		switch role {
		case "quorum":
			list = append(list, d.QuorumSecGrp)
		case "master":
			list = append(list, d.MasterSecGrp)
		case "worker":
			list = append(list, d.WorkerSecGrp)
		case "border":
			list = append(list, d.BorderSecGrp)
		}
	}
	return
}

//-----------------------------------------------------------------------------
// func: publishDNSRecords
//-----------------------------------------------------------------------------

func (d *Data) publishDNSRecords(roles string, out []byte) error {

	// Retrieve the instance IPs:
	var dat map[string]string
	if err := json.Unmarshal(out, &dat); err != nil {
		return err
	}

	// For every role in this instance:
	for _, role := range strings.Split(roles, ",") {

		// Forge the internal record command:
		cmdInt := exec.Command("katoctl", d.DNSProvider,
			"--api-key", d.DNSApiKey,
			"record", "add",
			"--zone", "int."+d.Domain,
			dat["internal"]+":A:"+role+"-"+d.HostID)

		// Execute the internal record command:
		cmdInt.Stderr = os.Stderr
		if err := cmdInt.Run(); err != nil {
			return err
		}

		// Forge the external record command:
		cmdExt := exec.Command("katoctl", d.DNSProvider,
			"--api-key", d.DNSApiKey,
			"record", "add",
			"--zone", "ext."+d.Domain,
			dat["external"]+":A:"+role+"-"+d.HostID)

		// Execute the external record command:
		cmdExt.Stderr = os.Stderr
		if err := cmdExt.Run(); err != nil {
			return err
		}
	}

	return nil
}
<commit_msg>Fix #55 reorder parameters<commit_after>package ec2

//-----------------------------------------------------------------------------
// Package factored import statement:
//-----------------------------------------------------------------------------

import (

	// Stdlib:
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"

	// Community:
	log "github.com/Sirupsen/logrus"
	"github.com/katosys/kato/pkg/tools"
)

//-----------------------------------------------------------------------------
// func: Add
//-----------------------------------------------------------------------------

// Add a new instance to the cluster.
func (d *Data) Add() {

	// Set current command:
	d.command = "add"

	// Load state from state file:
	if err := d.loadState(); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Retrieve the CoreOS AMI ID:
	var err error
	if d.AmiID, err = d.retrieveCoreOSAmiID(); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Execute the udata|run pipeline:
	out, err := tools.ExecutePipeline(d.forgeUdataCommand(), d.forgeRunCommand())
	if err != nil {
		log.WithField("cmd", "ec2:"+d.command).Fatal(err)
	}

	// Publish DNS records:
	if err := d.publishDNSRecords(d.Roles, out); err != nil {
		log.WithField("cmd", "ec2:"+d.command).Warning(err)
	}
}

//-----------------------------------------------------------------------------
// func: retrieveCoreOSAmiID
//-----------------------------------------------------------------------------

func (d *Data) retrieveCoreOSAmiID() (string, error) {

	// Send the request:
	res, err := http.Get("https://coreos.com/dist/aws/aws-" +
		d.CoreOSChannel + ".json")
	if err != nil {
		return "", err
	}

	// Retrieve the data:
	data, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return "", err
	}

	// Close the handler:
	if err = res.Body.Close(); err != nil {
		return "", err
	}

	// Decode JSON into Go values:
	var jsonData map[string]interface{}
	if err := json.Unmarshal(data, &jsonData); err != nil {
		return "", err
	}

	// Store the AMI ID:
	amis := jsonData[d.Region].(map[string]interface{})
	amiID := amis["hvm"].(string)

	// Log this action:
	log.WithFields(log.Fields{"cmd": "ec2:" + d.command, "id": amiID}).
		Info("Latest CoreOS " + d.CoreOSChannel + " AMI located")

	// Return the AMI ID:
	return amiID, nil
}

//-----------------------------------------------------------------------------
// func: forgeUdataCommand
//-----------------------------------------------------------------------------

func (d *Data) forgeUdataCommand() *exec.Cmd {

	// Udata arguments bundle:
	args := []string{"udata",
		"--roles", d.Roles,
		"--cluster-id", d.ClusterID,
		"--cluster-state", d.ClusterState,
		"--quorum-count", strconv.Itoa(d.QuorumCount),
		"--master-count", strconv.Itoa(d.MasterCount),
		"--host-name", d.HostName,
		"--host-id", d.HostID,
		"--domain", d.Domain,
		"--ec2-region", d.Region,
		"--dns-provider", d.DNSProvider,
		"--dns-api-key", d.DNSApiKey,
		"--etcd-token", d.EtcdToken,
		"--calico-ip-pool", d.CalicoIPPool,
		"--rexray-storage-driver", "ebs",
		"--iaas-provider", "ec2",
		"--prometheus",
		"--gzip-udata",
	}

	// Append flags if present:
	if d.SysdigAccessKey != "" {
		args = append(args, "--sysdig-access-key", d.SysdigAccessKey)
	}
	if d.DatadogAPIKey != "" {
		args = append(args, "--datadog-api-key", d.DatadogAPIKey)
	}
	if d.SlackWebhook != "" {
		args = append(args, "--slack-webhook", d.SlackWebhook)
	}
	if d.CaCertPath != "" {
		args = append(args, "--ca-cert-path", d.CaCertPath)
	}
	for _, z := range d.StubZones {
		args = append(args, "--stub-zone", z)
	}
	if d.SMTPURL != "" {
		args = append(args, "--smtp-url", d.SMTPURL)
	}
	if d.AdminEmail != "" {
		args = append(args, "--admin-email", d.AdminEmail)
	}

	// Forge the command and return:
	return exec.Command("katoctl", args...)
}

//-----------------------------------------------------------------------------
// func: forgeRunCommand
//-----------------------------------------------------------------------------

func (d *Data) forgeRunCommand() *exec.Cmd {

	// Ec2 run arguments bundle:
	args := []string{"ec2", "run",
		"--tag-name", d.HostName + "-" + d.HostID + "." + d.Domain,
		"--region", d.Region,
		"--zone", d.Zone,
		"--ami-id", d.AmiID,
		"--instance-type", d.InstanceType,
		"--key-pair", d.KeyPair,
		"--subnet-id", d.ExtSubnetID,
		"--security-group-ids", strings.Join(d.securityGroupIDs(d.Roles), ","),
		"--iam-role", "kato",
		"--source-dest-check", "false",
		"--public-ip", "true",
	}

	// Append flags if present:
	if strings.Contains(d.Roles, "master") {
		i, _ := strconv.Atoi(d.HostID)
		args = append(args, "--private-ip", tools.OffsetIP(d.ExtSubnetCidr, 10+i))
	}
	if strings.Contains(d.Roles, "worker") {
		args = append(args, "--elb-name", d.ClusterID)
	}

	// Forge the command and return:
	return exec.Command("katoctl", args...)
}

//-----------------------------------------------------------------------------
// func: securityGroupIDs
//-----------------------------------------------------------------------------

func (d *Data) securityGroupIDs(roles string) (list []string) {
	for _, role := range strings.Split(roles, ",") {
		switch role {
		case "quorum":
			list = append(list, d.QuorumSecGrp)
		case "master":
			list = append(list, d.MasterSecGrp)
		case "worker":
			list = append(list, d.WorkerSecGrp)
		case "border":
			list = append(list, d.BorderSecGrp)
		}
	}
	return
}

//-----------------------------------------------------------------------------
// func: publishDNSRecords
//-----------------------------------------------------------------------------

func (d *Data) publishDNSRecords(roles string, out []byte) error {

	// Retrieve the instance IPs:
	var dat map[string]string
	if err := json.Unmarshal(out, &dat); err != nil {
		return err
	}

	// For every role in this instance:
	for _, role := range strings.Split(roles, ",") {

		// Forge the internal record command:
		cmdInt := exec.Command("katoctl", d.DNSProvider,
			"--api-key", d.DNSApiKey,
			"record", "add",
			"--zone", "int."+d.Domain,
			role+"-"+d.HostID+":A:"+dat["internal"])

		// Execute the internal record command:
		cmdInt.Stderr = os.Stderr
		if err := cmdInt.Run(); err != nil {
			return err
		}

		// Forge the external record command:
		cmdExt := exec.Command("katoctl", d.DNSProvider,
			"--api-key", d.DNSApiKey,
			"record", "add",
			"--zone", "ext."+d.Domain,
			role+"-"+d.HostID+":A:"+dat["external"])

		// Execute the external record command:
		cmdExt.Stderr = os.Stderr
		if err := cmdExt.Run(); err != nil {
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>// Package group implements an actor-runner with deterministic teardown.
package group

// Group collects actors (functions) and runs them concurrently.
// When one actor (function) returns, the others are interrupted.
// The zero value of a Group is useful.
type Group struct {
	actors []actor
}

// Add an actor (function) to the group. Each actor must be pre-emptable by an
// interrupt function. That is, if interrupt is invoked, execute should return.
// Also, it must be safe to call interrupt even after execute has returned.
//
// To add a general processing function, you can use a cancel chan.
//
//    cancel := make(chan struct{})
//    g.Add(func() error {
//        select {
//        case <-time.After(5 * time.Second):
//            return errors.New("time elapsed")
//        case <-cancel:
//            return errors.New("canceled")
//        }
//    }, func(error) {
//        close(cancel)
//    })
//
// This is a lower-level version of the semantics provided by contexts. So, if
// your actor goroutine respects contexts, then you can easily accomplish the
// same thing that way.
//
//    ctx, cancel := context.WithCancel(context.Background())
//    g.Add(func() error {
//        select {
//        case <-time.After(5 * time.Second):
//            return errors.New("time elapsed")
//        case <-ctx.Done():
//            return ctx.Error()
//        }
//    }, func(error) {
//        cancel()
//    })
//
// To add an e.g. HTTP server, you'll need to provide an explicit listener, so
// that it may be interrupted.
//
//    ln, _ := net.Listen("tcp", "0.0.0.0:8080")
//    g.Add(func() error {
//        return http.Serve(ln, http.NewServeMux())
//    }, func(error) {
//        ln.Close()
//    })
//
func (g *Group) Add(execute func() error, interrupt func(error)) {
	g.actors = append(g.actors, actor{execute, interrupt})
}

// Run all actors (functions) concurrently.
// When the first actor returns, all others are interrupted.
// Run only returns when all actors (functions) have exited.
func (g *Group) Run() error {
	if len(g.actors) == 0 {
		return nil
	}

	// Run each actor.
	errors := make(chan error, len(g.actors))
	for _, a := range g.actors {
		go func(a actor) {
			errors <- a.execute()
		}(a)
	}

	// Wait for the first one to stop.
	err := <-errors

	// Signal all others to stop.
	for _, a := range g.actors {
		a.interrupt(err)
	}

	// Wait for them all to stop.
	for i := 1; i < cap(errors); i++ {
		<-errors
	}

	// Return the original error.
	return err
}

type actor struct {
	execute   func() error
	interrupt func(error)
}
<commit_msg>pkg/group: add more detail to package doc<commit_after>// Package group implements an actor-runner with deterministic teardown. It is
// very similar to package errgroup, except it does not require actor goroutines
// to understand context semantics. This makes it suitable for use in more
// circumstances; for example, goroutines which are handling connections from
// net.Listeners, or scanning input from a closable io.Reader.
package group

// Group collects actors (functions) and runs them concurrently.
// When one actor (function) returns, the others are interrupted.
// The zero value of a Group is useful.
type Group struct {
	actors []actor
}

// Add an actor (function) to the group. Each actor must be pre-emptable by an
// interrupt function. That is, if interrupt is invoked, execute should return.
// Also, it must be safe to call interrupt even after execute has returned.
//
// To add a general processing function, you can use a cancel chan.
//
//    cancel := make(chan struct{})
//    g.Add(func() error {
//        select {
//        case <-time.After(5 * time.Second):
//            return errors.New("time elapsed")
//        case <-cancel:
//            return errors.New("canceled")
//        }
//    }, func(error) {
//        close(cancel)
//    })
//
// This is a lower-level version of the semantics provided by contexts. So, if
// your actor goroutine respects contexts, then you can easily accomplish the
// same thing that way.
//
//    ctx, cancel := context.WithCancel(context.Background())
//    g.Add(func() error {
//        select {
//        case <-time.After(5 * time.Second):
//            return errors.New("time elapsed")
//        case <-ctx.Done():
//            return ctx.Error()
//        }
//    }, func(error) {
//        cancel()
//    })
//
// To add an e.g. HTTP server, you'll need to provide an explicit listener, so
// that it may be interrupted.
//
//    ln, _ := net.Listen("tcp", "0.0.0.0:8080")
//    g.Add(func() error {
//        return http.Serve(ln, http.NewServeMux())
//    }, func(error) {
//        ln.Close()
//    })
//
func (g *Group) Add(execute func() error, interrupt func(error)) {
	g.actors = append(g.actors, actor{execute, interrupt})
}

// Run all actors (functions) concurrently.
// When the first actor returns, all others are interrupted.
// Run only returns when all actors (functions) have exited.
func (g *Group) Run() error {
	if len(g.actors) == 0 {
		return nil
	}

	// Run each actor.
	errors := make(chan error, len(g.actors))
	for _, a := range g.actors {
		go func(a actor) {
			errors <- a.execute()
		}(a)
	}

	// Wait for the first one to stop.
	err := <-errors

	// Signal all others to stop.
	for _, a := range g.actors {
		a.interrupt(err)
	}

	// Wait for them all to stop.
	for i := 1; i < cap(errors); i++ {
		<-errors
	}

	// Return the original error.
	return err
}

type actor struct {
	execute   func() error
	interrupt func(error)
}
<|endoftext|>


<commit_before>package proto

import (
	"os"
	"io"
	"fmt"
	"strings"
	"strconv"
	"net/textproto"
)

// Operations
const (
	SendReq = "send req"
	SendRes = "send res"
	SendErr = "send err"
	ReadReq = "read req"
	ReadRes = "read res"
)

// Errors we can have
const (
	InvalidCommand = "invalid command"
)

type Line string

type Conn struct {
	*textproto.Conn
	RedirectAddr string
}

type ProtoError struct {
	Id    uint
	Op    string
	Error os.Error
}

func (e *ProtoError) String() string {
	return fmt.Sprintf("%s %d: %s", e.Op, e.Id, e.Error)
}

type ResponseError string

func (e ResponseError) String() string {
	return string(e)
}

type Redirect string

func (e Redirect) String() string {
	return "redirect to " + e.Addr()
}

func (e Redirect) Addr() string {
	return string(e)
}

func NewConn(conn io.ReadWriteCloser) *Conn {
	return &Conn{Conn: textproto.NewConn(conn)}
}

// Server functions

func (c *Conn) SendResponse(id uint, data interface{}) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := encode(&c.Writer, data)
	if err != nil {
		return &ProtoError{id, SendRes, err}
	}
	return nil
}

func (c *Conn) SendError(id uint, msg string) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := c.PrintfLine("-ERR: %s", msg)
	if err != nil {
		return &ProtoError{id, SendErr, err}
	}
	return nil
}

func (c *Conn) SendRedirect(id uint, addr string) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := c.PrintfLine("-REDIRECT: %s", addr)
	if err != nil {
		return &ProtoError{id, SendErr, err}
	}
	return nil
}

func (c *Conn) ReadRequest() (uint, []string, os.Error) {
	id := c.Next()
	c.StartRequest(id)
	parts, err := decode(&c.Reader)
	c.EndRequest(id)
	if err != nil {
		if err == os.EOF {
			return 0, nil, err
		} else {
			return 0, nil, &ProtoError{id, ReadReq, err}
		}
	}
	return id, parts, nil
}

// Client functions

func (c *Conn) SendRequest(data interface{}) (uint, os.Error) {
	id := c.Next()
	c.StartRequest(id)
	err := encode(&c.Writer, data)
	c.EndRequest(id)
	if err != nil {
		return 0, &ProtoError{id, SendReq, err}
	}
	return id, nil
}

func (c *Conn) ReadResponse(id uint) ([]string, os.Error) {
	c.StartResponse(id)
	defer c.EndResponse(id)

	parts, err := decode(&c.Reader)

	switch terr := err.(type) {
	default:
		return nil, &ProtoError{id, ReadRes, err}
	case nil:
		return parts, nil
	case ResponseError:
		if terr[0:9] == "REDIRECT:" {
			c.RedirectAddr = strings.TrimSpace(string(terr)[10:])
			err = Redirect(c.RedirectAddr)
		}
		return nil, err
	}

	panic("unreachable")
}

// Helpers

func decode(r *textproto.Reader) (parts []string, err os.Error) {
	var count int = 1
	var size int
	var line string

Loop:
	for count > 0 {
		// TODO: test if len(line) == 0
		line, err = r.ReadLine()
		switch {
		case err == os.EOF:
			return
		case err != nil:
			panic(err)
		}
		line = strings.TrimSpace(line)
		if len(line) < 1 {
			continue Loop
		}
		switch line[0] {
		case '-':
			err = ResponseError(line[1:])
			return
		case '*':
			count, _ = strconv.Atoi(line[1:])
			parts = make([]string, count)
		case '$':
			// TODO: test for err
			size, _ = strconv.Atoi(line[1:])
			buf := make([]byte, size)
			// TODO: test for err
			n, err := io.ReadFull(r.R, buf)
			switch {
			case n != size:
				panic(fmt.Sprintf("n:%d\n", n))
			case err != nil:
				panic(err)
			}
			parts[len(parts)-count] = string(buf)
			count--
		}
	}
	return
}

func encode(w *textproto.Writer, data interface{}) (err os.Error) {
	switch t := data.(type) {
	default:
		return os.NewError(fmt.Sprintf("unexpected type %T", t))
	case Line:
		if err = w.PrintfLine("+%s", t); err != nil {
			return
		}
	case os.Error:
		if err = w.PrintfLine("-%s", t.String()); err != nil {
			return
		}
	case nil:
		if err = w.PrintfLine("$-1"); err != nil {
			return
		}
	case int:
		return encodeInt64(w, int64(t))
	case int8:
		return encodeInt64(w, int64(t))
	case int16:
		return encodeInt64(w, int64(t))
	case int32:
		return encodeInt64(w, int64(t))
	case int64:
		return encodeInt64(w, t)
	case uint:
		return encodeUint64(w, uint64(t))
	case uint8:
		return encodeUint64(w, uint64(t))
	case uint16:
		return encodeUint64(w, uint64(t))
	case uint32:
		return encodeUint64(w, uint64(t))
	case uint64:
		return encodeUint64(w, t)
	case string:
		return encodeBytes(w, []byte(t))
	case []byte:
		return encodeBytes(w, t)
	case []string:
		// TODO use the builtin append function when it gets released:
		//return encodeSlice(w, append([]interface{}, t...))
		d := make([]interface{}, len(t))
		for i, x := range t {
			d[i] = x
		}
		return encodeSlice(w, d)
	case []interface{}:
		return encodeSlice(w, t)
	}
	return nil
}

func encodeInt64(w *textproto.Writer, data int64) os.Error {
	return w.PrintfLine(":%d", data)
}

func encodeUint64(w *textproto.Writer, data uint64) os.Error {
	return w.PrintfLine(":%d", data)
}

func encodeBytes(w *textproto.Writer, data []byte) (err os.Error) {
	if err = w.PrintfLine("$%d", len(data)); err != nil {
		return
	}
	if err = w.PrintfLine("%s", data); err != nil {
		return
	}
	return nil
}

func encodeSlice(w *textproto.Writer, data []interface{}) (err os.Error) {
	if err = w.PrintfLine("*%d", len(data)); err != nil {
		return
	}
	for _, part := range data {
		if err = encode(w, part); err != nil {
			return
		}
	}
	return nil
}
<commit_msg>more careful error handling<commit_after>package proto

import (
	"junta/util"
	"os"
	"io"
	"fmt"
	"strings"
	"strconv"
	"net/textproto"
)

// Operations
const (
	SendReq = "send req"
	SendRes = "send res"
	SendErr = "send err"
	ReadReq = "read req"
	ReadRes = "read res"
)

// Errors we can have
const (
	InvalidCommand = "invalid command"
)

var logger = util.NewLogger("proto")

type Line string

type Conn struct {
	*textproto.Conn
	RedirectAddr string
}

type ProtoError struct {
	Id    uint
	Op    string
	Error os.Error
}

func (e *ProtoError) String() string {
	return fmt.Sprintf("%s %d: %s", e.Op, e.Id, e.Error)
}

type ResponseError string

func (e ResponseError) String() string {
	return string(e)
}

type Redirect string

func (e Redirect) String() string {
	return "redirect to " + e.Addr()
}

func (e Redirect) Addr() string {
	return string(e)
}

func NewConn(conn io.ReadWriteCloser) *Conn {
	return &Conn{Conn: textproto.NewConn(conn)}
}

// Server functions

func (c *Conn) SendResponse(id uint, data interface{}) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := encode(&c.Writer, data)
	if err != nil {
		return &ProtoError{id, SendRes, err}
	}
	return nil
}

func (c *Conn) SendError(id uint, msg string) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := c.PrintfLine("-ERR: %s", msg)
	if err != nil {
		return &ProtoError{id, SendErr, err}
	}
	return nil
}

func (c *Conn) SendRedirect(id uint, addr string) os.Error {
	c.StartResponse(id)
	defer c.EndResponse(id)
	err := c.PrintfLine("-REDIRECT: %s", addr)
	if err != nil {
		return &ProtoError{id, SendErr, err}
	}
	return nil
}

func (c *Conn) ReadRequest() (uint, []string, os.Error) {
	id := c.Next()
	c.StartRequest(id)
	parts, err := decode(&c.Reader)
	c.EndRequest(id)
	if err != nil {
		if err == os.EOF {
			return 0, nil, err
		} else {
			return 0, nil, &ProtoError{id, ReadReq, err}
		}
	}
	return id, parts, nil
}

// Client functions

func (c *Conn) SendRequest(data interface{}) (uint, os.Error) {
	id := c.Next()
	c.StartRequest(id)
	err := encode(&c.Writer, data)
	c.EndRequest(id)
	if err != nil {
		return 0, &ProtoError{id, SendReq, err}
	}
	return id, nil
}

func (c *Conn) ReadResponse(id uint) ([]string, os.Error) {
	c.StartResponse(id)
	defer c.EndResponse(id)

	parts, err := decode(&c.Reader)

	switch terr := err.(type) {
	default:
		return nil, &ProtoError{id, ReadRes, err}
	case nil:
		return parts, nil
	case ResponseError:
		if terr[0:9] == "REDIRECT:" {
			c.RedirectAddr = strings.TrimSpace(string(terr)[10:])
			err = Redirect(c.RedirectAddr)
		}
		return nil, err
	}

	panic("unreachable")
}

// Helpers

func decode(r *textproto.Reader) (parts []string, err os.Error) {
	var count int = 1
	var size int
	var line string

Loop:
	for count > 0 {
		line, err = r.ReadLine()
		if err != nil {
			return nil, err
		}
		line = strings.TrimSpace(line)
		if len(line) < 1 {
			continue Loop
		}
		switch line[0] {
		case '-':
			err = ResponseError(line[1:])
			return
		case '*':
			count, _ = strconv.Atoi(line[1:])
			parts = make([]string, count)
		case '$':
			size, err = strconv.Atoi(line[1:])
			if err != nil {
				return nil, err
			}
			buf := make([]byte, size)
			_, err := io.ReadFull(r.R, buf)
			if err != nil {
				return nil, err
			}
			parts[len(parts)-count] = string(buf)
			count--
		}
	}
	return
}

func encode(w *textproto.Writer, data interface{}) (err os.Error) {
	switch t := data.(type) {
	default:
		return os.NewError(fmt.Sprintf("unexpected type %T", t))
	case Line:
		if err = w.PrintfLine("+%s", t); err != nil {
			return
		}
	case os.Error:
		if err = w.PrintfLine("-%s", t.String()); err != nil {
			return
		}
	case nil:
		if err = w.PrintfLine("$-1"); err != nil {
			return
		}
	case int:
		return encodeInt64(w, int64(t))
	case int8:
		return encodeInt64(w, int64(t))
	case int16:
		return encodeInt64(w, int64(t))
	case int32:
		return encodeInt64(w, int64(t))
	case int64:
		return encodeInt64(w, t)
	case uint:
		return encodeUint64(w, uint64(t))
	case uint8:
		return encodeUint64(w, uint64(t))
	case uint16:
		return encodeUint64(w, uint64(t))
	case uint32:
		return encodeUint64(w, uint64(t))
	case uint64:
		return encodeUint64(w, t)
	case string:
		return encodeBytes(w, []byte(t))
	case []byte:
		return encodeBytes(w, t)
	case []string:
		// TODO use the builtin append function when it gets released:
		//return encodeSlice(w, append([]interface{}, t...))
		d := make([]interface{}, len(t))
		for i, x := range t {
			d[i] = x
		}
		return encodeSlice(w, d)
	case []interface{}:
		return encodeSlice(w, t)
	}
	return nil
}

func encodeInt64(w *textproto.Writer, data int64) os.Error {
	return w.PrintfLine(":%d", data)
}

func encodeUint64(w *textproto.Writer, data uint64) os.Error {
	return w.PrintfLine(":%d", data)
}

func encodeBytes(w *textproto.Writer, data []byte) (err os.Error) {
	if err = w.PrintfLine("$%d", len(data)); err != nil {
		return
	}
	if err = w.PrintfLine("%s", data); err != nil {
		return
	}
	return nil
}

func encodeSlice(w *textproto.Writer, data []interface{}) (err os.Error) {
	if err = w.PrintfLine("*%d", len(data)); err != nil {
		return
	}
	for _, part := range data {
		if err = encode(w, part); err != nil {
			return
		}
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016-2017 Authors of Cilium
//
// 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 proxy

import (
	"fmt"
	"math/rand"
	"net"
	"strconv"
	"sync"
	"time"

	"github.com/cilium/cilium/common/addressing"
	"github.com/cilium/cilium/pkg/endpointmanager"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/node"
	"github.com/cilium/cilium/pkg/policy"
	"github.com/cilium/cilium/pkg/proxy/accesslog"

	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

var (
	log          = logging.DefaultLogger
	perFlowDebug = false
)

// Magic markers are attached to each packet. The lower 16 bits are used to
// identify packets which have gone through the proxy and to determine whether
// the packet is coming from a proxy at ingress or egress. The marking is
// compatible with Kubernetes's use of the packet mark.  The upper 16 bits can
// be used to carry the security identity.
const (
	magicMarkIngress int = 0x0FEA
	magicMarkEgress  int = 0x0FEB
	magicMarkK8sMasq int = 0x4000
	magicMarkK8sDrop int = 0x8000
)

// field names used while logging
const (
	fieldMarker          = "marker"
	fieldSocket          = "socket"
	fieldFd              = "fd"
	fieldProxyRedirectID = "id"

	// portReleaseDelay is the delay until a port is being released
	portReleaseDelay = time.Duration(5) * time.Minute
)

// Redirect is the generic proxy redirect interface that each proxy redirect
// type must export
type Redirect interface {
	ToPort() uint16
	UpdateRules(l4 *policy.L4Filter, completions policy.CompletionContainer) error
	getSource() ProxySource
	Close(completions policy.CompletionContainer)
	IsIngress() bool
}

// GetMagicMark returns the magic marker with which each packet must be marked.
// The mark is different depending on whether the proxy is injected at ingress
// or egress.
func GetMagicMark(isIngress bool, identity int) int {
	mark := 0

	if isIngress {
		mark = magicMarkIngress
	} else {
		mark = magicMarkEgress
	}

	if identity != 0 {
		mark |= identity << 16
	}

	return mark
}

// ProxySource returns information about the endpoint being proxied.
type ProxySource interface {
	GetID() uint64
	RLock()
	RUnlock()
	Lock()
	Unlock()
	GetLabels() []string
	GetLabelsSHA() string
	GetIdentity() policy.NumericIdentity
	ResolveIdentity(policy.NumericIdentity) *policy.Identity
	GetIPv4Address() string
	GetIPv6Address() string
}

// Proxy maintains state about redirects
type Proxy struct {
	// mutex is the lock required when modifying any proxy datastructure
	mutex lock.RWMutex

	// rangeMin is the minimum port used for proxy port allocation
	rangeMin uint16

	// rangeMax is the maximum port used for proxy port allocation.
	// If port is unspecified, the proxy will automatically allocate
	// ports out of the rangeMin-rangeMax range.
	rangeMax uint16

	// allocatedPorts is a map of all allocated proxy ports pointing
	// to the redirect rules attached to that port
	allocatedPorts map[uint16]Redirect

	// redirects is a map of all redirect configurations indexed by
	// the redirect identifier. Redirects may be implemented by different
	// proxies.
	redirects map[string]Redirect
}

// NewProxy creates a Proxy to keep track of redirects.
func NewProxy(minPort uint16, maxPort uint16) *Proxy {
	return &Proxy{
		rangeMin:       minPort,
		rangeMax:       maxPort,
		redirects:      make(map[string]Redirect),
		allocatedPorts: make(map[uint16]Redirect),
	}
}

var (
	portRandomizer = rand.New(rand.NewSource(time.Now().UnixNano()))
)

func (p *Proxy) allocatePort() (uint16, error) {
	for _, r := range portRandomizer.Perm(int(p.rangeMax - p.rangeMin + 1)) {
		resPort := uint16(r) + p.rangeMin

		if _, ok := p.allocatedPorts[resPort]; !ok {
			return resPort, nil
		}

	}

	return 0, fmt.Errorf("no available proxy ports")
}

var gcOnce sync.Once

// localEndpointInfo fills the access log with the local endpoint info.
func localEndpointInfo(r Redirect, info *accesslog.EndpointInfo) {
	source := r.getSource()
	source.Lock()
	info.ID = source.GetID()
	info.IPv4 = source.GetIPv4Address()
	info.IPv6 = source.GetIPv6Address()
	info.Labels = source.GetLabels()
	info.LabelsSHA256 = source.GetLabelsSHA()
	info.Identity = uint64(source.GetIdentity())
	source.Unlock()
}

func fillInfo(r Redirect, l *accesslog.LogRecord, srcIPPort, dstIPPort string, srcIdentity uint32) {

	ingress := r.IsIngress()

	if ingress {
		// At ingress the local origin endpoint is the destination
		localEndpointInfo(r, &l.DestinationEndpoint)
	} else {
		// At egress, the local origin endpoint is the source
		localEndpointInfo(r, &l.SourceEndpoint)
	}

	l.IPVersion = accesslog.VersionIPv4
	ipstr, port, err := net.SplitHostPort(srcIPPort)
	if err == nil {
		ip := net.ParseIP(ipstr)
		if ip != nil && ip.To4() == nil {
			l.IPVersion = accesslog.VersionIPV6
		}

		p, err := strconv.ParseUint(port, 10, 16)
		if err == nil {
			l.SourceEndpoint.Port = uint16(p)
			if ingress {
				fillIngressSourceInfo(&l.SourceEndpoint, &ip, srcIdentity)
			}
		}
	}

	ipstr, port, err = net.SplitHostPort(dstIPPort)
	if err == nil {
		p, err := strconv.ParseUint(port, 10, 16)
		if err == nil {
			l.DestinationEndpoint.Port = uint16(p)
			if !ingress {
				fillEgressDestinationInfo(&l.DestinationEndpoint, ipstr)
			}
		}
	}
}

// fillIdentity resolves the labels of the specified identity if known
// locally and fills in the following info member fields:
//  - info.Identity
//  - info.Labels
//  - info.LabelsSHA256
func fillIdentity(info *accesslog.EndpointInfo, id policy.NumericIdentity) {
	info.Identity = uint64(id)

	if identity := policy.LookupIdentityByID(id); identity != nil {
		info.Labels = identity.Labels.GetModel()
		info.LabelsSHA256 = identity.GetLabelsSHA256()
	}
}

// fillEndpointInfo tries to resolve the IP address and fills the EndpointInfo
// fields with either ReservedIdentityHost or ReservedIdentityWorld
func fillEndpointInfo(info *accesslog.EndpointInfo, ip net.IP) {
	if ip.To4() != nil {
		info.IPv4 = ip.String()

		// first we try to resolve and check if the IP is
		// same as Host
		if node.IsHostIPv4(ip) {
			fillIdentity(info, policy.ReservedIdentityHost)
			return
		}

		// If Host IP check fails, we try to resolve and check
		// if IP belongs to the cluster.
		if node.GetIPv4ClusterRange().Contains(ip) {
			c := addressing.DeriveCiliumIPv4(ip)
			ep := endpointmanager.LookupIPv4(c.String())
			if ep != nil {
				// Needs to be Lock as ep.GetLabelsSHA()
				// might overwrite internal endpoint attributes
				ep.Lock()
				info.ID = uint64(ep.ID)
				info.Labels = ep.GetLabels()
				info.LabelsSHA256 = ep.GetLabelsSHA()
				info.Identity = uint64(ep.GetIdentity())
				ep.Unlock()
			} else {
				fillIdentity(info, policy.ReservedIdentityCluster)
			}
		} else {
			// If we are unable to resolve the HostIP as well
			// as the cluster IP we mark this as a 'world' identity.
			fillIdentity(info, policy.ReservedIdentityWorld)
		}
	} else {
		info.IPv6 = ip.String()

		if node.IsHostIPv6(ip) {
			fillIdentity(info, policy.ReservedIdentityHost)
			return
		}

		if node.GetIPv6ClusterRange().Contains(ip) {
			c := addressing.DeriveCiliumIPv6(ip)
			id := c.EndpointID()
			info.ID = uint64(id)

			ep := endpointmanager.LookupCiliumID(id)
			if ep != nil {
				// Needs to be Lock as ep.GetLabelsSHA()
				// might overwrite internal endpoint attributes
				ep.Lock()
				info.Labels = ep.GetLabels()
				info.LabelsSHA256 = ep.GetLabelsSHA()
				info.Identity = uint64(ep.GetIdentity())
				ep.Unlock()
			} else {
				fillIdentity(info, policy.ReservedIdentityCluster)
			}
		} else {
			fillIdentity(info, policy.ReservedIdentityWorld)
		}
	}
}

// fillIngressSourceInfo fills the EndpointInfo fields, by fetching
// the consumable from the consumable cache of endpoint using identity sent by
// source. This is needed in ingress proxy while logging the source endpoint
// info.  Since there will be 2 proxies on the same host, if both egress and
// ingress policies are set, the ingress policy cannot determine the source
// endpoint info based on ip address, as the ip address would be that of the
// egress proxy i.e host.
func fillIngressSourceInfo(info *accesslog.EndpointInfo, ip *net.IP, srcIdentity uint32) {

	if srcIdentity != 0 {
		if ip != nil {
			if ip.To4() != nil {
				info.IPv4 = ip.String()
			} else {
				info.IPv6 = ip.String()
			}
		}
		fillIdentity(info, policy.NumericIdentity(srcIdentity))
	} else {
		// source security identity 0 is possible when somebody else other than
		// the BPF datapath attempts to
		// connect to the proxy.
		// We should try to resolve if the identity belongs to reserved_host
		// or reserved_world.
		if ip != nil {
			fillEndpointInfo(info, *ip)
		} else {
			log.Warn("Missing security identity in source endpoint info")
		}
	}
}

// fillEgressDestinationInfo returns the destination EndpointInfo for a flow
// leaving the proxy at egress.
func fillEgressDestinationInfo(info *accesslog.EndpointInfo, ipstr string) {
	ip := net.ParseIP(ipstr)
	if ip != nil {
		fillEndpointInfo(info, ip)
	}
}

// CreateOrUpdateRedirect creates or updates a L4 redirect with corresponding
// proxy configuration. This will allocate a proxy port as required and launch
// a proxy instance. If the redirect is already in place, only the rules will be
// updated.
func (p *Proxy) CreateOrUpdateRedirect(l4 *policy.L4Filter, id string, source ProxySource,
	notifier accesslog.LogRecordNotifier, completions policy.CompletionContainer) (Redirect, error) {
	gcOnce.Do(func() {
		if lf := viper.GetString("access-log"); lf != "" {
			if err := accesslog.OpenLogfile(lf, notifier); err != nil {
				log.WithError(err).WithField(accesslog.FieldFilePath, lf).
					Warn("Cannot open L7 access log")
			}
		}

		if labels := viper.GetStringSlice("agent-labels"); len(labels) != 0 {
			accesslog.SetMetadata(labels)
		}

		go func() {
			for {
				time.Sleep(time.Duration(10) * time.Second)
				if deleted := GC(); deleted > 0 {
					log.WithField("count", deleted).
						Debug("Evicted entries from proxy table")
				}
			}
		}()
	})

	p.mutex.Lock()
	defer p.mutex.Unlock()

	scopedLog := log.WithField(fieldProxyRedirectID, id)

	if r, ok := p.redirects[id]; ok {
		err := r.UpdateRules(l4, completions)
		if err != nil {
			scopedLog.WithError(err).Error("Unable to update ", l4.L7Parser, " proxy")
			return nil, err
		}
		scopedLog.WithField(logfields.Object, logfields.Repr(r)).
			Debug("updated existing ", l4.L7Parser, " proxy instance")
		return r, nil
	}

	to, err := p.allocatePort()
	if err != nil {
		return nil, err
	}

	var redir Redirect

	switch l4.L7Parser {
	case policy.ParserTypeKafka:
		redir, err = createKafkaRedirect(kafkaConfiguration{
			policy:     l4,
			id:         id,
			source:     source,
			listenPort: to})
	case policy.ParserTypeHTTP:
		redir, err = createEnvoyRedirect(l4, id, source, to, completions)
	default:
		return nil, fmt.Errorf("Unsupported L7 parser type: %s", l4.L7Parser)
	}
	if err != nil {
		scopedLog.WithError(err).Error("Unable to create ", l4.L7Parser, " proxy")
		return nil, err
	}
	scopedLog.WithField(logfields.Object, logfields.Repr(redir)).
		Debug("Created new ", l4.L7Parser, " proxy instance")

	p.allocatedPorts[to] = redir
	p.redirects[id] = redir

	return redir, nil
}

// RemoveRedirect removes an existing redirect.
func (p *Proxy) RemoveRedirect(id string, completions policy.CompletionContainer) error {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	r, ok := p.redirects[id]
	if !ok {
		return fmt.Errorf("unable to find redirect %s", id)
	}

	log.WithField(fieldProxyRedirectID, id).
		Debug("removing proxy redirect")
	toPort := r.ToPort()
	r.Close(completions)

	delete(p.redirects, id)

	// delay the release and reuse of the port number so it is guaranteed
	// to be safe to listen on the port again
	go func() {
		time.Sleep(portReleaseDelay)

		p.mutex.Lock()
		delete(p.allocatedPorts, toPort)
		p.mutex.Unlock()

		log.WithField(fieldProxyRedirectID, id).
			Debugf("Delayed release of proxy port %d", toPort)
	}()

	return nil
}

// ChangeLogLevel changes proxy log level to correspond to the logrus log level 'level'.
func ChangeLogLevel(level logrus.Level) {
	if envoyProxy != nil {
		envoyProxy.ChangeLogLevel(level)
	}
}
<commit_msg>proxy: Retry redirect creation a couple of times<commit_after>// Copyright 2016-2017 Authors of Cilium
//
// 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 proxy

import (
	"fmt"
	"math/rand"
	"net"
	"strconv"
	"sync"
	"time"

	"github.com/cilium/cilium/common/addressing"
	"github.com/cilium/cilium/pkg/endpointmanager"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/node"
	"github.com/cilium/cilium/pkg/policy"
	"github.com/cilium/cilium/pkg/proxy/accesslog"

	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

var (
	log          = logging.DefaultLogger
	perFlowDebug = false
)

// Magic markers are attached to each packet. The lower 16 bits are used to
// identify packets which have gone through the proxy and to determine whether
// the packet is coming from a proxy at ingress or egress. The marking is
// compatible with Kubernetes's use of the packet mark.  The upper 16 bits can
// be used to carry the security identity.
const (
	magicMarkIngress int = 0x0FEA
	magicMarkEgress  int = 0x0FEB
	magicMarkK8sMasq int = 0x4000
	magicMarkK8sDrop int = 0x8000
)

// field names used while logging
const (
	fieldMarker          = "marker"
	fieldSocket          = "socket"
	fieldFd              = "fd"
	fieldProxyRedirectID = "id"

	// portReleaseDelay is the delay until a port is being released
	portReleaseDelay = time.Duration(5) * time.Minute

	// redirectCreationAttempts is the number of attempts to create a redirect
	redirectCreationAttempts = 5
)

// Redirect is the generic proxy redirect interface that each proxy redirect
// type must export
type Redirect interface {
	ToPort() uint16
	UpdateRules(l4 *policy.L4Filter, completions policy.CompletionContainer) error
	getSource() ProxySource
	Close(completions policy.CompletionContainer)
	IsIngress() bool
}

// GetMagicMark returns the magic marker with which each packet must be marked.
// The mark is different depending on whether the proxy is injected at ingress
// or egress.
func GetMagicMark(isIngress bool, identity int) int {
	mark := 0

	if isIngress {
		mark = magicMarkIngress
	} else {
		mark = magicMarkEgress
	}

	if identity != 0 {
		mark |= identity << 16
	}

	return mark
}

// ProxySource returns information about the endpoint being proxied.
type ProxySource interface {
	GetID() uint64
	RLock()
	RUnlock()
	Lock()
	Unlock()
	GetLabels() []string
	GetLabelsSHA() string
	GetIdentity() policy.NumericIdentity
	ResolveIdentity(policy.NumericIdentity) *policy.Identity
	GetIPv4Address() string
	GetIPv6Address() string
}

// Proxy maintains state about redirects
type Proxy struct {
	// mutex is the lock required when modifying any proxy datastructure
	mutex lock.RWMutex

	// rangeMin is the minimum port used for proxy port allocation
	rangeMin uint16

	// rangeMax is the maximum port used for proxy port allocation.
	// If port is unspecified, the proxy will automatically allocate
	// ports out of the rangeMin-rangeMax range.
	rangeMax uint16

	// allocatedPorts is a map of all allocated proxy ports pointing
	// to the redirect rules attached to that port
	allocatedPorts map[uint16]Redirect

	// redirects is a map of all redirect configurations indexed by
	// the redirect identifier. Redirects may be implemented by different
	// proxies.
	redirects map[string]Redirect
}

// NewProxy creates a Proxy to keep track of redirects.
func NewProxy(minPort uint16, maxPort uint16) *Proxy {
	return &Proxy{
		rangeMin:       minPort,
		rangeMax:       maxPort,
		redirects:      make(map[string]Redirect),
		allocatedPorts: make(map[uint16]Redirect),
	}
}

var (
	portRandomizer = rand.New(rand.NewSource(time.Now().UnixNano()))
)

func (p *Proxy) allocatePort() (uint16, error) {
	for _, r := range portRandomizer.Perm(int(p.rangeMax - p.rangeMin + 1)) {
		resPort := uint16(r) + p.rangeMin

		if _, ok := p.allocatedPorts[resPort]; !ok {
			return resPort, nil
		}

	}

	return 0, fmt.Errorf("no available proxy ports")
}

var gcOnce sync.Once

// localEndpointInfo fills the access log with the local endpoint info.
func localEndpointInfo(r Redirect, info *accesslog.EndpointInfo) {
	source := r.getSource()
	source.Lock()
	info.ID = source.GetID()
	info.IPv4 = source.GetIPv4Address()
	info.IPv6 = source.GetIPv6Address()
	info.Labels = source.GetLabels()
	info.LabelsSHA256 = source.GetLabelsSHA()
	info.Identity = uint64(source.GetIdentity())
	source.Unlock()
}

func fillInfo(r Redirect, l *accesslog.LogRecord, srcIPPort, dstIPPort string, srcIdentity uint32) {

	ingress := r.IsIngress()

	if ingress {
		// At ingress the local origin endpoint is the destination
		localEndpointInfo(r, &l.DestinationEndpoint)
	} else {
		// At egress, the local origin endpoint is the source
		localEndpointInfo(r, &l.SourceEndpoint)
	}

	l.IPVersion = accesslog.VersionIPv4
	ipstr, port, err := net.SplitHostPort(srcIPPort)
	if err == nil {
		ip := net.ParseIP(ipstr)
		if ip != nil && ip.To4() == nil {
			l.IPVersion = accesslog.VersionIPV6
		}

		p, err := strconv.ParseUint(port, 10, 16)
		if err == nil {
			l.SourceEndpoint.Port = uint16(p)
			if ingress {
				fillIngressSourceInfo(&l.SourceEndpoint, &ip, srcIdentity)
			}
		}
	}

	ipstr, port, err = net.SplitHostPort(dstIPPort)
	if err == nil {
		p, err := strconv.ParseUint(port, 10, 16)
		if err == nil {
			l.DestinationEndpoint.Port = uint16(p)
			if !ingress {
				fillEgressDestinationInfo(&l.DestinationEndpoint, ipstr)
			}
		}
	}
}

// fillIdentity resolves the labels of the specified identity if known
// locally and fills in the following info member fields:
//  - info.Identity
//  - info.Labels
//  - info.LabelsSHA256
func fillIdentity(info *accesslog.EndpointInfo, id policy.NumericIdentity) {
	info.Identity = uint64(id)

	if identity := policy.LookupIdentityByID(id); identity != nil {
		info.Labels = identity.Labels.GetModel()
		info.LabelsSHA256 = identity.GetLabelsSHA256()
	}
}

// fillEndpointInfo tries to resolve the IP address and fills the EndpointInfo
// fields with either ReservedIdentityHost or ReservedIdentityWorld
func fillEndpointInfo(info *accesslog.EndpointInfo, ip net.IP) {
	if ip.To4() != nil {
		info.IPv4 = ip.String()

		// first we try to resolve and check if the IP is
		// same as Host
		if node.IsHostIPv4(ip) {
			fillIdentity(info, policy.ReservedIdentityHost)
			return
		}

		// If Host IP check fails, we try to resolve and check
		// if IP belongs to the cluster.
		if node.GetIPv4ClusterRange().Contains(ip) {
			c := addressing.DeriveCiliumIPv4(ip)
			ep := endpointmanager.LookupIPv4(c.String())
			if ep != nil {
				// Needs to be Lock as ep.GetLabelsSHA()
				// might overwrite internal endpoint attributes
				ep.Lock()
				info.ID = uint64(ep.ID)
				info.Labels = ep.GetLabels()
				info.LabelsSHA256 = ep.GetLabelsSHA()
				info.Identity = uint64(ep.GetIdentity())
				ep.Unlock()
			} else {
				fillIdentity(info, policy.ReservedIdentityCluster)
			}
		} else {
			// If we are unable to resolve the HostIP as well
			// as the cluster IP we mark this as a 'world' identity.
			fillIdentity(info, policy.ReservedIdentityWorld)
		}
	} else {
		info.IPv6 = ip.String()

		if node.IsHostIPv6(ip) {
			fillIdentity(info, policy.ReservedIdentityHost)
			return
		}

		if node.GetIPv6ClusterRange().Contains(ip) {
			c := addressing.DeriveCiliumIPv6(ip)
			id := c.EndpointID()
			info.ID = uint64(id)

			ep := endpointmanager.LookupCiliumID(id)
			if ep != nil {
				// Needs to be Lock as ep.GetLabelsSHA()
				// might overwrite internal endpoint attributes
				ep.Lock()
				info.Labels = ep.GetLabels()
				info.LabelsSHA256 = ep.GetLabelsSHA()
				info.Identity = uint64(ep.GetIdentity())
				ep.Unlock()
			} else {
				fillIdentity(info, policy.ReservedIdentityCluster)
			}
		} else {
			fillIdentity(info, policy.ReservedIdentityWorld)
		}
	}
}

// fillIngressSourceInfo fills the EndpointInfo fields, by fetching
// the consumable from the consumable cache of endpoint using identity sent by
// source. This is needed in ingress proxy while logging the source endpoint
// info.  Since there will be 2 proxies on the same host, if both egress and
// ingress policies are set, the ingress policy cannot determine the source
// endpoint info based on ip address, as the ip address would be that of the
// egress proxy i.e host.
func fillIngressSourceInfo(info *accesslog.EndpointInfo, ip *net.IP, srcIdentity uint32) {

	if srcIdentity != 0 {
		if ip != nil {
			if ip.To4() != nil {
				info.IPv4 = ip.String()
			} else {
				info.IPv6 = ip.String()
			}
		}
		fillIdentity(info, policy.NumericIdentity(srcIdentity))
	} else {
		// source security identity 0 is possible when somebody else other than
		// the BPF datapath attempts to
		// connect to the proxy.
		// We should try to resolve if the identity belongs to reserved_host
		// or reserved_world.
		if ip != nil {
			fillEndpointInfo(info, *ip)
		} else {
			log.Warn("Missing security identity in source endpoint info")
		}
	}
}

// fillEgressDestinationInfo returns the destination EndpointInfo for a flow
// leaving the proxy at egress.
func fillEgressDestinationInfo(info *accesslog.EndpointInfo, ipstr string) {
	ip := net.ParseIP(ipstr)
	if ip != nil {
		fillEndpointInfo(info, ip)
	}
}

// CreateOrUpdateRedirect creates or updates a L4 redirect with corresponding
// proxy configuration. This will allocate a proxy port as required and launch
// a proxy instance. If the redirect is already in place, only the rules will be
// updated.
func (p *Proxy) CreateOrUpdateRedirect(l4 *policy.L4Filter, id string, source ProxySource,
	notifier accesslog.LogRecordNotifier, completions policy.CompletionContainer) (Redirect, error) {
	gcOnce.Do(func() {
		if lf := viper.GetString("access-log"); lf != "" {
			if err := accesslog.OpenLogfile(lf, notifier); err != nil {
				log.WithError(err).WithField(accesslog.FieldFilePath, lf).
					Warn("Cannot open L7 access log")
			}
		}

		if labels := viper.GetStringSlice("agent-labels"); len(labels) != 0 {
			accesslog.SetMetadata(labels)
		}

		go func() {
			for {
				time.Sleep(time.Duration(10) * time.Second)
				if deleted := GC(); deleted > 0 {
					log.WithField("count", deleted).
						Debug("Evicted entries from proxy table")
				}
			}
		}()
	})

	p.mutex.Lock()
	defer p.mutex.Unlock()

	scopedLog := log.WithField(fieldProxyRedirectID, id)

	if r, ok := p.redirects[id]; ok {
		err := r.UpdateRules(l4, completions)
		if err != nil {
			scopedLog.WithError(err).Error("Unable to update ", l4.L7Parser, " proxy")
			return nil, err
		}
		scopedLog.WithField(logfields.Object, logfields.Repr(r)).
			Debug("updated existing ", l4.L7Parser, " proxy instance")
		return r, nil
	}

	nRetry := 0

retry:
	to, err := p.allocatePort()
	if err != nil {
		return nil, err
	}

	var redir Redirect

	switch l4.L7Parser {
	case policy.ParserTypeKafka:
		redir, err = createKafkaRedirect(kafkaConfiguration{
			policy:     l4,
			id:         id,
			source:     source,
			listenPort: to})

	case policy.ParserTypeHTTP:
		redir, err = createEnvoyRedirect(l4, id, source, to, completions)

	default:
		return nil, fmt.Errorf("Unsupported L7 parser type: %s", l4.L7Parser)
	}

	if err != nil {
		if nRetry >= redirectCreationAttempts {
			scopedLog.WithError(err).Error("Unable to create ", l4.L7Parser, " proxy")
			return nil, err
		}

		scopedLog.WithError(err).Warning("Unable to create ", l4.L7Parser, " proxy, will retry")
		nRetry++
		goto retry
	}

	scopedLog.WithField(logfields.Object, logfields.Repr(redir)).
		Debug("Created new ", l4.L7Parser, " proxy instance")

	p.allocatedPorts[to] = redir
	p.redirects[id] = redir

	return redir, nil
}

// RemoveRedirect removes an existing redirect.
func (p *Proxy) RemoveRedirect(id string, completions policy.CompletionContainer) error {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	r, ok := p.redirects[id]
	if !ok {
		return fmt.Errorf("unable to find redirect %s", id)
	}

	log.WithField(fieldProxyRedirectID, id).
		Debug("removing proxy redirect")
	toPort := r.ToPort()
	r.Close(completions)

	delete(p.redirects, id)

	// delay the release and reuse of the port number so it is guaranteed
	// to be safe to listen on the port again
	go func() {
		time.Sleep(portReleaseDelay)

		p.mutex.Lock()
		delete(p.allocatedPorts, toPort)
		p.mutex.Unlock()

		log.WithField(fieldProxyRedirectID, id).
			Debugf("Delayed release of proxy port %d", toPort)
	}()

	return nil
}

// ChangeLogLevel changes proxy log level to correspond to the logrus log level 'level'.
func ChangeLogLevel(level logrus.Level) {
	if envoyProxy != nil {
		envoyProxy.ChangeLogLevel(level)
	}
}
<|endoftext|>


<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 scale

import (
	"fmt"
	"strconv"
	"time"

	autoscalingv1 "k8s.io/api/autoscaling/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
	scaleclient "k8s.io/client-go/scale"
)

// Scaler provides an interface for resources that can be scaled.
type Scaler interface {
	// Scale scales the named resource after checking preconditions. It optionally
	// retries in the event of resource version mismatch (if retry is not nil),
	// and optionally waits until the status of the resource matches newSize (if wait is not nil)
	// TODO: Make the implementation of this watch-based (#56075) once #31345 is fixed.
	Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource) error
	// ScaleSimple does a simple one-shot attempt at scaling - not useful on its own, but
	// a necessary building block for Scale
	ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource) (updatedResourceVersion string, err error)
}

// NewScaler get a scaler for a given resource
func NewScaler(scalesGetter scaleclient.ScalesGetter) Scaler {
	return &genericScaler{scalesGetter}
}

// ScalePrecondition describes a condition that must be true for the scale to take place
// If CurrentSize == -1, it is ignored.
// If CurrentResourceVersion is the empty string, it is ignored.
// Otherwise they must equal the values in the resource for it to be valid.
type ScalePrecondition struct {
	Size            int
	ResourceVersion string
}

// A PreconditionError is returned when a resource fails to match
// the scale preconditions passed to kubectl.
type PreconditionError struct {
	Precondition  string
	ExpectedValue string
	ActualValue   string
}

func (pe PreconditionError) Error() string {
	return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue)
}

// RetryParams encapsulates the retry parameters used by kubectl's scaler.
type RetryParams struct {
	Interval, Timeout time.Duration
}

func NewRetryParams(interval, timeout time.Duration) *RetryParams {
	return &RetryParams{interval, timeout}
}

// ScaleCondition is a closure around Scale that facilitates retries via util.wait
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource) wait.ConditionFunc {
	return func() (bool, error) {
		rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr)
		if updatedResourceVersion != nil {
			*updatedResourceVersion = rv
		}
		// Retry only on update conflicts.
		if errors.IsConflict(err) {
			return false, nil
		}
		if err != nil {
			return false, err
		}
		return true, nil
	}
}

// validateGeneric ensures that the preconditions match. Returns nil if they are valid, otherwise an error
func (precondition *ScalePrecondition) validate(scale *autoscalingv1.Scale) error {
	if precondition.Size != -1 && int(scale.Spec.Replicas) != precondition.Size {
		return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(int(scale.Spec.Replicas))}
	}
	if len(precondition.ResourceVersion) > 0 && scale.ResourceVersion != precondition.ResourceVersion {
		return PreconditionError{"resource version", precondition.ResourceVersion, scale.ResourceVersion}
	}
	return nil
}

// genericScaler can update scales for resources in a particular namespace
type genericScaler struct {
	scaleNamespacer scaleclient.ScalesGetter
}

var _ Scaler = &genericScaler{}

// ScaleSimple updates a scale of a given resource. It returns the resourceVersion of the scale if the update was successful.
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource) (updatedResourceVersion string, err error) {
	if preconditions != nil {
		scale, err := s.scaleNamespacer.Scales(namespace).Get(gvr.GroupResource(), name)
		if err != nil {
			return "", err
		}
		if err = preconditions.validate(scale); err != nil {
			return "", err
		}
		scale.Spec.Replicas = int32(newSize)
		updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(gvr.GroupResource(), scale)
		if err != nil {
			return "", err
		}
		return updatedScale.ResourceVersion, nil
	}

	patch := []byte(fmt.Sprintf(`{"spec":{"replicas":%d}}`, newSize))
	updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(gvr, name, types.MergePatchType, patch)
	if err != nil {
		return "", err
	}
	return updatedScale.ResourceVersion, nil
}

// Scale updates a scale of a given resource to a new size, with optional precondition check (if preconditions is not nil),
// optional retries (if retry is not nil), and then optionally waits for the status to reach desired count.
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource) error {
	if retry == nil {
		// make it try only once, immediately
		retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond}
	}
	cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr)
	if err := wait.PollImmediate(retry.Interval, retry.Timeout, cond); err != nil {
		return err
	}
	if waitForReplicas != nil {
		return WaitForScaleHasDesiredReplicas(s.scaleNamespacer, gvr.GroupResource(), resourceName, namespace, newSize, waitForReplicas)
	}
	return nil
}

// scaleHasDesiredReplicas returns a condition that will be true if and only if the desired replica
// count for a scale (Spec) equals its updated replicas count (Status)
func scaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, desiredReplicas int32) wait.ConditionFunc {
	return func() (bool, error) {
		actualScale, err := sClient.Scales(namespace).Get(gr, resourceName)
		if err != nil {
			return false, err
		}
		// this means the desired scale target has been reset by something else
		if actualScale.Spec.Replicas != desiredReplicas {
			return true, nil
		}
		return actualScale.Spec.Replicas == actualScale.Status.Replicas &&
			desiredReplicas == actualScale.Status.Replicas, nil
	}
}

// WaitForScaleHasDesiredReplicas waits until condition scaleHasDesiredReplicas is satisfied
// or returns error when timeout happens
func WaitForScaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, newSize uint, waitForReplicas *RetryParams) error {
	if waitForReplicas == nil {
		return fmt.Errorf("waitForReplicas parameter cannot be nil")
	}
	err := wait.PollImmediate(
		waitForReplicas.Interval,
		waitForReplicas.Timeout,
		scaleHasDesiredReplicas(sClient, gr, resourceName, namespace, int32(newSize)))
	if err == wait.ErrWaitTimeout {
		return fmt.Errorf("timed out waiting for %q to be synced", resourceName)
	}
	return err
}
<commit_msg>Add context and options to scale client<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 scale

import (
	"context"
	"fmt"
	"strconv"
	"time"

	autoscalingv1 "k8s.io/api/autoscaling/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
	scaleclient "k8s.io/client-go/scale"
)

// Scaler provides an interface for resources that can be scaled.
type Scaler interface {
	// Scale scales the named resource after checking preconditions. It optionally
	// retries in the event of resource version mismatch (if retry is not nil),
	// and optionally waits until the status of the resource matches newSize (if wait is not nil)
	// TODO: Make the implementation of this watch-based (#56075) once #31345 is fixed.
	Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource) error
	// ScaleSimple does a simple one-shot attempt at scaling - not useful on its own, but
	// a necessary building block for Scale
	ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource) (updatedResourceVersion string, err error)
}

// NewScaler get a scaler for a given resource
func NewScaler(scalesGetter scaleclient.ScalesGetter) Scaler {
	return &genericScaler{scalesGetter}
}

// ScalePrecondition describes a condition that must be true for the scale to take place
// If CurrentSize == -1, it is ignored.
// If CurrentResourceVersion is the empty string, it is ignored.
// Otherwise they must equal the values in the resource for it to be valid.
type ScalePrecondition struct {
	Size            int
	ResourceVersion string
}

// A PreconditionError is returned when a resource fails to match
// the scale preconditions passed to kubectl.
type PreconditionError struct {
	Precondition  string
	ExpectedValue string
	ActualValue   string
}

func (pe PreconditionError) Error() string {
	return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue)
}

// RetryParams encapsulates the retry parameters used by kubectl's scaler.
type RetryParams struct {
	Interval, Timeout time.Duration
}

func NewRetryParams(interval, timeout time.Duration) *RetryParams {
	return &RetryParams{interval, timeout}
}

// ScaleCondition is a closure around Scale that facilitates retries via util.wait
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource) wait.ConditionFunc {
	return func() (bool, error) {
		rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr)
		if updatedResourceVersion != nil {
			*updatedResourceVersion = rv
		}
		// Retry only on update conflicts.
		if errors.IsConflict(err) {
			return false, nil
		}
		if err != nil {
			return false, err
		}
		return true, nil
	}
}

// validateGeneric ensures that the preconditions match. Returns nil if they are valid, otherwise an error
func (precondition *ScalePrecondition) validate(scale *autoscalingv1.Scale) error {
	if precondition.Size != -1 && int(scale.Spec.Replicas) != precondition.Size {
		return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(int(scale.Spec.Replicas))}
	}
	if len(precondition.ResourceVersion) > 0 && scale.ResourceVersion != precondition.ResourceVersion {
		return PreconditionError{"resource version", precondition.ResourceVersion, scale.ResourceVersion}
	}
	return nil
}

// genericScaler can update scales for resources in a particular namespace
type genericScaler struct {
	scaleNamespacer scaleclient.ScalesGetter
}

var _ Scaler = &genericScaler{}

// ScaleSimple updates a scale of a given resource. It returns the resourceVersion of the scale if the update was successful.
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource) (updatedResourceVersion string, err error) {
	if preconditions != nil {
		scale, err := s.scaleNamespacer.Scales(namespace).Get(context.TODO(), gvr.GroupResource(), name, metav1.GetOptions{})
		if err != nil {
			return "", err
		}
		if err = preconditions.validate(scale); err != nil {
			return "", err
		}
		scale.Spec.Replicas = int32(newSize)
		updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, metav1.UpdateOptions{})
		if err != nil {
			return "", err
		}
		return updatedScale.ResourceVersion, nil
	}

	patch := []byte(fmt.Sprintf(`{"spec":{"replicas":%d}}`, newSize))
	updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, metav1.PatchOptions{})
	if err != nil {
		return "", err
	}
	return updatedScale.ResourceVersion, nil
}

// Scale updates a scale of a given resource to a new size, with optional precondition check (if preconditions is not nil),
// optional retries (if retry is not nil), and then optionally waits for the status to reach desired count.
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource) error {
	if retry == nil {
		// make it try only once, immediately
		retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond}
	}
	cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr)
	if err := wait.PollImmediate(retry.Interval, retry.Timeout, cond); err != nil {
		return err
	}
	if waitForReplicas != nil {
		return WaitForScaleHasDesiredReplicas(s.scaleNamespacer, gvr.GroupResource(), resourceName, namespace, newSize, waitForReplicas)
	}
	return nil
}

// scaleHasDesiredReplicas returns a condition that will be true if and only if the desired replica
// count for a scale (Spec) equals its updated replicas count (Status)
func scaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, desiredReplicas int32) wait.ConditionFunc {
	return func() (bool, error) {
		actualScale, err := sClient.Scales(namespace).Get(context.TODO(), gr, resourceName, metav1.GetOptions{})
		if err != nil {
			return false, err
		}
		// this means the desired scale target has been reset by something else
		if actualScale.Spec.Replicas != desiredReplicas {
			return true, nil
		}
		return actualScale.Spec.Replicas == actualScale.Status.Replicas &&
			desiredReplicas == actualScale.Status.Replicas, nil
	}
}

// WaitForScaleHasDesiredReplicas waits until condition scaleHasDesiredReplicas is satisfied
// or returns error when timeout happens
func WaitForScaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, newSize uint, waitForReplicas *RetryParams) error {
	if waitForReplicas == nil {
		return fmt.Errorf("waitForReplicas parameter cannot be nil")
	}
	err := wait.PollImmediate(
		waitForReplicas.Interval,
		waitForReplicas.Timeout,
		scaleHasDesiredReplicas(sClient, gr, resourceName, namespace, int32(newSize)))
	if err == wait.ErrWaitTimeout {
		return fmt.Errorf("timed out waiting for %q to be synced", resourceName)
	}
	return err
}
<|endoftext|>