<commit_before>// Copyright 2018 Google Inc.
//
// 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
//
// https://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 run provides a sub tool for running pipelines.
package run

// This tool takes as an input a script file composed of a series of command
// lines to execute.
//
// By default, each command line is executed using the 'bash' container and
// must succeed before the next command is executed.  This behaviour can be
// modified using a '&' at the end of the line which will cause the command to
// run in the background.  Additionally, flags and options can be specified
// after a '#' character to control what image is used or to apply other action
// flags.
//
// Files from GCS can be specified as inputs to the script using the --inputs
// flag.  These files will be copied onto the VM.  The names of the localized
// files are exposed via the environment variables $INPUT0 to $INPUTN.  Note
// that the input files are placed on a read-only disk.
//
// Similarly, destinations in GCS may be specified with the --outputs flag.
// Each output file will be exposed by via the environment variables $OUTPUT0
// to $OUTPUTN.
//
// As a convenience, the tool will automatically use the google/cloud-sdk image
// whenever the command line starts with gsutil or gcloud, and will
// automatically include the cloud-platform API scope whenever the cloud-sdk
// container is used.
//
// Any 'export' commands are interpreted prior to execution of the pipeline.
// The specified value is injected into the environment for all subsequent
// commands.
//
// Variable references of the form ${VARIABLE} or $VARIABLE are replaced using
// values from the host environment (unless the value has been overwritten by a
// previous export command).
//
// If any command references '/tmp' the attached persistent disk is mounted
// there, read-write.
//
// If the --output flag is specified, an action is appended that copies the
// combined pipeline output to the specified GCS path.
//
// The --dry-run flag can be used to see what pipeline would be produced
// without executing it.
//
// Example 1: Simple 'hello world' script
//
//    echo "Hello World!"
//
// Example 2: Background action
//
//    while true; do echo "Hello background world!"; sleep 1; done &
//    sleep 5
//    echo "Hello foreground world!"
//
// Example 3: Indexing a BAM
//
//    export BAM=NA12892_S1.bam
//    export BAI=${BAM}.bai
//    gsutil cp gs://my-bucket/${BAM} /tmp
//    index /tmp/${BAM} /tmp/${BAI} # image=gcr.io/genomics-tools/samtools
//    gsutil cp /tmp/${BAI} gs://my-bucket/
//
// Example 4: SHA1 sum a file (using the --inputs and --outputs flags).
//
//    sha1sum ${INPUT0} > ${OUTPUT0}
//
import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	genomics "google.golang.org/api/genomics/v2alpha1"
	"google.golang.org/api/googleapi"
)

var (
	flags = flag.NewFlagSet("", flag.ExitOnError)

	basePath       = flags.String("base-path", "", "optional API service base path")
	name           = flags.String("name", "", "optional name applied as a label")
	script         = flags.String("script", "", "the script to run")
	scopes         = flags.String("scopes", "", "comma separated list of additional API scopes")
	zone           = flags.String("zone", "us-east1-d", "zone to run in")
	output         = flags.String("output", "", "GCS path to write output to")
	dryRun         = flags.Bool("dry-run", false, "don't run, just show pipeline")
	wait           = flags.Bool("wait", true, "wait for the pipeline to finish")
	machineType    = flags.String("machine-type", "n1-standard-1", "machine type to create")
	preemptible    = flags.Bool("preemptible", true, "use a preemptible VM")
	inputs         = flags.String("inputs", "", "comma separated list of GCS objects to localize to the VM")
	outputs        = flags.String("outputs", "", "comma separated list of GCS objects to delocalize from the VM")
	diskSizeGb     = flags.Int("disk-size", 500, "the attached disk size (in GB)")
	privateAddress = flags.Bool("private-address", false, "use a private IP address")
)

const (
	inputRoot  = "/mnt/input"
	outputRoot = "/mnt/output"
	diskName   = "shared"
)

func Invoke(ctx context.Context, service *genomics.Service, project string, arguments []string) error {
	flags.Parse(arguments)

	if *script == "" {
		return errors.New("the script flag is required")
	}

	labels := map[string]string{"script-filename": *script}
	if *name != "" {
		labels["name"] = *name
	}

	environment := make(map[string]string)

	var localizers, delocalizers []*genomics.Action
	for i, input := range listOf(*inputs) {
		filename := filepath.Join(inputRoot, fmt.Sprintf("input%d", i))
		localizers = append(localizers, &genomics.Action{
			ImageUri: "google/cloud-sdk",
			Commands: []string{"gsutil", "-q", "cp", input, filename},
			Mounts:   []*genomics.Mount{{Disk: diskName, Path: inputRoot}},
		})
		environment[fmt.Sprintf("INPUT%d", i)] = filename
	}
	for i, output := range listOf(*outputs) {
		filename := filepath.Join(outputRoot, fmt.Sprintf("output%d", i))
		delocalizers = append(delocalizers, &genomics.Action{
			ImageUri: "google/cloud-sdk",
			Commands: []string{"gsutil", "-q", "cp", filename, output},
			Flags:    []string{"ALWAYS_RUN"},
			Mounts:   []*genomics.Mount{{Disk: diskName, Path: outputRoot, ReadOnly: true}},
		})
		environment[fmt.Sprintf("OUTPUT%d", i)] = filename
	}
	if *output != "" {
		delocalizers = append(delocalizers, &genomics.Action{
			ImageUri: "google/cloud-sdk",
			Commands: []string{"gsutil", "-q", "cp", "/google/logs/output", *output},
			Flags:    []string{"ALWAYS_RUN"},
		})
	}

	actions, err := parseScript(*script, environment)
	if err != nil {
		return fmt.Errorf("creating pipeline from script: %v", err)
	}

	pipeline := &genomics.Pipeline{
		Resources: &genomics.Resources{
			ProjectId: project,
			Zones:     []string{*zone},
			VirtualMachine: &genomics.VirtualMachine{
				MachineType: *machineType,
				Preemptible: *preemptible,
				Disks:       []*genomics.Disk{{Name: diskName, SizeGb: int64(*diskSizeGb)}},
				Network: &genomics.Network{
					UsePrivateAddress: *privateAddress,
				},
				ServiceAccount: &genomics.ServiceAccount{Scopes: listOf(*scopes)},
			},
		},
		Actions:     append(localizers, append(actions, delocalizers...)...),
		Environment: environment,
	}

	addRequiredScopes(pipeline)

	encoded, err := json.MarshalIndent(pipeline, "", "  ")
	if err != nil {
		return fmt.Errorf("encoding pipeline: %v", err)
	}
	fmt.Printf("%s\n", encoded)

	if *dryRun {
		return nil
	}

	req := &genomics.RunPipelineRequest{Pipeline: pipeline, Labels: labels}
	lro, err := service.Pipelines.Run(req).Context(ctx).Do()
	if err != nil {
		if err, ok := err.(*googleapi.Error); ok {
			return fmt.Errorf("starting pipeline: %q: %q", err.Message, err.Body)
		}
		return fmt.Errorf("starting pipeline: %v", err)
	}

	fmt.Printf("Pipeline running as %q\n", lro.Name)
	if *output != "" {
		fmt.Printf("Output will be written to %q\n", *output)
	}

	if !*wait {
		return nil
	}

	result, err := monitor(ctx, service, lro.Name)
	if err != nil {
		return fmt.Errorf("monitoring pipeline: %v", err)
	}

	if status, ok := result.(*genomics.Status); ok {
		return fmt.Errorf("executing pipeline: %s", status.Message)
	}

	fmt.Println("Pipeline execution completed")
	return nil
}

func monitor(ctx context.Context, service *genomics.Service, name string) (interface{}, error) {
	var events []*genomics.Event
	for {
		time.Sleep(5 * time.Second)
		lro, err := service.Projects.Operations.Get(name).Context(ctx).Do()
		if err != nil {
			return nil, fmt.Errorf("getting operation status: %v", err)
		}

		var metadata genomics.Metadata
		if err := json.Unmarshal(lro.Metadata, &metadata); err != nil {
			return nil, fmt.Errorf("parsing metadata: %v", err)
		}

		if len(events) != len(metadata.Events) {
			for i := len(metadata.Events) - len(events) - 1; i >= 0; i-- {
				timestamp, _ := time.Parse(time.RFC3339Nano, metadata.Events[i].Timestamp)
				fmt.Println(timestamp.Format("15:04:05"), metadata.Events[i].Description)
			}
			events = metadata.Events
		}

		if lro.Done {
			if lro.Error != nil {
				return lro.Error, nil
			}
			return lro.Response, nil
		}
	}
}

func parseScript(filename string, globalEnv map[string]string) ([]*genomics.Action, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("opening script: %v", err)
	}
	defer f.Close()

	var line int
	var actions []*genomics.Action
	localEnv := make(map[string]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line++
		action, err := parse(scanner.Text(), localEnv, globalEnv)
		if err != nil {
			return nil, fmt.Errorf("line %d: %v", line, err)
		}
		if action != nil {
			actions = append(actions, action)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("reading script: %v", err)
	}
	return actions, nil
}

func parse(line string, localEnv, globalEnv map[string]string) (*genomics.Action, error) {
	var (
		commands []string
		flags    []string
		options  = make(map[string]string)
	)

	if n := strings.Index(line, "#"); n >= 0 {
		for _, option := range strings.Fields(strings.TrimSpace(line[n+1:])) {
			if n := strings.Index(option, "="); n >= 0 {
				options[option[:n]] = option[n+1:]
			} else {
				flags = append(flags, strings.ToUpper(option))
			}
		}
		line = line[:n]
	}

	var missing string
	line = os.Expand(line, func(name string) string {
		if value, ok := localEnv[name]; ok {
			return value
		}
		if value, ok := globalEnv[name]; ok {
			return value
		}
		if value, ok := os.LookupEnv(name); ok {
			return value
		}
		missing = name
		return ""
	})

	if missing != "" {
		return nil, fmt.Errorf("missing value for variable %q", missing)
	}

	commands = strings.Fields(strings.TrimSpace(line))
	if len(commands) == 0 {
		return nil, nil
	}

	if commands[len(commands)-1] == "&" {
		flags = append(flags, "RUN_IN_BACKGROUND")
		commands = commands[:len(commands)-1]
	}

	if commands[0] == "export" {
		fields := strings.SplitN(strings.Join(commands[1:], " "), "=", 2)
		if len(fields) != 2 {
			return nil, fmt.Errorf("missing assignment in export command: %q", line)
		}

		localEnv[fields[0]] = fields[1]
		return nil, nil
	}

	image := detectImage(commands, options)
	mounts := detectMounts(commands)
	if image == "bash" {
		commands = []string{"-c", strings.Join(commands, " ")}
	}

	return &genomics.Action{
		ImageUri:    image,
		Commands:    commands,
		Flags:       flags,
		Environment: localEnv,
		Mounts:      mounts,
	}, nil
}

func detectImage(command []string, options map[string]string) string {
	if image, ok := options["image"]; ok {
		return image
	}
	if isCloudCommand(command[0]) {
		return "google/cloud-sdk"
	}
	return "bash"
}

func detectMounts(commands []string) []*genomics.Mount {
	var mounts []*genomics.Mount
	if findReference("/tmp", commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: "/tmp"})
	}
	if findReference(outputRoot, commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: outputRoot})
	}
	if findReference(inputRoot, commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: inputRoot, ReadOnly: true})
	}
	return mounts
}

func findReference(root string, commands []string) bool {
	for _, command := range commands {
		if strings.HasPrefix(command, root) {
			return true
		}
	}
	return false
}

func addRequiredScopes(pipeline *genomics.Pipeline) {
	scopes := &pipeline.Resources.VirtualMachine.ServiceAccount.Scopes
	for _, action := range pipeline.Actions {
		if action.ImageUri == "google/cloud-sdk" || isCloudCommand(action.Commands[0]) {
			*scopes = append(*scopes, "https://www.googleapis.com/auth/devstorage.read_write")
			return
		}
	}
}

func isCloudCommand(command string) bool {
	return command == "gsutil" || command == "gcloud"
}

func listOf(input string) []string {
	if input == "" {
		return nil
	}
	return strings.Split(input, ",")
}
<commit_msg>Use the alpine variant of cloud-sdk<commit_after>// Copyright 2018 Google Inc.
//
// 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
//
// https://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 run provides a sub tool for running pipelines.
package run

// This tool takes as an input a script file composed of a series of command
// lines to execute.
//
// By default, each command line is executed using the 'bash' container and
// must succeed before the next command is executed.  This behaviour can be
// modified using a '&' at the end of the line which will cause the command to
// run in the background.  Additionally, flags and options can be specified
// after a '#' character to control what image is used or to apply other action
// flags.
//
// Files from GCS can be specified as inputs to the script using the --inputs
// flag.  These files will be copied onto the VM.  The names of the localized
// files are exposed via the environment variables $INPUT0 to $INPUTN.  Note
// that the input files are placed on a read-only disk.
//
// Similarly, destinations in GCS may be specified with the --outputs flag.
// Each output file will be exposed by via the environment variables $OUTPUT0
// to $OUTPUTN.
//
// As a convenience, the tool will automatically use the google/cloud-sdk image
// whenever the command line starts with gsutil or gcloud, and will
// automatically include the cloud-platform API scope whenever the cloud-sdk
// container is used.
//
// Any 'export' commands are interpreted prior to execution of the pipeline.
// The specified value is injected into the environment for all subsequent
// commands.
//
// Variable references of the form ${VARIABLE} or $VARIABLE are replaced using
// values from the host environment (unless the value has been overwritten by a
// previous export command).
//
// If any command references '/tmp' the attached persistent disk is mounted
// there, read-write.
//
// If the --output flag is specified, an action is appended that copies the
// combined pipeline output to the specified GCS path.
//
// The --dry-run flag can be used to see what pipeline would be produced
// without executing it.
//
// Example 1: Simple 'hello world' script
//
//    echo "Hello World!"
//
// Example 2: Background action
//
//    while true; do echo "Hello background world!"; sleep 1; done &
//    sleep 5
//    echo "Hello foreground world!"
//
// Example 3: Indexing a BAM
//
//    export BAM=NA12892_S1.bam
//    export BAI=${BAM}.bai
//    gsutil cp gs://my-bucket/${BAM} /tmp
//    index /tmp/${BAM} /tmp/${BAI} # image=gcr.io/genomics-tools/samtools
//    gsutil cp /tmp/${BAI} gs://my-bucket/
//
// Example 4: SHA1 sum a file (using the --inputs and --outputs flags).
//
//    sha1sum ${INPUT0} > ${OUTPUT0}
//
import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	genomics "google.golang.org/api/genomics/v2alpha1"
	"google.golang.org/api/googleapi"
)

var (
	flags = flag.NewFlagSet("", flag.ExitOnError)

	basePath       = flags.String("base-path", "", "optional API service base path")
	name           = flags.String("name", "", "optional name applied as a label")
	script         = flags.String("script", "", "the script to run")
	scopes         = flags.String("scopes", "", "comma separated list of additional API scopes")
	zone           = flags.String("zone", "us-east1-d", "zone to run in")
	output         = flags.String("output", "", "GCS path to write output to")
	dryRun         = flags.Bool("dry-run", false, "don't run, just show pipeline")
	wait           = flags.Bool("wait", true, "wait for the pipeline to finish")
	machineType    = flags.String("machine-type", "n1-standard-1", "machine type to create")
	preemptible    = flags.Bool("preemptible", true, "use a preemptible VM")
	inputs         = flags.String("inputs", "", "comma separated list of GCS objects to localize to the VM")
	outputs        = flags.String("outputs", "", "comma separated list of GCS objects to delocalize from the VM")
	diskSizeGb     = flags.Int("disk-size", 500, "the attached disk size (in GB)")
	privateAddress = flags.Bool("private-address", false, "use a private IP address")
	cloudSDKImage  = flags.String("cloud_sdk_image", "google/cloud-sdk:alpine", "the cloud SDK image to use")
)

const (
	inputRoot  = "/mnt/input"
	outputRoot = "/mnt/output"
	diskName   = "shared"
)

func Invoke(ctx context.Context, service *genomics.Service, project string, arguments []string) error {
	flags.Parse(arguments)

	if *script == "" {
		return errors.New("the script flag is required")
	}

	labels := map[string]string{"script-filename": *script}
	if *name != "" {
		labels["name"] = *name
	}

	environment := make(map[string]string)

	var localizers, delocalizers []*genomics.Action
	for i, input := range listOf(*inputs) {
		filename := filepath.Join(inputRoot, fmt.Sprintf("input%d", i))
		localizers = append(localizers, &genomics.Action{
			ImageUri: *cloudSDKImage,
			Commands: []string{"gsutil", "-q", "cp", input, filename},
			Mounts:   []*genomics.Mount{{Disk: diskName, Path: inputRoot}},
		})
		environment[fmt.Sprintf("INPUT%d", i)] = filename
	}
	for i, output := range listOf(*outputs) {
		filename := filepath.Join(outputRoot, fmt.Sprintf("output%d", i))
		delocalizers = append(delocalizers, &genomics.Action{
			ImageUri: *cloudSDKImage,
			Commands: []string{"gsutil", "-q", "cp", filename, output},
			Flags:    []string{"ALWAYS_RUN"},
			Mounts:   []*genomics.Mount{{Disk: diskName, Path: outputRoot, ReadOnly: true}},
		})
		environment[fmt.Sprintf("OUTPUT%d", i)] = filename
	}
	if *output != "" {
		delocalizers = append(delocalizers, &genomics.Action{
			ImageUri: *cloudSDKImage,
			Commands: []string{"gsutil", "-q", "cp", "/google/logs/output", *output},
			Flags:    []string{"ALWAYS_RUN"},
		})
	}

	actions, err := parseScript(*script, environment)
	if err != nil {
		return fmt.Errorf("creating pipeline from script: %v", err)
	}

	pipeline := &genomics.Pipeline{
		Resources: &genomics.Resources{
			ProjectId: project,
			Zones:     []string{*zone},
			VirtualMachine: &genomics.VirtualMachine{
				MachineType: *machineType,
				Preemptible: *preemptible,
				Disks:       []*genomics.Disk{{Name: diskName, SizeGb: int64(*diskSizeGb)}},
				Network: &genomics.Network{
					UsePrivateAddress: *privateAddress,
				},
				ServiceAccount: &genomics.ServiceAccount{Scopes: listOf(*scopes)},
			},
		},
		Actions:     append(localizers, append(actions, delocalizers...)...),
		Environment: environment,
	}

	addRequiredScopes(pipeline)

	encoded, err := json.MarshalIndent(pipeline, "", "  ")
	if err != nil {
		return fmt.Errorf("encoding pipeline: %v", err)
	}
	fmt.Printf("%s\n", encoded)

	if *dryRun {
		return nil
	}

	req := &genomics.RunPipelineRequest{Pipeline: pipeline, Labels: labels}
	lro, err := service.Pipelines.Run(req).Context(ctx).Do()
	if err != nil {
		if err, ok := err.(*googleapi.Error); ok {
			return fmt.Errorf("starting pipeline: %q: %q", err.Message, err.Body)
		}
		return fmt.Errorf("starting pipeline: %v", err)
	}

	fmt.Printf("Pipeline running as %q\n", lro.Name)
	if *output != "" {
		fmt.Printf("Output will be written to %q\n", *output)
	}

	if !*wait {
		return nil
	}

	result, err := monitor(ctx, service, lro.Name)
	if err != nil {
		return fmt.Errorf("monitoring pipeline: %v", err)
	}

	if status, ok := result.(*genomics.Status); ok {
		return fmt.Errorf("executing pipeline: %s", status.Message)
	}

	fmt.Println("Pipeline execution completed")
	return nil
}

func monitor(ctx context.Context, service *genomics.Service, name string) (interface{}, error) {
	var events []*genomics.Event
	for {
		time.Sleep(5 * time.Second)
		lro, err := service.Projects.Operations.Get(name).Context(ctx).Do()
		if err != nil {
			return nil, fmt.Errorf("getting operation status: %v", err)
		}

		var metadata genomics.Metadata
		if err := json.Unmarshal(lro.Metadata, &metadata); err != nil {
			return nil, fmt.Errorf("parsing metadata: %v", err)
		}

		if len(events) != len(metadata.Events) {
			for i := len(metadata.Events) - len(events) - 1; i >= 0; i-- {
				timestamp, _ := time.Parse(time.RFC3339Nano, metadata.Events[i].Timestamp)
				fmt.Println(timestamp.Format("15:04:05"), metadata.Events[i].Description)
			}
			events = metadata.Events
		}

		if lro.Done {
			if lro.Error != nil {
				return lro.Error, nil
			}
			return lro.Response, nil
		}
	}
}

func parseScript(filename string, globalEnv map[string]string) ([]*genomics.Action, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("opening script: %v", err)
	}
	defer f.Close()

	var line int
	var actions []*genomics.Action
	localEnv := make(map[string]string)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line++
		action, err := parse(scanner.Text(), localEnv, globalEnv)
		if err != nil {
			return nil, fmt.Errorf("line %d: %v", line, err)
		}
		if action != nil {
			actions = append(actions, action)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("reading script: %v", err)
	}
	return actions, nil
}

func parse(line string, localEnv, globalEnv map[string]string) (*genomics.Action, error) {
	var (
		commands []string
		flags    []string
		options  = make(map[string]string)
	)

	if n := strings.Index(line, "#"); n >= 0 {
		for _, option := range strings.Fields(strings.TrimSpace(line[n+1:])) {
			if n := strings.Index(option, "="); n >= 0 {
				options[option[:n]] = option[n+1:]
			} else {
				flags = append(flags, strings.ToUpper(option))
			}
		}
		line = line[:n]
	}

	var missing string
	line = os.Expand(line, func(name string) string {
		if value, ok := localEnv[name]; ok {
			return value
		}
		if value, ok := globalEnv[name]; ok {
			return value
		}
		if value, ok := os.LookupEnv(name); ok {
			return value
		}
		missing = name
		return ""
	})

	if missing != "" {
		return nil, fmt.Errorf("missing value for variable %q", missing)
	}

	commands = strings.Fields(strings.TrimSpace(line))
	if len(commands) == 0 {
		return nil, nil
	}

	if commands[len(commands)-1] == "&" {
		flags = append(flags, "RUN_IN_BACKGROUND")
		commands = commands[:len(commands)-1]
	}

	if commands[0] == "export" {
		fields := strings.SplitN(strings.Join(commands[1:], " "), "=", 2)
		if len(fields) != 2 {
			return nil, fmt.Errorf("missing assignment in export command: %q", line)
		}

		localEnv[fields[0]] = fields[1]
		return nil, nil
	}

	image := detectImage(commands, options)
	mounts := detectMounts(commands)
	if image == "bash" {
		commands = []string{"-c", strings.Join(commands, " ")}
	}

	return &genomics.Action{
		ImageUri:    image,
		Commands:    commands,
		Flags:       flags,
		Environment: localEnv,
		Mounts:      mounts,
	}, nil
}

func detectImage(command []string, options map[string]string) string {
	if image, ok := options["image"]; ok {
		return image
	}
	if isCloudCommand(command[0]) {
		return *cloudSDKImage
	}
	return "bash"
}

func detectMounts(commands []string) []*genomics.Mount {
	var mounts []*genomics.Mount
	if findReference("/tmp", commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: "/tmp"})
	}
	if findReference(outputRoot, commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: outputRoot})
	}
	if findReference(inputRoot, commands) {
		mounts = append(mounts, &genomics.Mount{Disk: diskName, Path: inputRoot, ReadOnly: true})
	}
	return mounts
}

func findReference(root string, commands []string) bool {
	for _, command := range commands {
		if strings.HasPrefix(command, root) {
			return true
		}
	}
	return false
}

func addRequiredScopes(pipeline *genomics.Pipeline) {
	scopes := &pipeline.Resources.VirtualMachine.ServiceAccount.Scopes
	for _, action := range pipeline.Actions {
		if strings.HasPrefix(action.ImageUri, "google/cloud-sdk") || isCloudCommand(action.Commands[0]) {
			*scopes = append(*scopes, "https://www.googleapis.com/auth/devstorage.read_write")
			return
		}
	}
}

func isCloudCommand(command string) bool {
	return command == "gsutil" || command == "gcloud"
}

func listOf(input string) []string {
	if input == "" {
		return nil
	}
	return strings.Split(input, ",")
}
<|endoftext|>


<commit_before><commit_msg>Validate IptablesBackend for Calico and Canal<commit_after><|endoftext|>


<commit_before>/*
 * // Copyright (c) 2017 Cisco and/or its affiliates.
 * //
 * // 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 processor

import (
	nsmodel "github.com/contiv/vpp/plugins/ksr/model/namespace"
	podmodel "github.com/contiv/vpp/plugins/ksr/model/pod"
	policymodel "github.com/contiv/vpp/plugins/ksr/model/policy"
	"github.com/contiv/vpp/plugins/policy/utils"
)

const (
	in           = policymodel.Policy_LabelSelector_LabelExpression_IN
	notIn        = policymodel.Policy_LabelSelector_LabelExpression_NOT_IN
	exists       = policymodel.Policy_LabelSelector_LabelExpression_EXISTS
	doesNotExist = policymodel.Policy_LabelSelector_LabelExpression_DOES_NOT_EXIST
)

// isPodLabelSelectorMatch finds if podLabelSelecor matches Pod's labels or not
func (pp *PolicyProcessor) isPodLabelSelectorMatch(
	pod *podmodel.Pod,
	matchPodLabels []*policymodel.Policy_Label,
	matchPodExpressions []*policymodel.Policy_LabelSelector_LabelExpression,
	policyNamespace string) bool {

	if len(matchPodLabels) > 0 && len(matchPodExpressions) > 0 {
		if !pp.isPodLabelMatch(pod, matchPodLabels, policyNamespace) &&
			!pp.isPodExpressionMatch(pod, matchPodExpressions, policyNamespace) {
			return false
		}
		return true

	} else if len(matchPodLabels) == 0 && len(matchPodExpressions) > 0 {
		if !pp.isPodExpressionMatch(pod, matchPodExpressions, policyNamespace) {
			return false
		}
		return true

	} else if len(matchPodLabels) > 0 && len(matchPodExpressions) == 0 {
		if !pp.isPodLabelMatch(pod, matchPodLabels, policyNamespace) {
			return false
		}
		return true

	}

	return true
}

// isPodLabelMatch finds if pod labels match a collection of pod selector labels.
func (pp *PolicyProcessor) isPodLabelMatch(pod *podmodel.Pod,
	matchPodLabels []*policymodel.Policy_Label, policyNamespace string) bool {

	namespace := pod.Namespace
	podLabels := pod.Label
	labelExists := make(map[string]bool)

	for _, podLabel := range podLabels {
		label := namespace + "/" + podLabel.Key + "/" + podLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchPodLabel := range matchPodLabels {
		label := policyNamespace + "/" + matchPodLabel.Key + "/" + matchPodLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isPodExpressionMatch finds if pod labels match a collection of pod selector expressions.
func (pp *PolicyProcessor) isPodExpressionMatch(pod *podmodel.Pod,
	matchPodExpressions []*policymodel.Policy_LabelSelector_LabelExpression, policyNamespace string) bool {

	podNamespace := pod.Namespace
	podLabels := pod.Label
	podMap := make(map[string]string)

	for _, podLabel := range podLabels {
		podMapKey := podNamespace + "/" + podLabel.Key
		podMap[podMapKey] = podLabel.Value
	}

	for _, matchPodExpression := range matchPodExpressions {
		switch matchPodExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchPodExpression.Key, matchPodExpression.Value)
			for _, label := range labels {
				if podMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchPodExpression.Key, matchPodExpression.Value)
			for _, label := range labels {
				if podMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := policyNamespace + "/" + matchPodExpression.Key
			if podMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := policyNamespace + "/" + matchPodExpression.Key
			if podMap[expressionKey] == "" {
				return false
			}
		}
	}

	return true
}

// isNsLabelSelectorMatch finds if namespace LabelSelector matches Pod's namespace labels.
func (pp *PolicyProcessor) isNsLabelSelectorMatch(
	pod *podmodel.Pod,
	matchNsLabels []*policymodel.Policy_Label,
	matchNsExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {

	if len(matchNsLabels) > 0 && len(matchNsExpressions) > 0 {
		if !pp.isNsLabelMatch(pod, matchNsLabels) &&
			!pp.isNsExpressionMatch(pod, matchNsExpressions) {
			return false
		}
		return true

	} else if len(matchNsLabels) == 0 && len(matchNsExpressions) > 0 {
		if !pp.isNsExpressionMatch(pod, matchNsExpressions) {
			return false
		}
		return true

	} else if len(matchNsLabels) > 0 && len(matchNsExpressions) == 0 {
		if !pp.isNsLabelMatch(pod, matchNsLabels) {
			return false
		}
		return true

	}

	return true
}

// isNsLabelMatch finds if pod's namespace labels match a collection of namespace selector labels.
func (pp *PolicyProcessor) isNsLabelMatch(pod *podmodel.Pod,
	matchNsLabels []*policymodel.Policy_Label) bool {
	//Get Pod's namespace labels
	podNamespaceID := nsmodel.ID(pod.Namespace)
	_, nsData := pp.Cache.LookupNamespace(podNamespaceID)
	nsLabels := nsData.Label
	labelExists := make(map[string]bool)

	for _, nsLabel := range nsLabels {
		label := nsLabel.Key + "/" + nsLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchNsLabel := range matchNsLabels {
		label := matchNsLabel.Key + "/" + matchNsLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isNsExpressionMatch finds if pod's namespace labels match a collection of namespace selector expressions.
func (pp *PolicyProcessor) isNsExpressionMatch(pod *podmodel.Pod,
	matchNsExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {
	//Get Pod's namespace labels
	podNamespaceID := nsmodel.ID(pod.Namespace)
	_, nsData := pp.Cache.LookupNamespace(podNamespaceID)
	nsLabels := nsData.Label
	nsMap := make(map[string]string)

	for _, nsLabel := range nsLabels {
		nsMap[nsLabel.Key] = nsLabel.Value
	}

	for _, matchNsExpression := range matchNsExpressions {
		switch matchNsExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchNsExpression.Key, matchNsExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchNsExpression.Key, matchNsExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := matchNsExpression.Key
			if nsMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := matchNsExpression.Key
			if nsMap[expressionKey] == "" {
				return false
			}
		}
	}
	return true
}

// isNsUpdateLabelSelectorMatch finds if pod's namespace labels match a collection of namespace selector labels,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateLabelSelectorMatch(
	ns *nsmodel.Namespace,
	matchNsUpdateLabels []*policymodel.Policy_Label,
	matchNsUpdateExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {

	if len(matchNsUpdateLabels) > 0 && len(matchNsUpdateExpressions) > 0 {
		if !pp.isNsUpdateLabelMatch(ns, matchNsUpdateLabels) &&
			!pp.isNsUpdateExpressionMatch(ns, matchNsUpdateExpressions) {
			return false
		}
		return true

	} else if len(matchNsUpdateLabels) == 0 && len(matchNsUpdateExpressions) > 0 {
		if !pp.isNsUpdateExpressionMatch(ns, matchNsUpdateExpressions) {
			return false
		}
		return true

	} else if len(matchNsUpdateLabels) > 0 && len(matchNsUpdateExpressions) == 0 {
		if !pp.isNsUpdateLabelMatch(ns, matchNsUpdateLabels) {
			return false
		}
		return true

	}

	return true
}

// isNsUpdateLabelMatch finds if namespace LabelSelector matches Pod's namespace labels,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateLabelMatch(ns *nsmodel.Namespace,
	matchNsUpdateLabels []*policymodel.Policy_Label) bool {
	//Get Pod's namespace labels
	_, nsData := pp.Cache.LookupNamespace(nsmodel.GetID(ns))
	nsLabels := nsData.Label
	labelExists := make(map[string]bool)

	for _, nsLabel := range nsLabels {
		label := nsLabel.Key + "/" + nsLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchNsLabel := range matchNsUpdateLabels {
		label := matchNsLabel.Key + "/" + matchNsLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isNsUpdateExpressionMatch finds if pod's namespace labels match a collection of namespace selector expressions,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateExpressionMatch(ns *nsmodel.Namespace,
	matchNsUpdateExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {
	//Get Pod's namespace labels
	_, nsData := pp.Cache.LookupNamespace(nsmodel.GetID(ns))
	nsLabels := nsData.Label
	nsMap := make(map[string]string)

	for _, nsLabel := range nsLabels {
		nsMap[nsLabel.Key] = nsLabel.Value
	}

	for _, matchNsUpdateExpression := range matchNsUpdateExpressions {
		switch matchNsUpdateExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchNsUpdateExpression.Key, matchNsUpdateExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchNsUpdateExpression.Key, matchNsUpdateExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := matchNsUpdateExpression.Key
			if nsMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := matchNsUpdateExpression.Key
			if nsMap[expressionKey] == "" {
				return false
			}
		}
	}
	return true
}
<commit_msg>Fix condition operator.<commit_after>/*
 * // Copyright (c) 2017 Cisco and/or its affiliates.
 * //
 * // 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 processor

import (
	nsmodel "github.com/contiv/vpp/plugins/ksr/model/namespace"
	podmodel "github.com/contiv/vpp/plugins/ksr/model/pod"
	policymodel "github.com/contiv/vpp/plugins/ksr/model/policy"
	"github.com/contiv/vpp/plugins/policy/utils"
)

const (
	in           = policymodel.Policy_LabelSelector_LabelExpression_IN
	notIn        = policymodel.Policy_LabelSelector_LabelExpression_NOT_IN
	exists       = policymodel.Policy_LabelSelector_LabelExpression_EXISTS
	doesNotExist = policymodel.Policy_LabelSelector_LabelExpression_DOES_NOT_EXIST
)

// isPodLabelSelectorMatch finds if podLabelSelecor matches Pod's labels or not
func (pp *PolicyProcessor) isPodLabelSelectorMatch(
	pod *podmodel.Pod,
	matchPodLabels []*policymodel.Policy_Label,
	matchPodExpressions []*policymodel.Policy_LabelSelector_LabelExpression,
	policyNamespace string) bool {

	if len(matchPodLabels) > 0 && len(matchPodExpressions) > 0 {
		if !pp.isPodLabelMatch(pod, matchPodLabels, policyNamespace) ||
			!pp.isPodExpressionMatch(pod, matchPodExpressions, policyNamespace) {
			return false
		}
		return true

	} else if len(matchPodLabels) == 0 && len(matchPodExpressions) > 0 {
		if !pp.isPodExpressionMatch(pod, matchPodExpressions, policyNamespace) {
			return false
		}
		return true

	} else if len(matchPodLabels) > 0 && len(matchPodExpressions) == 0 {
		if !pp.isPodLabelMatch(pod, matchPodLabels, policyNamespace) {
			return false
		}
		return true

	}

	return true
}

// isPodLabelMatch finds if pod labels match a collection of pod selector labels.
func (pp *PolicyProcessor) isPodLabelMatch(pod *podmodel.Pod,
	matchPodLabels []*policymodel.Policy_Label, policyNamespace string) bool {

	namespace := pod.Namespace
	podLabels := pod.Label
	labelExists := make(map[string]bool)

	for _, podLabel := range podLabels {
		label := namespace + "/" + podLabel.Key + "/" + podLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchPodLabel := range matchPodLabels {
		label := policyNamespace + "/" + matchPodLabel.Key + "/" + matchPodLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isPodExpressionMatch finds if pod labels match a collection of pod selector expressions.
func (pp *PolicyProcessor) isPodExpressionMatch(pod *podmodel.Pod,
	matchPodExpressions []*policymodel.Policy_LabelSelector_LabelExpression, policyNamespace string) bool {

	podNamespace := pod.Namespace
	podLabels := pod.Label
	podMap := make(map[string]string)

	for _, podLabel := range podLabels {
		podMapKey := podNamespace + "/" + podLabel.Key
		podMap[podMapKey] = podLabel.Value
	}

	for _, matchPodExpression := range matchPodExpressions {
		switch matchPodExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchPodExpression.Key, matchPodExpression.Value)
			for _, label := range labels {
				if podMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchPodExpression.Key, matchPodExpression.Value)
			for _, label := range labels {
				if podMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := policyNamespace + "/" + matchPodExpression.Key
			if podMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := policyNamespace + "/" + matchPodExpression.Key
			if podMap[expressionKey] == "" {
				return false
			}
		}
	}

	return true
}

// isNsLabelSelectorMatch finds if namespace LabelSelector matches Pod's namespace labels.
func (pp *PolicyProcessor) isNsLabelSelectorMatch(
	pod *podmodel.Pod,
	matchNsLabels []*policymodel.Policy_Label,
	matchNsExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {

	if len(matchNsLabels) > 0 && len(matchNsExpressions) > 0 {
		if !pp.isNsLabelMatch(pod, matchNsLabels) &&
			!pp.isNsExpressionMatch(pod, matchNsExpressions) {
			return false
		}
		return true

	} else if len(matchNsLabels) == 0 && len(matchNsExpressions) > 0 {
		if !pp.isNsExpressionMatch(pod, matchNsExpressions) {
			return false
		}
		return true

	} else if len(matchNsLabels) > 0 && len(matchNsExpressions) == 0 {
		if !pp.isNsLabelMatch(pod, matchNsLabels) {
			return false
		}
		return true

	}

	return true
}

// isNsLabelMatch finds if pod's namespace labels match a collection of namespace selector labels.
func (pp *PolicyProcessor) isNsLabelMatch(pod *podmodel.Pod,
	matchNsLabels []*policymodel.Policy_Label) bool {
	//Get Pod's namespace labels
	podNamespaceID := nsmodel.ID(pod.Namespace)
	_, nsData := pp.Cache.LookupNamespace(podNamespaceID)
	nsLabels := nsData.Label
	labelExists := make(map[string]bool)

	for _, nsLabel := range nsLabels {
		label := nsLabel.Key + "/" + nsLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchNsLabel := range matchNsLabels {
		label := matchNsLabel.Key + "/" + matchNsLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isNsExpressionMatch finds if pod's namespace labels match a collection of namespace selector expressions.
func (pp *PolicyProcessor) isNsExpressionMatch(pod *podmodel.Pod,
	matchNsExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {
	//Get Pod's namespace labels
	podNamespaceID := nsmodel.ID(pod.Namespace)
	_, nsData := pp.Cache.LookupNamespace(podNamespaceID)
	nsLabels := nsData.Label
	nsMap := make(map[string]string)

	for _, nsLabel := range nsLabels {
		nsMap[nsLabel.Key] = nsLabel.Value
	}

	for _, matchNsExpression := range matchNsExpressions {
		switch matchNsExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchNsExpression.Key, matchNsExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchNsExpression.Key, matchNsExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := matchNsExpression.Key
			if nsMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := matchNsExpression.Key
			if nsMap[expressionKey] == "" {
				return false
			}
		}
	}
	return true
}

// isNsUpdateLabelSelectorMatch finds if pod's namespace labels match a collection of namespace selector labels,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateLabelSelectorMatch(
	ns *nsmodel.Namespace,
	matchNsUpdateLabels []*policymodel.Policy_Label,
	matchNsUpdateExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {

	if len(matchNsUpdateLabels) > 0 && len(matchNsUpdateExpressions) > 0 {
		if !pp.isNsUpdateLabelMatch(ns, matchNsUpdateLabels) &&
			!pp.isNsUpdateExpressionMatch(ns, matchNsUpdateExpressions) {
			return false
		}
		return true

	} else if len(matchNsUpdateLabels) == 0 && len(matchNsUpdateExpressions) > 0 {
		if !pp.isNsUpdateExpressionMatch(ns, matchNsUpdateExpressions) {
			return false
		}
		return true

	} else if len(matchNsUpdateLabels) > 0 && len(matchNsUpdateExpressions) == 0 {
		if !pp.isNsUpdateLabelMatch(ns, matchNsUpdateLabels) {
			return false
		}
		return true

	}

	return true
}

// isNsUpdateLabelMatch finds if namespace LabelSelector matches Pod's namespace labels,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateLabelMatch(ns *nsmodel.Namespace,
	matchNsUpdateLabels []*policymodel.Policy_Label) bool {
	//Get Pod's namespace labels
	_, nsData := pp.Cache.LookupNamespace(nsmodel.GetID(ns))
	nsLabels := nsData.Label
	labelExists := make(map[string]bool)

	for _, nsLabel := range nsLabels {
		label := nsLabel.Key + "/" + nsLabel.Value
		labelExists[label] = true
	}

	isMatch := true
	for _, matchNsLabel := range matchNsUpdateLabels {
		label := matchNsLabel.Key + "/" + matchNsLabel.Value
		if labelExists[label] != true {
			isMatch = false
			break
		}
	}

	return isMatch
}

// isNsUpdateExpressionMatch finds if pod's namespace labels match a collection of namespace selector expressions,
// after an update in namespace labels
func (pp *PolicyProcessor) isNsUpdateExpressionMatch(ns *nsmodel.Namespace,
	matchNsUpdateExpressions []*policymodel.Policy_LabelSelector_LabelExpression) bool {
	//Get Pod's namespace labels
	_, nsData := pp.Cache.LookupNamespace(nsmodel.GetID(ns))
	nsLabels := nsData.Label
	nsMap := make(map[string]string)

	for _, nsLabel := range nsLabels {
		nsMap[nsLabel.Key] = nsLabel.Value
	}

	for _, matchNsUpdateExpression := range matchNsUpdateExpressions {
		switch matchNsUpdateExpression.Operator {
		case in:
			labels := utils.ConstructLabels(matchNsUpdateExpression.Key, matchNsUpdateExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					break
				}
			}
			return false

		case notIn:
			labels := utils.ConstructLabels(matchNsUpdateExpression.Key, matchNsUpdateExpression.Value)
			for _, label := range labels {
				if nsMap[label.Key] == label.Value {
					return false
				}
			}

		case exists:
			expressionKey := matchNsUpdateExpression.Key
			if nsMap[expressionKey] != "" {
				return true
			}

		case doesNotExist:
			expressionKey := matchNsUpdateExpression.Key
			if nsMap[expressionKey] == "" {
				return false
			}
		}
	}
	return true
}
<|endoftext|>


<commit_before>package terminal

import (
	"bytes"
	"errors"
	"fmt"
	"os/exec"
	"strings"
)

const (
	sessionPrefix     = "koding"
	defaultScreenPath = "/usr/bin/screen"
)

type Command struct {
	// Name is used for starting the terminal instance, it's the program path
	// usually
	Name string

	// Args is passed to the program name
	Args []string
}

var (
	ErrNoSession      = errors.New("ErrNoSession")
	ErrInvalidSession = errors.New("ErrInvalidSession")
)

// newCmd returns a new command instance that is used to start the terminal.
// The command line is created differently based on the incoming mode.
func newCommand(mode, session, username string) (*Command, error) {
	// let's assume by default its Screen
	name := defaultScreenPath
	args := []string{"-S"}

	switch mode {
	case "shared", "resume":
		if session == "" {
			return nil, errors.New("session is needed for 'shared' or 'resume' mode")
		}

		if !sessionExists(session, username) {
			return nil, ErrNoSession
		}

		args = append(args, sessionPrefix+"."+session)
		if mode == "shared" {
			args = append(args, "-x") // multiuser mode
		} else if mode == "resume" {
			args = append(args, "-raAd") // resume
		}
	case "noscreen":
		name = "/bin/bash"
		args = []string{}
	case "create":
		args = append(args, sessionPrefix+"."+randomString())
	default:
		return nil, fmt.Errorf("mode '%s' is unknown. Valid modes are:  [shared|noscreen|resume|create]", mode)
	}

	c := &Command{
		Name: name,
		Args: args,
	}

	fmt.Printf("c %#v\n", c)

	return c, nil
}

// screenSessions returns a list of sessions that belongs to the given
// username.  The sessions are in the form of ["k7sdjv12344", "askIj12sas12",
// ...]
func screenSessions(username string) []string {
	// Do not include dead sessions in our result
	exec.Command(defaultScreenPath, "-wipe").Run()

	// We need to use ls here, because /var/run/screen mount is only
	// visible from inside of container. Errors are ignored.
	out, _ := exec.Command("ls", "/var/run/screen/S-"+username).Output()
	shellOut := string(bytes.TrimSpace(out))
	if shellOut == "" {
		return []string{}
	}

	names := strings.Split(shellOut, "\n")
	sessions := make([]string, len(names))

	prefix := sessionPrefix + "."
	for i, name := range names {
		segments := strings.SplitN(name, ".", 2)
		sessions[i] = strings.TrimPrefix(segments[1], prefix)
	}

	return sessions
}

// screenExists checks whether the given session exists in the running list of
// screen sessions.
func sessionExists(session, username string) bool {
	for _, s := range screenSessions(username) {
		if s == session {
			return true
		}
	}

	return false
}

// killSession kills the given SessionID
func killSession(session string) error {
	out, err := exec.Command(defaultScreenPath, "-X", "-S", sessionPrefix+"."+session, "kill").Output()
	if err != nil {
		return commandError("screen kill failed", err, out)
	}

	return nil
}

func commandError(message string, err error, out []byte) error {
	return fmt.Errorf("%s\n%s\n%s", message, err.Error(), string(out))
}
<commit_msg>terminal: add note about darwin<commit_after>package terminal

import (
	"bytes"
	"errors"
	"fmt"
	"os/exec"
	"strings"
)

const (
	sessionPrefix     = "koding"
	defaultScreenPath = "/usr/bin/screen"
)

type Command struct {
	// Name is used for starting the terminal instance, it's the program path
	// usually
	Name string

	// Args is passed to the program name
	Args []string
}

var (
	ErrNoSession      = errors.New("ErrNoSession")
	ErrInvalidSession = errors.New("ErrInvalidSession")
)

// newCmd returns a new command instance that is used to start the terminal.
// The command line is created differently based on the incoming mode.
func newCommand(mode, session, username string) (*Command, error) {
	// let's assume by default its Screen
	name := defaultScreenPath
	args := []string{"-S"}

	switch mode {
	case "shared", "resume":
		if session == "" {
			return nil, errors.New("session is needed for 'shared' or 'resume' mode")
		}

		if !sessionExists(session, username) {
			return nil, ErrNoSession
		}

		args = append(args, sessionPrefix+"."+session)
		if mode == "shared" {
			args = append(args, "-x") // multiuser mode
		} else if mode == "resume" {
			args = append(args, "-raAd") // resume
		}
	case "noscreen":
		name = "/bin/bash"
		args = []string{}
	case "create":
		args = append(args, sessionPrefix+"."+randomString())
	default:
		return nil, fmt.Errorf("mode '%s' is unknown. Valid modes are:  [shared|noscreen|resume|create]", mode)
	}

	c := &Command{
		Name: name,
		Args: args,
	}

	fmt.Printf("c %#v\n", c)

	return c, nil
}

// screenSessions returns a list of sessions that belongs to the given
// username.  The sessions are in the form of ["k7sdjv12344", "askIj12sas12",
// ...]
// TODO: socket directory is different under darwin, it will not work probably
func screenSessions(username string) []string {
	// Do not include dead sessions in our result
	exec.Command(defaultScreenPath, "-wipe").Run()

	// We need to use ls here, because /var/run/screen mount is only
	// visible from inside of container. Errors are ignored.
	out, _ := exec.Command("ls", "/var/run/screen/S-"+username).Output()
	shellOut := string(bytes.TrimSpace(out))
	if shellOut == "" {
		return []string{}
	}

	names := strings.Split(shellOut, "\n")
	sessions := make([]string, len(names))

	prefix := sessionPrefix + "."
	for i, name := range names {
		segments := strings.SplitN(name, ".", 2)
		sessions[i] = strings.TrimPrefix(segments[1], prefix)
	}

	return sessions
}

// screenExists checks whether the given session exists in the running list of
// screen sessions.
func sessionExists(session, username string) bool {
	for _, s := range screenSessions(username) {
		if s == session {
			return true
		}
	}

	return false
}

// killSession kills the given SessionID
func killSession(session string) error {
	out, err := exec.Command(defaultScreenPath, "-X", "-S", sessionPrefix+"."+session, "kill").Output()
	if err != nil {
		return commandError("screen kill failed", err, out)
	}

	return nil
}

func commandError(message string, err error, out []byte) error {
	return fmt.Errorf("%s\n%s\n%s", message, err.Error(), string(out))
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"strings"

	"github.com/coreos/go-etcd/etcd"
	"github.com/lytics/metafora"
	"github.com/lytics/metafora/m_etcd"
)

const lflags = log.Ldate | log.Lmicroseconds | log.Lshortfile

var logger = log.New(os.Stdout, "", lflags)

func main() {
	mlvl := metafora.LogLevelInfo

	peers := flag.String("etcd", "127.0.0.1:5001", "comma delimited etcd peer list")
	namespace := flag.String("namespace", "koalemos", "metafora namespace")
	name := flag.String("name", "", "node name or empty for automatic")
	loglvl := flag.String("log", mlvl.String(), "set log level: [debug], info, warn, error")
	flag.Parse()

	etcdc := etcd.NewClient(strings.Split(*peers, ","))

	if !etcdc.SyncCluster() {
		log.Fatalf("Unable to connect to etcd cluster: %s", *peers)
	}

	switch *loglvl {
	case "debug":
		mlvl = metafora.LogLevelDebug
	case "warn":
		mlvl = metafora.LogLevelWarn
	case "error":
		mlvl = metafora.LogLevelError
	}

	hfunc := makeHandlerFunc(etcdc)
	coord := m_etcd.NewEtcdCoordinator(*name, *namespace, etcdc).(*m_etcd.EtcdCoordinator)
	bal := &metafora.DumbBalancer{}
	c := metafora.NewConsumer(coord, hfunc, bal)
	log.Printf(
		"Starting koalsmosd with etcd=%s; namespace=%s; name=%s; loglvl=%s",
		*peers, *namespace, coord.NodeID, mlvl)
	go c.Run()

	sigC := make(chan os.Signal, 1)
	signal.Notify(sigC, os.Interrupt, os.Kill)
	s := <-sigC
	log.Printf("Received signal %s, shutting down", s)
	c.Shutdown()
	log.Printf("Shutdown")
}
<commit_msg>Set logger in koalemosd<commit_after>package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"strings"

	"github.com/coreos/go-etcd/etcd"
	"github.com/lytics/metafora"
	"github.com/lytics/metafora/m_etcd"
)

const lflags = log.Ldate | log.Lmicroseconds | log.Lshortfile

var logger = log.New(os.Stdout, "", lflags)

func main() {
	mlvl := metafora.LogLevelInfo

	peers := flag.String("etcd", "127.0.0.1:5001", "comma delimited etcd peer list")
	namespace := flag.String("namespace", "koalemos", "metafora namespace")
	name := flag.String("name", "", "node name or empty for automatic")
	loglvl := flag.String("log", mlvl.String(), "set log level: [debug], info, warn, error")
	flag.Parse()

	etcdc := etcd.NewClient(strings.Split(*peers, ","))

	if !etcdc.SyncCluster() {
		log.Fatalf("Unable to connect to etcd cluster: %s", *peers)
	}

	switch *loglvl {
	case "debug":
		mlvl = metafora.LogLevelDebug
	case "warn":
		mlvl = metafora.LogLevelWarn
	case "error":
		mlvl = metafora.LogLevelError
	}

	hfunc := makeHandlerFunc(etcdc)
	coord := m_etcd.NewEtcdCoordinator(*name, *namespace, etcdc).(*m_etcd.EtcdCoordinator)
	bal := &metafora.DumbBalancer{}
	c := metafora.NewConsumer(coord, hfunc, bal)
	c.SetLogger(logger, mlvl)
	log.Printf(
		"Starting koalsmosd with etcd=%s; namespace=%s; name=%s; loglvl=%s",
		*peers, *namespace, coord.NodeID, mlvl)
	go c.Run()

	sigC := make(chan os.Signal, 1)
	signal.Notify(sigC, os.Interrupt, os.Kill)
	s := <-sigC
	log.Printf("Received signal %s, shutting down", s)
	c.Shutdown()
	log.Printf("Shutdown")
}
<|endoftext|>


<commit_before>package plugins

import (
	"fmt"

	"golang.org/x/net/context"
)

var (
	ctx     context.Context
	plugins = []Plugin{}
	stop    = false
)

type Plugin struct {
	Key interface{}
	BotMessagePlugin
}

func (p Plugin) Name() string {
	return fmt.Sprintf("%s", p.Key)
}

func AddPlugin(key interface{}, val BotMessagePlugin) {
	fmt.Println("insert plugin ", key)
	plugins = append(plugins, Plugin{key, val})
}

func DelPlugin(key interface{}) {
	// TODO: 未実装
	panic("未実装")
}

// Stop bot stop
func Stop() {
	stop = true
}

// Start bot start
func Start() {
	stop = false
}

// GetPlugins returns all plugins
func GetPlugins() []Plugin {
	return plugins
}

func init() {
	ctx = context.Background()
}

type BotMessagePlugin interface {
	CheckMessage(ctx context.Context, message string) (bool, string)
	DoAction(ctx context.Context, message string) bool
}

func Context() context.Context {
	return ctx
}

func ExecPlugins(ctx context.Context, message string) {
	if stop {
		return
	}

	for _, p := range plugins {
		ok, m := p.CheckMessage(ctx, message)
		if !ok {
			continue
		}

		next := p.DoAction(ctx, m)
		if !next {
			break
		}
	}
}
<commit_msg>pluginの実行はstopしないでsendMessgeだけstopするようにした<commit_after>package plugins

import (
	"fmt"

	"golang.org/x/net/context"
)

var (
	ctx     context.Context
	plugins = []Plugin{}
	stop    = false
)

type Plugin struct {
	Key interface{}
	BotMessagePlugin
}

func (p Plugin) Name() string {
	return fmt.Sprintf("%s", p.Key)
}

func AddPlugin(key interface{}, val BotMessagePlugin) {
	fmt.Println("insert plugin ", key)
	plugins = append(plugins, Plugin{key, val})
}

func DelPlugin(key interface{}) {
	// TODO: 未実装
	panic("未実装")
}

// Stop bot stop
func Stop() {
	stop = true
}

// Start bot start
func Start() {
	stop = false
}

// GetPlugins returns all plugins
func GetPlugins() []Plugin {
	return plugins
}

func init() {
	ctx = context.Background()
}

type BotMessagePlugin interface {
	CheckMessage(ctx context.Context, message string) (bool, string)
	DoAction(ctx context.Context, message string) bool
}

func Context() context.Context {
	return ctx
}

func ExecPlugins(ctx context.Context, message string) {
	for _, p := range plugins {
		ok, m := p.CheckMessage(ctx, message)
		if !ok {
			continue
		}

		next := p.DoAction(ctx, m)
		if !next {
			break
		}
	}
}
<|endoftext|>


<commit_before>package pm

import (
	"crypto/rand"
	"math/big"
	"reflect"
	"strings"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/ethereum/go-ethereum/accounts"
	ethcommon "github.com/ethereum/go-ethereum/common"
)

func TestStartSession_GivenSomeRecipientRandHash_UsesItAsSessionId(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := ethcommon.Address{}
	ticketParams := defaultTicketParams(t)
	expectedSessionID := ticketParams.RecipientRandHash.Hex()

	sessionID := sender.StartSession(recipient, TicketParams{
		FaceValue:         big.NewInt(0),
		WinProb:           big.NewInt(0),
		Seed:              big.NewInt(0),
		RecipientRandHash: ticketParams.RecipientRandHash,
	})

	if sessionID != expectedSessionID {
		t.Errorf("expected %v to equal %v", sessionID, expectedSessionID)
	}
}

func TestStartSession_GivenConcurrentUsage_RecordsAllSessions(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := ethcommon.Address{}

	var sessions []string
	var sessionCount int32
	ch := make(chan struct{})
	for i := 0; i < 100; i++ {
		ticketParams := defaultTicketParams(t)
		expectedSessionID := ticketParams.RecipientRandHash.Hex()
		sessions = append(sessions, expectedSessionID)

		go func() {
			sender.StartSession(recipient, TicketParams{
				FaceValue:         big.NewInt(0),
				WinProb:           big.NewInt(0),
				Seed:              big.NewInt(0),
				RecipientRandHash: ticketParams.RecipientRandHash,
			})
			currentCount := atomic.AddInt32(&sessionCount, 1)
			if currentCount >= 100 {
				ch <- struct{}{}
			}
		}()
	}
	// waiting for all session to be created
	<-ch

	for _, sessionID := range sessions {
		_, ok := sender.sessions.Load(sessionID)
		if !ok {
			t.Errorf("expected to find sessionID in sender. sessionID: %v", sessionID)
		}
	}
}

func TestCreateTicket_GivenNonexistentSession_ReturnsError(t *testing.T) {
	sender := defaultNewSender(t)

	_, _, _, err := sender.CreateTicket("foo")

	if err == nil {
		t.Errorf("expected an error for nonexistent session")
	}
	if !strings.Contains(err.Error(), "unknown session") {
		t.Errorf("expected error to contain 'unknown session' but instead got: %v", err.Error())
	}
}

func TestCreateTicket_GivenValidSessionId_UsesSessionParamsInTicket(t *testing.T) {
	sender := defaultNewSender(t)
	am := sender.accountManager.(*stubAccountManager)
	am.signShouldFail = false
	am.saveSignRequest = true
	am.signResponse = randBytesOrFatal(42, t)
	senderAddress := sender.accountManager.Account().Address
	recipient := randAddressOrFatal(t)
	recipientRandHash := randHashOrFatal(t)
	ticketParams := TicketParams{
		FaceValue:         big.NewInt(1111),
		WinProb:           big.NewInt(2222),
		Seed:              big.NewInt(3333),
		RecipientRandHash: recipientRandHash,
	}
	sessionID := sender.StartSession(recipient, ticketParams)

	ticket, actualSeed, actualSig, err := sender.CreateTicket(sessionID)

	if err != nil {
		t.Errorf("error tryint to create a ticket: %v", err)
	}
	if ticket.Sender != senderAddress {
		t.Errorf("expected ticket sender %v to be %v", ticket.Sender, senderAddress)
	}
	if ticket.Recipient != recipient {
		t.Errorf("expeceted ticket recipient %v to be %v", ticket.Recipient, recipient)
	}
	if ticket.RecipientRandHash != recipientRandHash {
		t.Errorf("expected ticket recipientRandHash %v to be %v", ticket.RecipientRandHash, recipientRandHash)
	}
	if ticket.FaceValue != ticketParams.FaceValue {
		t.Errorf("expected ticket FaceValue %v to be %v", ticket.FaceValue, ticketParams.FaceValue)
	}
	if ticket.WinProb != ticketParams.WinProb {
		t.Errorf("expected ticket WinProb %v to be %v", ticket.WinProb, ticketParams.WinProb)
	}
	if ticket.SenderNonce != 1 {
		t.Errorf("expected ticket SenderNonce %d to be 1", ticket.SenderNonce)
	}
	if actualSeed != ticketParams.Seed {
		t.Errorf("expected actual seed %d to be %d", actualSeed, ticketParams.Seed)
	}
	if !reflect.DeepEqual(actualSig, am.signResponse) {
		t.Errorf("expected actual sig %v to be %v", actualSig, am.signResponse)
	}
	if !reflect.DeepEqual(am.lastSignRequest, ticket.Hash().Bytes()) {
		t.Errorf("expected sig message bytes %v to be %v", am.lastSignRequest, ticket.Hash().Bytes())
	}
}

func TestCreateTicket_GivenSigningError_ReturnsError(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := randAddressOrFatal(t)
	ticketParams := defaultTicketParams(t)
	sessionID := sender.StartSession(recipient, ticketParams)
	am := sender.accountManager.(*stubAccountManager)
	am.signShouldFail = true

	_, _, _, err := sender.CreateTicket(sessionID)

	if err == nil {
		t.Errorf("expected an error when trying to sign the ticket")
	}
	if !strings.Contains(err.Error(), "error signing") {
		t.Errorf("expected error to contain 'error signing' but instead got: %v", err.Error())
	}
}

func TestCreateTicket_GivenConcurrentCallsForSameSession_SenderNonceIncrementsCorrectly(t *testing.T) {
	totalTickets := 100
	lock := sync.RWMutex{}
	sender := defaultNewSender(t)
	recipient := randAddressOrFatal(t)
	ticketParams := defaultTicketParams(t)
	sessionID := sender.StartSession(recipient, ticketParams)

	var ticketCount int32
	ch := make(chan struct{})
	var tickets []*Ticket
	for i := 0; i < totalTickets; i++ {

		go func() {
			ticket, _, _, _ := sender.CreateTicket(sessionID)

			lock.Lock()
			tickets = append(tickets, ticket)
			lock.Unlock()

			currentCount := atomic.AddInt32(&ticketCount, 1)
			if currentCount >= int32(totalTickets) {
				ch <- struct{}{}
			}
		}()
	}
	// waiting for all session to be created
	<-ch

	sessionUntyped, ok := sender.sessions.Load(sessionID)
	if !ok {
		t.Fatalf("failed to find session with ID %v", sessionID)
	}
	session := sessionUntyped.(*session)
	if session.senderNonce != uint64(totalTickets) {
		t.Errorf("expected end state SenderNonce %d to be %d", session.senderNonce, totalTickets)
	}

	uniqueNonces := make(map[uint64]bool)
	for _, ticket := range tickets {
		uniqueNonces[ticket.SenderNonce] = true
	}
	if len(uniqueNonces) != totalTickets {
		t.Errorf("expected unique nonces count %d to be %d", len(uniqueNonces), totalTickets)
	}
}

func defaultNewSender(t *testing.T) *defaultSender {
	account := accounts.Account{
		Address: randAddressOrFatal(t),
	}
	am := &stubAccountManager{
		account: account,
	}
	sender := NewSender(am)
	return sender.(*defaultSender)
}

func defaultTicketParams(t *testing.T) TicketParams {
	recipientRandHash := randHashOrFatal(t)
	return TicketParams{
		FaceValue:         big.NewInt(0),
		WinProb:           big.NewInt(0),
		Seed:              big.NewInt(0),
		RecipientRandHash: recipientRandHash,
	}
}

func randHashOrFatal(t *testing.T) ethcommon.Hash {
	key, err := randBytes(32)

	if err != nil {
		t.Fatalf("failed generating random hash: %v", err)
		return ethcommon.Hash{}
	}

	return ethcommon.BytesToHash(key[:])
}

func randAddressOrFatal(t *testing.T) ethcommon.Address {
	key, err := randBytes(addressSize)

	if err != nil {
		t.Fatalf("failed generating random address: %v", err)
		return ethcommon.Address{}
	}

	return ethcommon.BytesToAddress(key[:])
}

func randBytesOrFatal(size int, t *testing.T) []byte {
	res, err := randBytes(size)

	if err != nil {
		t.Fatalf("failed generating random bytes: %v", err)
		return nil
	}

	return res
}

func randBytes(size int) ([]byte, error) {
	key := make([]byte, size)
	_, err := rand.Read(key)

	return key, err
}
<commit_msg>PM: test cleanup<commit_after>package pm

import (
	"crypto/rand"
	"math/big"
	"reflect"
	"strings"
	"sync"
	"testing"

	"github.com/ethereum/go-ethereum/accounts"
	ethcommon "github.com/ethereum/go-ethereum/common"
)

func TestStartSession_GivenSomeRecipientRandHash_UsesItAsSessionId(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := ethcommon.Address{}
	ticketParams := defaultTicketParams(t)
	expectedSessionID := ticketParams.RecipientRandHash.Hex()

	sessionID := sender.StartSession(recipient, TicketParams{
		FaceValue:         big.NewInt(0),
		WinProb:           big.NewInt(0),
		Seed:              big.NewInt(0),
		RecipientRandHash: ticketParams.RecipientRandHash,
	})

	if sessionID != expectedSessionID {
		t.Errorf("expected %v to equal %v", sessionID, expectedSessionID)
	}
}

func TestStartSession_GivenConcurrentUsage_RecordsAllSessions(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := ethcommon.Address{}

	var sessions []string
	var wg sync.WaitGroup
	wg.Add(100)
	for i := 0; i < 100; i++ {
		ticketParams := defaultTicketParams(t)
		expectedSessionID := ticketParams.RecipientRandHash.Hex()
		sessions = append(sessions, expectedSessionID)

		go func() {
			sender.StartSession(recipient, TicketParams{
				FaceValue:         big.NewInt(0),
				WinProb:           big.NewInt(0),
				Seed:              big.NewInt(0),
				RecipientRandHash: ticketParams.RecipientRandHash,
			})
			wg.Done()
		}()
	}
	wg.Wait()

	for _, sessionID := range sessions {
		_, ok := sender.sessions.Load(sessionID)
		if !ok {
			t.Errorf("expected to find sessionID in sender. sessionID: %v", sessionID)
		}
	}
}

func TestCreateTicket_GivenNonexistentSession_ReturnsError(t *testing.T) {
	sender := defaultNewSender(t)

	_, _, _, err := sender.CreateTicket("foo")

	if err == nil {
		t.Errorf("expected an error for nonexistent session")
	}
	if !strings.Contains(err.Error(), "unknown session") {
		t.Errorf("expected error to contain 'unknown session' but instead got: %v", err.Error())
	}
}

func TestCreateTicket_GivenValidSessionId_UsesSessionParamsInTicket(t *testing.T) {
	sender := defaultNewSender(t)
	am := sender.accountManager.(*stubAccountManager)
	am.signShouldFail = false
	am.saveSignRequest = true
	am.signResponse = randBytesOrFatal(42, t)
	senderAddress := sender.accountManager.Account().Address
	recipient := randAddressOrFatal(t)
	recipientRandHash := randHashOrFatal(t)
	ticketParams := TicketParams{
		FaceValue:         big.NewInt(1111),
		WinProb:           big.NewInt(2222),
		Seed:              big.NewInt(3333),
		RecipientRandHash: recipientRandHash,
	}
	sessionID := sender.StartSession(recipient, ticketParams)

	ticket, actualSeed, actualSig, err := sender.CreateTicket(sessionID)

	if err != nil {
		t.Errorf("error tryint to create a ticket: %v", err)
	}
	if ticket.Sender != senderAddress {
		t.Errorf("expected ticket sender %v to be %v", ticket.Sender, senderAddress)
	}
	if ticket.Recipient != recipient {
		t.Errorf("expeceted ticket recipient %v to be %v", ticket.Recipient, recipient)
	}
	if ticket.RecipientRandHash != recipientRandHash {
		t.Errorf("expected ticket recipientRandHash %v to be %v", ticket.RecipientRandHash, recipientRandHash)
	}
	if ticket.FaceValue != ticketParams.FaceValue {
		t.Errorf("expected ticket FaceValue %v to be %v", ticket.FaceValue, ticketParams.FaceValue)
	}
	if ticket.WinProb != ticketParams.WinProb {
		t.Errorf("expected ticket WinProb %v to be %v", ticket.WinProb, ticketParams.WinProb)
	}
	if ticket.SenderNonce != 1 {
		t.Errorf("expected ticket SenderNonce %d to be 1", ticket.SenderNonce)
	}
	if actualSeed != ticketParams.Seed {
		t.Errorf("expected actual seed %d to be %d", actualSeed, ticketParams.Seed)
	}
	if !reflect.DeepEqual(actualSig, am.signResponse) {
		t.Errorf("expected actual sig %v to be %v", actualSig, am.signResponse)
	}
	if !reflect.DeepEqual(am.lastSignRequest, ticket.Hash().Bytes()) {
		t.Errorf("expected sig message bytes %v to be %v", am.lastSignRequest, ticket.Hash().Bytes())
	}
}

func TestCreateTicket_GivenSigningError_ReturnsError(t *testing.T) {
	sender := defaultNewSender(t)
	recipient := randAddressOrFatal(t)
	ticketParams := defaultTicketParams(t)
	sessionID := sender.StartSession(recipient, ticketParams)
	am := sender.accountManager.(*stubAccountManager)
	am.signShouldFail = true

	_, _, _, err := sender.CreateTicket(sessionID)

	if err == nil {
		t.Errorf("expected an error when trying to sign the ticket")
	}
	if !strings.Contains(err.Error(), "error signing") {
		t.Errorf("expected error to contain 'error signing' but instead got: %v", err.Error())
	}
}

func TestCreateTicket_GivenConcurrentCallsForSameSession_SenderNonceIncrementsCorrectly(t *testing.T) {
	totalTickets := 100
	lock := sync.RWMutex{}
	sender := defaultNewSender(t)
	recipient := randAddressOrFatal(t)
	ticketParams := defaultTicketParams(t)
	sessionID := sender.StartSession(recipient, ticketParams)

	var wg sync.WaitGroup
	wg.Add(totalTickets)
	var tickets []*Ticket
	for i := 0; i < totalTickets; i++ {

		go func() {
			ticket, _, _, _ := sender.CreateTicket(sessionID)

			lock.Lock()
			tickets = append(tickets, ticket)
			lock.Unlock()

			wg.Done()
		}()
	}
	wg.Wait()

	sessionUntyped, ok := sender.sessions.Load(sessionID)
	if !ok {
		t.Fatalf("failed to find session with ID %v", sessionID)
	}
	session := sessionUntyped.(*session)
	if session.senderNonce != uint64(totalTickets) {
		t.Errorf("expected end state SenderNonce %d to be %d", session.senderNonce, totalTickets)
	}

	uniqueNonces := make(map[uint64]bool)
	for _, ticket := range tickets {
		uniqueNonces[ticket.SenderNonce] = true
	}
	if len(uniqueNonces) != totalTickets {
		t.Errorf("expected unique nonces count %d to be %d", len(uniqueNonces), totalTickets)
	}
}

func defaultNewSender(t *testing.T) *defaultSender {
	account := accounts.Account{
		Address: randAddressOrFatal(t),
	}
	am := &stubAccountManager{
		account: account,
	}
	sender := NewSender(am)
	return sender.(*defaultSender)
}

func defaultTicketParams(t *testing.T) TicketParams {
	recipientRandHash := randHashOrFatal(t)
	return TicketParams{
		FaceValue:         big.NewInt(0),
		WinProb:           big.NewInt(0),
		Seed:              big.NewInt(0),
		RecipientRandHash: recipientRandHash,
	}
}

func randHashOrFatal(t *testing.T) ethcommon.Hash {
	key, err := randBytes(32)

	if err != nil {
		t.Fatalf("failed generating random hash: %v", err)
		return ethcommon.Hash{}
	}

	return ethcommon.BytesToHash(key[:])
}

func randAddressOrFatal(t *testing.T) ethcommon.Address {
	key, err := randBytes(addressSize)

	if err != nil {
		t.Fatalf("failed generating random address: %v", err)
		return ethcommon.Address{}
	}

	return ethcommon.BytesToAddress(key[:])
}

func randBytesOrFatal(size int, t *testing.T) []byte {
	res, err := randBytes(size)

	if err != nil {
		t.Fatalf("failed generating random bytes: %v", err)
		return nil
	}

	return res
}

func randBytes(size int) ([]byte, error) {
	key := make([]byte, size)
	_, err := rand.Read(key)

	return key, err
}
<|endoftext|>


<commit_before>package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"github.com/streadway/amqp"
	"koding/tools/amqputil"
	"log"
)

type Consumer struct {
	conn    *amqp.Connection
	channel *amqp.Channel
	tag     string
}

type Producer struct {
	conn    *amqp.Connection
	channel *amqp.Channel
}

type JoinMsg struct {
	Name        string `json:"name"`
	BindingKey  string `json:"bindingKey"`
	Exchange    string `json:"exchange"`
	RoutingKey  string `json:"routingKey"`
	ConsumerTag string
	Suffix      string `json:"suffix"`
}

type LeaveMsg struct {
	RoutingKey string `json:"routingKey"`
}

var authPairs map[string]JoinMsg
var exchanges map[string]uint
var producer *Producer

func main() {
	log.Println("routing worker started")

	authPairs = make(map[string]JoinMsg)
	exchanges = make(map[string]uint)

	var err error
	producer, err = createProducer()
	if err != nil {
		log.Println(err)
	}

	startRouting()
}

func startRouting() {
	c := &Consumer{
		conn:    nil,
		channel: nil,
		tag:     "",
	}

	var err error

	log.Printf("creating consumer connections")
	c.conn = amqputil.CreateConnection("routing")
	c.channel = amqputil.CreateChannel(c.conn)

	err = c.channel.ExchangeDeclare("routing-control", "fanout", false, true, false, false, nil)
	if err != nil {
		log.Fatalf("exchange.declare: %s", err)
	}

	if _, err := c.channel.QueueDeclare("", false, true, false, false, nil); err != nil {
		log.Fatalf("queue.declare: %s", err)
	}

	if err := c.channel.QueueBind("", "", "routing-control", false, nil); err != nil {
		log.Fatalf("queue.bind: %s", err)
	}

	authStream, err := c.channel.Consume("", "", true, false, false, false, nil)
	if err != nil {
		log.Fatalf("basic.consume: %s", err)
	}

	log.Println("routing started...")
	for msg := range authStream {
		log.Printf("got %dB message data: [%v]-[%s] %s",
			len(msg.Body),
			msg.DeliveryTag,
			msg.RoutingKey,
			msg.Body)

		switch msg.RoutingKey {
		case "auth.join":
			var join JoinMsg
			err := json.Unmarshal(msg.Body, &join)
			if err != nil {
				log.Print("bad json incoming msg: ", err)
			}

			join.ConsumerTag = generateUniqueConsumerTag(join.BindingKey)
			authPairs[join.RoutingKey] = join

			log.Println("Auth pairs:", authPairs) // this is just for debug

			declareExchange(c, join.Exchange)

			go consumeAndRepublish(c, join.Exchange, join.BindingKey, join.RoutingKey, join.Suffix, join.ConsumerTag)
		case "auth.leave":
			var leave LeaveMsg
			err := json.Unmarshal(msg.Body, &leave)
			if err != nil {
				log.Print("bad json incoming msg: ", err)
			}

			// cancel consuming
			err = c.channel.Cancel(authPairs[leave.RoutingKey].ConsumerTag, false)
			if err != nil {
				log.Fatalf("basic.cancel: %s", err)
			}
			decrementExchangeCounter(leave)

		default:
			log.Println("routing key is not defined: ", msg.RoutingKey)
		}
	}
}

func generateUniqueConsumerTag(bindingKey string) string {
	r := make([]byte, 32/8)
	rand.Read(r)
	return bindingKey + "." + base64.StdEncoding.EncodeToString(r)
}

func generateUniqueQueueName() string {
	r := make([]byte, 32/8)
	rand.Read(r)
	return base64.StdEncoding.EncodeToString(r)
}

func declareExchange(c *Consumer, exchange string) {
	if exchanges[exchange] <= 0 {
		if err := c.channel.ExchangeDeclare(exchange, "topic", false, true, false, false, nil); err != nil {
			log.Fatalf("exchange.declare: %s", err)
		}
		exchanges[exchange] = 0
	}
	exchanges[exchange]++
}

func decrementExchangeCounter(leave LeaveMsg) {
	exchange := authPairs[leave.RoutingKey].Exchange
	// decrement exchange counter
	exchanges[exchange]--
	// delete authPairs map
	delete(authPairs, leave.RoutingKey)
}

func consumeAndRepublish(c *Consumer, exchange, bindingKey, routingKey, suffix string, consumerTag string) {
	log.Printf("Consume from:\n exchange %s\n bindingKey %s\n routingKey %s\n consumerTag %s\n",
		exchange, bindingKey, routingKey, consumerTag)

	if len(suffix) > 0 {
		routingKey += suffix
	}

	uniqueQueueName := generateUniqueQueueName()

	if _, err := c.channel.QueueDeclare(uniqueQueueName, false, true, true, false, nil); err != nil {
		log.Fatalf("queue.declare: %s", err)
	}

	if err := c.channel.QueueBind(uniqueQueueName, bindingKey, exchange, false, nil); err != nil {
		log.Fatalf("queue.bind: %s", err)
	}

	messages, err := c.channel.Consume(uniqueQueueName, consumerTag, true, false, false, false, nil)
	if err != nil {
		log.Fatalf("basic.consume: %s", err)
	}

	for msg := range messages {
		log.Printf("messages stream got %dB message data: [%v] %s",
			len(msg.Body),
			msg.DeliveryTag,
			msg.Body)

		publishToBroker(msg.Body, routingKey)
	}

}

func publishToBroker(data []byte, routingKey string) {
	msg := amqp.Publishing{
		Headers:         amqp.Table{},
		ContentType:     "text/plain",
		ContentEncoding: "",
		Body:            data,
		DeliveryMode:    1, // 1=non-persistent, 2=persistent
		Priority:        0, // 0-9
	}

	log.Println("publishing data ", string(data), routingKey)
	err := producer.channel.Publish("broker", routingKey, false, false, msg)
	if err != nil {
		log.Printf("error while publishing proxy message: %s", err)
	}

}

func createProducer() (*Producer, error) {
	p := &Producer{
		conn:    nil,
		channel: nil,
	}

	log.Printf("creating publisher connections")

	p.conn = amqputil.CreateConnection("routing")
	p.channel = amqputil.CreateChannel(p.conn)

	return p, nil
}
<commit_msg>rerouting: quick fix for the race condition<commit_after>package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"github.com/streadway/amqp"
	"koding/tools/amqputil"
	"log"
)

type Consumer struct {
	conn    *amqp.Connection
	channel *amqp.Channel
	tag     string
}

type Producer struct {
	conn    *amqp.Connection
	channel *amqp.Channel
}

type JoinMsg struct {
	Name        string `json:"name"`
	BindingKey  string `json:"bindingKey"`
	Exchange    string `json:"exchange"`
	RoutingKey  string `json:"routingKey"`
	ConsumerTag string
	Suffix      string `json:"suffix"`
}

type LeaveMsg struct {
	RoutingKey string `json:"routingKey"`
}

var authPairs map[string]JoinMsg
var exchanges map[string]uint
var producer *Producer

func main() {
	log.Println("routing worker started")

	authPairs = make(map[string]JoinMsg)
	exchanges = make(map[string]uint)

	var err error
	producer, err = createProducer()
	if err != nil {
		log.Println(err)
	}

	startRouting()
}

func startRouting() {
	c := &Consumer{
		conn:    nil,
		channel: nil,
		tag:     "",
	}

	var err error

	log.Printf("creating consumer connections")
	c.conn = amqputil.CreateConnection("routing")
	c.channel = amqputil.CreateChannel(c.conn)

	err = c.channel.ExchangeDeclare("routing-control", "fanout", false, true, false, false, nil)
	if err != nil {
		log.Fatalf("exchange.declare: %s", err)
	}

	if _, err := c.channel.QueueDeclare("", false, true, false, false, nil); err != nil {
		log.Fatalf("queue.declare: %s", err)
	}

	if err := c.channel.QueueBind("", "", "routing-control", false, nil); err != nil {
		log.Fatalf("queue.bind: %s", err)
	}

	authStream, err := c.channel.Consume("", "", true, false, false, false, nil)
	if err != nil {
		log.Fatalf("basic.consume: %s", err)
	}

	log.Println("routing started...")
	for msg := range authStream {
		log.Printf("got %dB message data: [%v]-[%s] %s",
			len(msg.Body),
			msg.DeliveryTag,
			msg.RoutingKey,
			msg.Body)

		switch msg.RoutingKey {
		case "auth.join":
			var join JoinMsg
			err := json.Unmarshal(msg.Body, &join)
			if err != nil {
				log.Print("bad json incoming msg: ", err)
			}

			join.ConsumerTag = generateUniqueConsumerTag(join.BindingKey)
			authPairs[join.RoutingKey] = join

			log.Println("Auth pairs:", authPairs) // this is just for debug

			declareExchange(c, join.Exchange)

			go consumeAndRepublish(c, join.Exchange, join.BindingKey, join.RoutingKey, join.Suffix, join.ConsumerTag, make(chan error))
		case "auth.leave":
			var leave LeaveMsg
			err := json.Unmarshal(msg.Body, &leave)
			if err != nil {
				log.Print("bad json incoming msg: ", err)
			}

			// cancel consuming
			err = c.channel.Cancel(authPairs[leave.RoutingKey].ConsumerTag, false)
			if err != nil {
				log.Fatalf("basic.cancel: %s", err)
			}
			decrementExchangeCounter(leave)

		default:
			log.Println("routing key is not defined: ", msg.RoutingKey)
		}
	}
}

func generateUniqueConsumerTag(bindingKey string) string {
	r := make([]byte, 32/8)
	rand.Read(r)
	return bindingKey + "." + base64.StdEncoding.EncodeToString(r)
}

func generateUniqueQueueName() string {
	r := make([]byte, 32/8)
	rand.Read(r)
	return base64.StdEncoding.EncodeToString(r)
}

func declareExchange(c *Consumer, exchange string) {
	if exchanges[exchange] <= 0 {
		if err := c.channel.ExchangeDeclare(exchange, "topic", false, true, false, false, nil); err != nil {
			log.Fatalf("exchange.declare: %s", err)
		}
		exchanges[exchange] = 0
	}
	exchanges[exchange]++
}

func decrementExchangeCounter(leave LeaveMsg) {
	exchange := authPairs[leave.RoutingKey].Exchange
	// decrement exchange counter
	exchanges[exchange]--
	// delete authPairs map
	delete(authPairs, leave.RoutingKey)
}

func consumeAndRepublish(c *Consumer, exchange, bindingKey, routingKey, suffix string, consumerTag string, done chan error) {
	log.Printf("Consume from:\n exchange %s\n bindingKey %s\n routingKey %s\n consumerTag %s\n",
		exchange, bindingKey, routingKey, consumerTag)

	if len(suffix) > 0 {
		routingKey += suffix
	}

	channel, err := c.conn.Channel()
	if err != nil {
		done <- err
		return
	}
	defer channel.Close()

	uniqueQueueName := generateUniqueQueueName()

	if _, err := channel.QueueDeclare(uniqueQueueName, false, true, true, false, nil); err != nil {
		log.Fatalf("queue.declare: %s", err)
	}

	if err := channel.QueueBind(uniqueQueueName, bindingKey, exchange, false, nil); err != nil {
		log.Fatalf("queue.bind: %s", err)
	}

	messages, err := channel.Consume(uniqueQueueName, consumerTag, true, false, false, false, nil)
	if err != nil {
		log.Fatalf("basic.consume: %s", err)
	}

	for msg := range messages {
		log.Printf("messages stream got %dB message data: [%v] %s",
			len(msg.Body),
			msg.DeliveryTag,
			msg.Body)

		publishToBroker(msg.Body, routingKey)
	}

}

func publishToBroker(data []byte, routingKey string) {
	msg := amqp.Publishing{
		Headers:         amqp.Table{},
		ContentType:     "text/plain",
		ContentEncoding: "",
		Body:            data,
		DeliveryMode:    1, // 1=non-persistent, 2=persistent
		Priority:        0, // 0-9
	}

	log.Println("publishing data ", string(data), routingKey)
	err := producer.channel.Publish("broker", routingKey, false, false, msg)
	if err != nil {
		log.Printf("error while publishing proxy message: %s", err)
	}

}

func createProducer() (*Producer, error) {
	p := &Producer{
		conn:    nil,
		channel: nil,
	}

	log.Printf("creating publisher connections")

	p.conn = amqputil.CreateConnection("routing")
	p.channel = amqputil.CreateChannel(p.conn)

	return p, nil
}
<|endoftext|>


<commit_before>// Copyright (c) 2018 Cisco and/or its affiliates.
//
// 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 service

import (
	"context"
	"sync"

	"github.com/ligato/cn-infra/datasync"
	"github.com/ligato/cn-infra/datasync/resync"
	"github.com/ligato/cn-infra/flavors/local"
	"github.com/ligato/cn-infra/logging"
	"github.com/ligato/cn-infra/utils/safeclose"

	"github.com/ligato/vpp-agent/plugins/defaultplugins"
	"github.com/ligato/vpp-agent/plugins/govppmux"

	"github.com/contiv/vpp/plugins/contiv"
	epmodel "github.com/contiv/vpp/plugins/ksr/model/endpoints"
	podmodel "github.com/contiv/vpp/plugins/ksr/model/pod"
	svcmodel "github.com/contiv/vpp/plugins/ksr/model/service"

	"github.com/contiv/vpp/plugins/service/configurator"
	"github.com/contiv/vpp/plugins/service/processor"
)

// Plugin watches configuration of K8s resources (as reflected by KSR into ETCD)
// for changes in services, endpoints and pods and updates the NAT configuration
// in the VPP accordingly.
type Plugin struct {
	Deps

	resyncChan chan datasync.ResyncEvent
	changeChan chan datasync.ChangeEvent

	watchConfigReg datasync.WatchRegistration

	resyncLock sync.Mutex
	ctx        context.Context
	cancel     context.CancelFunc
	wg         sync.WaitGroup

	// delay resync until the contiv plugin has been re-synchronized.
	pendingResync  datasync.ResyncEvent
	pendingChanges []datasync.ChangeEvent

	processor    *processor.ServiceProcessor
	configurator *configurator.ServiceConfigurator
}

// Deps defines dependencies of the service plugin.
type Deps struct {
	local.PluginInfraDeps
	Resync  resync.Subscriber
	Watcher datasync.KeyValProtoWatcher /* prefixed for KSR-published K8s state data */
	Contiv  contiv.API                  /* to get the Node IP and all interface names */

	/* until supported in vpp-agent, we call NAT binary APIs directly */
	VPP   defaultplugins.API /* interface indexes */
	GoVPP govppmux.API       /* NAT binary APIs*/
}

// Init initializes the service plugin and starts watching ETCD for K8s configuration.
func (p *Plugin) Init() error {
	var err error
	p.Log.SetLevel(logging.DebugLevel)

	p.resyncChan = make(chan datasync.ResyncEvent)
	p.changeChan = make(chan datasync.ChangeEvent)

	goVppCh, err := p.GoVPP.NewAPIChannel()
	if err != nil {
		return err
	}

	p.configurator = &configurator.ServiceConfigurator{
		Deps: configurator.Deps{
			Log:       p.Log.NewLogger("-serviceConfigurator"),
			Contiv:    p.Contiv,
			VPP:       p.VPP,
			GoVPPChan: goVppCh,
		},
	}
	p.configurator.Log.SetLevel(logging.DebugLevel)

	p.processor = &processor.ServiceProcessor{
		Deps: processor.Deps{
			Log:          p.Log.NewLogger("-serviceProcessor"),
			Contiv:       p.Contiv,
			Configurator: p.configurator,
		},
	}
	p.processor.Log.SetLevel(logging.DebugLevel)

	p.configurator.Init()
	p.processor.Init()

	p.ctx, p.cancel = context.WithCancel(context.Background())

	go p.watchEvents()
	err = p.subscribeWatcher()
	if err != nil {
		return err
	}

	return nil
}

// AfterInit registers to the ResyncOrchestrator. The registration is done in this phase
// in order to ensure that the resync for this plugin is triggered only after
// resync of the Contiv plugin has finished.
func (p *Plugin) AfterInit() error {
	if p.Resync != nil {
		reg := p.Resync.Register(string(p.PluginName))
		go p.handleResync(reg.StatusChan())
	}
	return nil
}

func (p *Plugin) subscribeWatcher() (err error) {
	p.watchConfigReg, err = p.Watcher.
		Watch("K8s endpoints & services & pods", p.changeChan, p.resyncChan,
			epmodel.KeyPrefix(), svcmodel.KeyPrefix(), podmodel.KeyPrefix())
	return err
}

func (p *Plugin) watchEvents() {
	p.wg.Add(1)
	defer p.wg.Done()

	for {
		select {
		case resyncConfigEv := <-p.resyncChan:
			p.resyncLock.Lock()
			p.pendingResync = resyncConfigEv
			p.pendingChanges = []datasync.ChangeEvent{}
			resyncConfigEv.Done(nil)
			p.Log.WithField("config", resyncConfigEv).Info("Delaying RESYNC config")
			p.resyncLock.Unlock()

		case dataChngEv := <-p.changeChan:
			p.resyncLock.Lock()
			if p.pendingResync != nil {
				p.pendingChanges = append(p.pendingChanges, dataChngEv)
				dataChngEv.Done(nil)
				p.Log.WithField("config", dataChngEv).Info("Delaying data-change")
			} else {
				err := p.processor.Update(dataChngEv)
				dataChngEv.Done(err)
			}
			p.resyncLock.Unlock()

		case <-p.ctx.Done():
			p.Log.Debug("Stop watching events")
			return
		}
	}
}

func (p *Plugin) handleResync(resyncChan chan resync.StatusEvent) {
	for {
		select {
		case ev := <-resyncChan:
			var err error
			status := ev.ResyncStatus()
			if status == resync.Started {
				p.resyncLock.Lock()
				if p.pendingResync != nil {
					p.Log.WithField("config", p.pendingResync).Info("Applying delayed RESYNC config")
					err = p.processor.Resync(p.pendingResync)
					for i := 0; err == nil && i < len(p.pendingChanges); i++ {
						dataChngEv := p.pendingChanges[i]
						p.Log.WithField("config", dataChngEv).Info("Applying delayed data-change")
						err = p.processor.Update(dataChngEv)
					}
					p.pendingResync = nil
					p.pendingChanges = []datasync.ChangeEvent{}
				}
				p.resyncLock.Unlock()
			}
			if err != nil {
				p.Log.Error(err)
			}
			ev.Ack()
		case <-p.ctx.Done():
			return
		}
	}
}

// Close stops watching of KSR reflected data.
func (p *Plugin) Close() error {
	p.cancel()
	p.wg.Wait()
	safeclose.CloseAll(p.watchConfigReg, p.resyncChan, p.changeChan)
	return nil
}
<commit_msg>temporary logs<commit_after>// Copyright (c) 2018 Cisco and/or its affiliates.
//
// 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 service

import (
	"context"
	"sync"

	"github.com/ligato/cn-infra/datasync"
	"github.com/ligato/cn-infra/datasync/resync"
	"github.com/ligato/cn-infra/flavors/local"
	"github.com/ligato/cn-infra/logging"
	"github.com/ligato/cn-infra/utils/safeclose"

	"github.com/ligato/vpp-agent/plugins/defaultplugins"
	"github.com/ligato/vpp-agent/plugins/govppmux"

	"github.com/contiv/vpp/plugins/contiv"
	epmodel "github.com/contiv/vpp/plugins/ksr/model/endpoints"
	podmodel "github.com/contiv/vpp/plugins/ksr/model/pod"
	svcmodel "github.com/contiv/vpp/plugins/ksr/model/service"

	"github.com/contiv/vpp/plugins/service/configurator"
	"github.com/contiv/vpp/plugins/service/processor"
)

// Plugin watches configuration of K8s resources (as reflected by KSR into ETCD)
// for changes in services, endpoints and pods and updates the NAT configuration
// in the VPP accordingly.
type Plugin struct {
	Deps

	resyncChan chan datasync.ResyncEvent
	changeChan chan datasync.ChangeEvent

	watchConfigReg datasync.WatchRegistration

	resyncLock sync.Mutex
	ctx        context.Context
	cancel     context.CancelFunc
	wg         sync.WaitGroup

	// delay resync until the contiv plugin has been re-synchronized.
	pendingResync  datasync.ResyncEvent
	pendingChanges []datasync.ChangeEvent

	processor    *processor.ServiceProcessor
	configurator *configurator.ServiceConfigurator
}

// Deps defines dependencies of the service plugin.
type Deps struct {
	local.PluginInfraDeps
	Resync  resync.Subscriber
	Watcher datasync.KeyValProtoWatcher /* prefixed for KSR-published K8s state data */
	Contiv  contiv.API                  /* to get the Node IP and all interface names */

	/* until supported in vpp-agent, we call NAT binary APIs directly */
	VPP   defaultplugins.API /* interface indexes */
	GoVPP govppmux.API       /* NAT binary APIs*/
}

// Init initializes the service plugin and starts watching ETCD for K8s configuration.
func (p *Plugin) Init() error {
	var err error
	p.Log.SetLevel(logging.DebugLevel)

	p.resyncChan = make(chan datasync.ResyncEvent)
	p.changeChan = make(chan datasync.ChangeEvent)

	goVppCh, err := p.GoVPP.NewAPIChannel()
	if err != nil {
		return err
	}

	p.configurator = &configurator.ServiceConfigurator{
		Deps: configurator.Deps{
			Log:       p.Log.NewLogger("-serviceConfigurator"),
			Contiv:    p.Contiv,
			VPP:       p.VPP,
			GoVPPChan: goVppCh,
		},
	}
	p.configurator.Log.SetLevel(logging.DebugLevel)

	p.processor = &processor.ServiceProcessor{
		Deps: processor.Deps{
			Log:          p.Log.NewLogger("-serviceProcessor"),
			Contiv:       p.Contiv,
			Configurator: p.configurator,
		},
	}
	p.processor.Log.SetLevel(logging.DebugLevel)

	p.configurator.Init()
	p.processor.Init()

	p.ctx, p.cancel = context.WithCancel(context.Background())

	go p.watchEvents()
	err = p.subscribeWatcher()
	if err != nil {
		return err
	}

	return nil
}

// AfterInit registers to the ResyncOrchestrator. The registration is done in this phase
// in order to ensure that the resync for this plugin is triggered only after
// resync of the Contiv plugin has finished.
func (p *Plugin) AfterInit() error {
	if p.Resync != nil {
		reg := p.Resync.Register(string(p.PluginName))
		go p.handleResync(reg.StatusChan())
	}
	return nil
}

func (p *Plugin) subscribeWatcher() (err error) {
	p.watchConfigReg, err = p.Watcher.
		Watch("K8s endpoints & services & pods", p.changeChan, p.resyncChan,
			epmodel.KeyPrefix(), svcmodel.KeyPrefix(), podmodel.KeyPrefix())
	return err
}

func (p *Plugin) watchEvents() {
	p.wg.Add(1)
	defer p.wg.Done()

	for {
		select {
		case resyncConfigEv := <-p.resyncChan:
			p.Log.WithField("config", resyncConfigEv).Info("Data RESYNC")
			p.resyncLock.Lock()
			p.pendingResync = resyncConfigEv
			p.pendingChanges = []datasync.ChangeEvent{}
			resyncConfigEv.Done(nil)
			p.Log.WithField("config", resyncConfigEv).Info("Delaying RESYNC config")
			p.resyncLock.Unlock()

		case dataChngEv := <-p.changeChan:
			p.Log.WithField("config", dataChngEv).Info("Data CHANGE")
			p.resyncLock.Lock()
			if p.pendingResync != nil {
				p.pendingChanges = append(p.pendingChanges, dataChngEv)
				dataChngEv.Done(nil)
				p.Log.WithField("config", dataChngEv).Info("Delaying data-change")
			} else {
				err := p.processor.Update(dataChngEv)
				dataChngEv.Done(err)
			}
			p.resyncLock.Unlock()

		case <-p.ctx.Done():
			p.Log.Debug("Stop watching events")
			return
		}
	}
}

func (p *Plugin) handleResync(resyncChan chan resync.StatusEvent) {
	for {
		select {
		case ev := <-resyncChan:
			var err error
			status := ev.ResyncStatus()
			if status == resync.Started {
				p.resyncLock.Lock()
				if p.pendingResync != nil {
					p.Log.WithField("config", p.pendingResync).Info("Applying delayed RESYNC config")
					err = p.processor.Resync(p.pendingResync)
					for i := 0; err == nil && i < len(p.pendingChanges); i++ {
						dataChngEv := p.pendingChanges[i]
						p.Log.WithField("config", dataChngEv).Info("Applying delayed data-change")
						err = p.processor.Update(dataChngEv)
					}
					p.pendingResync = nil
					p.pendingChanges = []datasync.ChangeEvent{}
				}
				p.resyncLock.Unlock()
			}
			if err != nil {
				p.Log.Error(err)
			}
			ev.Ack()
		case <-p.ctx.Done():
			return
		}
	}
}

// Close stops watching of KSR reflected data.
func (p *Plugin) Close() error {
	p.cancel()
	p.wg.Wait()
	safeclose.CloseAll(p.watchConfigReg, p.resyncChan, p.changeChan)
	return nil
}
<|endoftext|>