<commit_before>// build_and_deploy_cipd performs a Bazel build of the given targets and uploads
// a CIPD package including the given build products.
package main

import (
	"context"
	"flag"
	"fmt"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	cipd_pkg "go.chromium.org/luci/cipd/client/cipd/pkg"
	cipd_common "go.chromium.org/luci/cipd/common"
	"golang.org/x/oauth2"

	"go.skia.org/infra/go/auth"
	"go.skia.org/infra/go/cipd"
	"go.skia.org/infra/go/common"
	"go.skia.org/infra/go/httputils"
	"go.skia.org/infra/go/skerr"
	"go.skia.org/infra/task_driver/go/lib/auth_steps"
	"go.skia.org/infra/task_driver/go/lib/bazel"
	"go.skia.org/infra/task_driver/go/lib/os_steps"
	"go.skia.org/infra/task_driver/go/td"
)

var (
	// Required properties for this task.
	projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
	taskId    = flag.String("task_id", "", "ID of this task.")
	taskName  = flag.String("task_name", "", "Name of the task.")

	pkgName       = flag.String("package_name", "", "Name of the CIPD package.")
	targets       = common.NewMultiStringFlag("target", nil, "Bazel build targets.")
	platformsList = common.NewMultiStringFlag("platform", nil, "Pairs of Bazel build platform and CIPD platform in <bazel platform>=<cipd platform> format.")
	includePaths  = common.NewMultiStringFlag("include_path", nil, "Paths to include, relative to //_bazel_bin.  Use [.exe] for optional suffix, eg. \"program[.exe]\"")

	// Optional flags.
	buildDir       = flag.String("build_dir", ".", "Directory containing the Bazel workspace to build.")
	cipdServiceURL = flag.String("cipd_service_url", cipd.DefaultServiceURL, "CIPD service URL.")
	tags           = common.NewMultiStringFlag("tag", nil, "Tags to apply to the package, in key:value format.")
	refs           = common.NewMultiStringFlag("ref", nil, "Refs to apply to the package.")
	metadata       = common.NewMultiStringFlag("metadata", nil, "Metadata to apply to the package, in key:value format.")
	rbe            = flag.Bool("rbe", false, "Whether to run Bazel on RBE or locally.")
	rbeKey         = flag.String("rbe_key", "", "Path to the service account key to use for RBE.")
	local          = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
	output         = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)

var (
	// executableSuffixRegex is used to parse an --include_path which uses the
	// path[.extension] format.
	executableSuffixRegex = regexp.MustCompile(`(.+)\[(.+)\]`)
)

func main() {
	// Setup.
	ctx := td.StartRun(projectId, taskId, taskName, output, local)
	defer td.EndRun(ctx)

	if *pkgName == "" {
		td.Fatalf(ctx, "--package_name is required.")
	}
	if len(*includePaths) == 0 {
		td.Fatalf(ctx, "At least one --include_path is required.")
	}
	if len(*targets) == 0 {
		td.Fatalf(ctx, "At least one --target is required.")
	}
	if len(*platformsList) == 0 {
		td.Fatalf(ctx, "At least one --platform is required.")
	}
	for _, tag := range *tags {
		splitPair(ctx, tag, ":")
	}
	metadataMap := make(map[string]string, len(*metadata))
	for _, md := range *metadata {
		k, v := splitPair(ctx, md, ":")
		metadataMap[k] = v
	}

	// Create directories for each of the build platforms.
	pkgs := make([]*pkgSpec, 0, len(*platformsList))
	var ts oauth2.TokenSource
	if err := td.Do(ctx, td.Props("Setup").Infra(), func(ctx context.Context) error {
		var err error
		ts, err = auth_steps.Init(ctx, *local, auth.SCOPE_USERINFO_EMAIL)
		if err != nil {
			return err
		}
		for _, platform := range *platformsList {
			bzlPlatform, cipdPlatform := splitPair(ctx, platform, "=")
			tmpDir, err := os_steps.TempDir(ctx, "", cipdPlatform)
			if err != nil {
				return err
			}
			pkgs = append(pkgs, &pkgSpec{
				bazelPlatform: bzlPlatform,
				cipdPlatform:  cipdPlatform,
				cipdPkgPath:   path.Join(*pkgName, cipdPlatform),
				tmpDir:        tmpDir,
			})
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}
	defer func() {
		if err := td.Do(ctx, td.Props("Cleanup").Infra(), func(ctx context.Context) error {
			var rvErr error
			for _, pkg := range pkgs {
				tmpDir := pkg.tmpDir
				if err := os_steps.RemoveAll(ctx, tmpDir); err != nil {
					rvErr = err
				}
			}
			return rvErr
		}); err != nil {
			td.Fatal(ctx, err)
		}
	}()

	// Perform the build(s).
	if err := td.Do(ctx, td.Props("Build"), func(ctx context.Context) (rvErr error) {
		bzl, cleanup, err := bazel.New(ctx, *buildDir, *local, *rbeKey)
		if err != nil {
			return err
		}
		defer cleanup()

		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props("Build "+pkg.cipdPlatform), func(ctx context.Context) error {
				// We're building for multiple platforms, and Bazel writes all
				// of the build products into the same directory regardless of
				// platform, so there's a potential for accidental inclusion of
				// incompatible binaries in the CIPD package, eg. "app.exe" vs
				// "app". "bazel clean" prevents that by emptying the output
				// directory between builds.
				if _, err := bzl.Do(ctx, "clean"); err != nil {
					return err
				}

				// Perform the build.
				args := []string{fmt.Sprintf("--platforms=%s", pkg.bazelPlatform)}
				args = append(args, *targets...)
				doFunc := bzl.Do
				if *rbe {
					doFunc = bzl.DoOnRBE
				}
				if _, err := doFunc(ctx, "build", args...); err != nil {
					return err
				}

				// Copy the outputs to the destination dir.
				for _, path := range *includePaths {
					paths := []string{path}
					m := executableSuffixRegex.FindAllStringSubmatch(path, -1)
					if m != nil {
						paths = []string{m[0][1], m[0][1] + m[0][2]}
					}
					found := false
					for _, path := range paths {
						path := filepath.Join(*buildDir, path)
						if _, err := os_steps.Stat(ctx, path); err == nil {
							dest := filepath.Join(pkg.tmpDir, filepath.Base(path))
							if err := os_steps.CopyFile(ctx, path, dest); err != nil {
								return err
							}
							found = true
							break
						}
					}
					if !found {
						return fmt.Errorf("Unable to find %q; tried %v", path, paths)
					}
				}
				return nil
			}); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}

	// Upload the package(s) to CIPD.
	// TODO(borenet): See if we can use the CIPD Go code directly, rather than
	// having to ship a separate binary.
	if err := td.Do(ctx, td.Props("Upload to CIPD"), func(ctx context.Context) error {
		httpClient := httputils.DefaultClientConfig().WithTokenSource(ts).Client()
		cipdClient, err := cipd.NewClient(httpClient, ".", *cipdServiceURL)
		if err != nil {
			return err
		}

		// Upload all of the package instances.
		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props(fmt.Sprintf("Upload %s", pkg.cipdPlatform)), func(ctx context.Context) error {
				pin, err := cipdClient.Create(ctx, pkg.cipdPkgPath, pkg.tmpDir, cipd_pkg.InstallModeCopy, nil, nil, nil, nil)
				if err != nil {
					return err
				}
				pkg.pin = pin
				return nil
			}); err != nil {
				return err
			}
		}
		// Apply refs, tags, and metadata. Do this after all platforms have been
		// built and uploaded to increase the likelihood that the refs and tags
		// get applied to all packages or none. Otherwise it's possible for some
		// platforms to be missing when querying by ref or tag.
		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props(fmt.Sprintf("Attach %s", pkg.cipdPlatform)), func(ctx context.Context) error {
				// If any of the provided tags is already attached to a
				// different instance, stop and return an error.
				for _, tag := range *tags {
					found, err := cipdClient.SearchInstances(ctx, pkg.cipdPkgPath, []string{tag})
					if err != nil {
						return err
					}
					if len(found) == 1 && found[0].InstanceID != pkg.pin.InstanceID {
						return skerr.Fmt("Found existing instance %s of package %s with tag %s", found[0].InstanceID, pkg.cipdPkgPath, tag)
					}
					if len(found) > 1 {
						return skerr.Fmt("Found more than one instance of package %s with tag %s. This may result in failure to retrieve the package by tag due to ambiguity. Please contact the current infra gardener to investigate. To detach tags, see https://g3doc.corp.google.com/company/teams/chrome/ops/luci/cipd.md#detachtags", pkg.cipdPkgPath, tag)
					}
				}
				return cipdClient.Attach(ctx, pkg.pin, *refs, *tags, metadataMap)
			}); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}
}

// splitPair splits a key and value from a command line flag and Fatals if it
// does not follow the expected format.
func splitPair(ctx context.Context, elem, sep string) (string, string) {
	split := strings.SplitN(elem, sep, 2)
	if len(split) != 2 {
		td.Fatalf(ctx, "Expected <key>%s<value> format for %q", sep, elem)
	}
	return split[0], split[1]
}

// pkgSpec contains information about how to build and upload an indivdual CIPD
// package instance.
type pkgSpec struct {
	bazelPlatform string
	cipdPlatform  string
	cipdPkgPath   string
	tmpDir        string
	pin           cipd_common.Pin
}
<commit_msg>[cipd] Fix link for detachtags doc<commit_after>// build_and_deploy_cipd performs a Bazel build of the given targets and uploads
// a CIPD package including the given build products.
package main

import (
	"context"
	"flag"
	"fmt"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	cipd_pkg "go.chromium.org/luci/cipd/client/cipd/pkg"
	cipd_common "go.chromium.org/luci/cipd/common"
	"golang.org/x/oauth2"

	"go.skia.org/infra/go/auth"
	"go.skia.org/infra/go/cipd"
	"go.skia.org/infra/go/common"
	"go.skia.org/infra/go/httputils"
	"go.skia.org/infra/go/skerr"
	"go.skia.org/infra/task_driver/go/lib/auth_steps"
	"go.skia.org/infra/task_driver/go/lib/bazel"
	"go.skia.org/infra/task_driver/go/lib/os_steps"
	"go.skia.org/infra/task_driver/go/td"
)

var (
	// Required properties for this task.
	projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
	taskId    = flag.String("task_id", "", "ID of this task.")
	taskName  = flag.String("task_name", "", "Name of the task.")

	pkgName       = flag.String("package_name", "", "Name of the CIPD package.")
	targets       = common.NewMultiStringFlag("target", nil, "Bazel build targets.")
	platformsList = common.NewMultiStringFlag("platform", nil, "Pairs of Bazel build platform and CIPD platform in <bazel platform>=<cipd platform> format.")
	includePaths  = common.NewMultiStringFlag("include_path", nil, "Paths to include, relative to //_bazel_bin.  Use [.exe] for optional suffix, eg. \"program[.exe]\"")

	// Optional flags.
	buildDir       = flag.String("build_dir", ".", "Directory containing the Bazel workspace to build.")
	cipdServiceURL = flag.String("cipd_service_url", cipd.DefaultServiceURL, "CIPD service URL.")
	tags           = common.NewMultiStringFlag("tag", nil, "Tags to apply to the package, in key:value format.")
	refs           = common.NewMultiStringFlag("ref", nil, "Refs to apply to the package.")
	metadata       = common.NewMultiStringFlag("metadata", nil, "Metadata to apply to the package, in key:value format.")
	rbe            = flag.Bool("rbe", false, "Whether to run Bazel on RBE or locally.")
	rbeKey         = flag.String("rbe_key", "", "Path to the service account key to use for RBE.")
	local          = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
	output         = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)

var (
	// executableSuffixRegex is used to parse an --include_path which uses the
	// path[.extension] format.
	executableSuffixRegex = regexp.MustCompile(`(.+)\[(.+)\]`)
)

func main() {
	// Setup.
	ctx := td.StartRun(projectId, taskId, taskName, output, local)
	defer td.EndRun(ctx)

	if *pkgName == "" {
		td.Fatalf(ctx, "--package_name is required.")
	}
	if len(*includePaths) == 0 {
		td.Fatalf(ctx, "At least one --include_path is required.")
	}
	if len(*targets) == 0 {
		td.Fatalf(ctx, "At least one --target is required.")
	}
	if len(*platformsList) == 0 {
		td.Fatalf(ctx, "At least one --platform is required.")
	}
	for _, tag := range *tags {
		splitPair(ctx, tag, ":")
	}
	metadataMap := make(map[string]string, len(*metadata))
	for _, md := range *metadata {
		k, v := splitPair(ctx, md, ":")
		metadataMap[k] = v
	}

	// Create directories for each of the build platforms.
	pkgs := make([]*pkgSpec, 0, len(*platformsList))
	var ts oauth2.TokenSource
	if err := td.Do(ctx, td.Props("Setup").Infra(), func(ctx context.Context) error {
		var err error
		ts, err = auth_steps.Init(ctx, *local, auth.SCOPE_USERINFO_EMAIL)
		if err != nil {
			return err
		}
		for _, platform := range *platformsList {
			bzlPlatform, cipdPlatform := splitPair(ctx, platform, "=")
			tmpDir, err := os_steps.TempDir(ctx, "", cipdPlatform)
			if err != nil {
				return err
			}
			pkgs = append(pkgs, &pkgSpec{
				bazelPlatform: bzlPlatform,
				cipdPlatform:  cipdPlatform,
				cipdPkgPath:   path.Join(*pkgName, cipdPlatform),
				tmpDir:        tmpDir,
			})
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}
	defer func() {
		if err := td.Do(ctx, td.Props("Cleanup").Infra(), func(ctx context.Context) error {
			var rvErr error
			for _, pkg := range pkgs {
				tmpDir := pkg.tmpDir
				if err := os_steps.RemoveAll(ctx, tmpDir); err != nil {
					rvErr = err
				}
			}
			return rvErr
		}); err != nil {
			td.Fatal(ctx, err)
		}
	}()

	// Perform the build(s).
	if err := td.Do(ctx, td.Props("Build"), func(ctx context.Context) (rvErr error) {
		bzl, cleanup, err := bazel.New(ctx, *buildDir, *local, *rbeKey)
		if err != nil {
			return err
		}
		defer cleanup()

		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props("Build "+pkg.cipdPlatform), func(ctx context.Context) error {
				// We're building for multiple platforms, and Bazel writes all
				// of the build products into the same directory regardless of
				// platform, so there's a potential for accidental inclusion of
				// incompatible binaries in the CIPD package, eg. "app.exe" vs
				// "app". "bazel clean" prevents that by emptying the output
				// directory between builds.
				if _, err := bzl.Do(ctx, "clean"); err != nil {
					return err
				}

				// Perform the build.
				args := []string{fmt.Sprintf("--platforms=%s", pkg.bazelPlatform)}
				args = append(args, *targets...)
				doFunc := bzl.Do
				if *rbe {
					doFunc = bzl.DoOnRBE
				}
				if _, err := doFunc(ctx, "build", args...); err != nil {
					return err
				}

				// Copy the outputs to the destination dir.
				for _, path := range *includePaths {
					paths := []string{path}
					m := executableSuffixRegex.FindAllStringSubmatch(path, -1)
					if m != nil {
						paths = []string{m[0][1], m[0][1] + m[0][2]}
					}
					found := false
					for _, path := range paths {
						path := filepath.Join(*buildDir, path)
						if _, err := os_steps.Stat(ctx, path); err == nil {
							dest := filepath.Join(pkg.tmpDir, filepath.Base(path))
							if err := os_steps.CopyFile(ctx, path, dest); err != nil {
								return err
							}
							found = true
							break
						}
					}
					if !found {
						return fmt.Errorf("Unable to find %q; tried %v", path, paths)
					}
				}
				return nil
			}); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}

	// Upload the package(s) to CIPD.
	// TODO(borenet): See if we can use the CIPD Go code directly, rather than
	// having to ship a separate binary.
	if err := td.Do(ctx, td.Props("Upload to CIPD"), func(ctx context.Context) error {
		httpClient := httputils.DefaultClientConfig().WithTokenSource(ts).Client()
		cipdClient, err := cipd.NewClient(httpClient, ".", *cipdServiceURL)
		if err != nil {
			return err
		}

		// Upload all of the package instances.
		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props(fmt.Sprintf("Upload %s", pkg.cipdPlatform)), func(ctx context.Context) error {
				pin, err := cipdClient.Create(ctx, pkg.cipdPkgPath, pkg.tmpDir, cipd_pkg.InstallModeCopy, nil, nil, nil, nil)
				if err != nil {
					return err
				}
				pkg.pin = pin
				return nil
			}); err != nil {
				return err
			}
		}
		// Apply refs, tags, and metadata. Do this after all platforms have been
		// built and uploaded to increase the likelihood that the refs and tags
		// get applied to all packages or none. Otherwise it's possible for some
		// platforms to be missing when querying by ref or tag.
		for _, pkg := range pkgs {
			if err := td.Do(ctx, td.Props(fmt.Sprintf("Attach %s", pkg.cipdPlatform)), func(ctx context.Context) error {
				// If any of the provided tags is already attached to a
				// different instance, stop and return an error.
				for _, tag := range *tags {
					found, err := cipdClient.SearchInstances(ctx, pkg.cipdPkgPath, []string{tag})
					if err != nil {
						return err
					}
					if len(found) == 1 && found[0].InstanceID != pkg.pin.InstanceID {
						return skerr.Fmt("Found existing instance %s of package %s with tag %s", found[0].InstanceID, pkg.cipdPkgPath, tag)
					}
					if len(found) > 1 {
						return skerr.Fmt("Found more than one instance of package %s with tag %s. This may result in failure to retrieve the package by tag due to ambiguity. Please contact the current infra gardener to investigate. To detach tags, see http://go/luci-cipd#detachtags", pkg.cipdPkgPath, tag)
					}
				}
				return cipdClient.Attach(ctx, pkg.pin, *refs, *tags, metadataMap)
			}); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		td.Fatal(ctx, err)
	}
}

// splitPair splits a key and value from a command line flag and Fatals if it
// does not follow the expected format.
func splitPair(ctx context.Context, elem, sep string) (string, string) {
	split := strings.SplitN(elem, sep, 2)
	if len(split) != 2 {
		td.Fatalf(ctx, "Expected <key>%s<value> format for %q", sep, elem)
	}
	return split[0], split[1]
}

// pkgSpec contains information about how to build and upload an indivdual CIPD
// package instance.
type pkgSpec struct {
	bazelPlatform string
	cipdPlatform  string
	cipdPkgPath   string
	tmpDir        string
	pin           cipd_common.Pin
}
<|endoftext|>


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

package upstart

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"regexp"
	"text/template"
	"time"

	"launchpad.net/juju-core/utils"
)

var startedRE = regexp.MustCompile(`^.* start/running, process (\d+)\n$`)

// InitDir holds the default init directory name.
var InitDir = "/etc/init"

var InstallStartRetryAttempts = utils.AttemptStrategy{
	Total: 1 * time.Second,
	Delay: 250 * time.Millisecond,
}

// Service provides visibility into and control over an upstart service.
type Service struct {
	Name    string
	InitDir string // defaults to "/etc/init"
}

func NewService(name string) *Service {
	return &Service{Name: name, InitDir: InitDir}
}

// confPath returns the path to the service's configuration file.
func (s *Service) confPath() string {
	return path.Join(s.InitDir, s.Name+".conf")
}

// Installed returns whether the service configuration exists in the
// init directory.
func (s *Service) Installed() bool {
	_, err := os.Stat(s.confPath())
	return err == nil
}

// Running returns true if the Service appears to be running.
func (s *Service) Running() bool {
	cmd := exec.Command("status", "--system", s.Name)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return false
	}
	return startedRE.Match(out)
}

// Start starts the service.
func (s *Service) Start() error {
	if s.Running() {
		return nil
	}
	err := runCommand("start", "--system", s.Name)
	if err != nil {
		// Double check to see if we were started before our command ran.
		if s.Running() {
			return nil
		}
	}
	return err
}

func runCommand(args ...string) error {
	out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
	if err == nil {
		return nil
	}
	out = bytes.TrimSpace(out)
	if len(out) > 0 {
		return fmt.Errorf("exec %q: %v (%s)", args, err, out)
	}
	return fmt.Errorf("exec %q: %v", args, err)
}

// Stop stops the service.
func (s *Service) Stop() error {
	if !s.Running() {
		return nil
	}
	return runCommand("stop", "--system", s.Name)
}

// StopAndRemove stops the service and then deletes the service
// configuration from the init directory.
func (s *Service) StopAndRemove() error {
	if !s.Installed() {
		return nil
	}
	if err := s.Stop(); err != nil {
		return err
	}
	return os.Remove(s.confPath())
}

// Remove deletes the service configuration from the init directory.
func (s *Service) Remove() error {
	if !s.Installed() {
		return nil
	}
	return os.Remove(s.confPath())
}

// BUG: %q quoting does not necessarily match libnih quoting rules
// (as used by upstart); this may become an issue in the future.
var confT = template.Must(template.New("").Parse(`
description "{{.Desc}}"
author "Juju Team <juju@lists.ubuntu.com>"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
normal exit 0
{{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}}
{{end}}
{{range $k, $v := .Limit}}limit {{$k}} {{$v}}
{{end}}
exec {{.Cmd}}{{if .Out}} >> {{.Out}} 2>&1{{end}}
`[1:]))

// Conf is responsible for defining and installing upstart services. Its fields
// represent elements of an upstart service configuration file.
type Conf struct {
	Service
	// Desc is the upstart service's description.
	Desc string
	// Env holds the environment variables that will be set when the command runs.
	Env map[string]string
	// Limit holds the ulimit values that will be set when the command runs.
	Limit map[string]string
	// Cmd is the command (with arguments) that will be run.
	// The command will be restarted if it exits with a non-zero exit code.
	Cmd string
	// Out, if set, will redirect output to that path.
	Out string
}

// validate returns an error if the service is not adequately defined.
func (c *Conf) validate() error {
	if c.Name == "" {
		return errors.New("missing Name")
	}
	if c.InitDir == "" {
		return errors.New("missing InitDir")
	}
	if c.Desc == "" {
		return errors.New("missing Desc")
	}
	if c.Cmd == "" {
		return errors.New("missing Cmd")
	}
	return nil
}

// Render returns the upstart configuration for the service as a slice of bytes.
func (c *Conf) render() ([]byte, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	if err := confT.Execute(&buf, c); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// Install installs and starts the service.
func (c *Conf) Install() error {
	conf, err := c.render()
	if err != nil {
		return err
	}

	exists, err := c.removeOld(conf)
	if err != nil {
		return err
	}
	if !exists {
		if err := ioutil.WriteFile(c.confPath(), conf, 0644); err != nil {
			return err
		}
	}

	// On slower disks, upstart may take a short time to realise
	// that there is a service there.
	for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); {
		if err = c.Start(); err == nil {
			break
		}
	}
	return err
}

func (c *Conf) removeOld(expected []byte) (exists bool, err error) {
	current, err := ioutil.ReadFile(c.confPath())
	if os.IsNotExist(err) {
		// no existing config
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("upstart: could not read existing service config: %v", err)
	}

	// if we have a current config on disk, check to see if it's different
	if bytes.Equal(current, expected) {
		return true, nil
	}
	if err := c.StopAndRemove(); err != nil {
		return false, fmt.Errorf("upstart: could not remove installed service: %s", err)
	}
	return false, nil
}

// InstallCommands returns shell commands to install and start the service.
func (c *Conf) InstallCommands() ([]string, error) {
	conf, err := c.render()
	if err != nil {
		return nil, err
	}
	return []string{
		fmt.Sprintf("cat >> %s << 'EOF'\n%sEOF\n", c.confPath(), conf),
		"start " + c.Name,
	}, nil
}
<commit_msg>match case of function<commit_after>// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package upstart

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"regexp"
	"text/template"
	"time"

	"launchpad.net/juju-core/utils"
)

var startedRE = regexp.MustCompile(`^.* start/running, process (\d+)\n$`)

// InitDir holds the default init directory name.
var InitDir = "/etc/init"

var InstallStartRetryAttempts = utils.AttemptStrategy{
	Total: 1 * time.Second,
	Delay: 250 * time.Millisecond,
}

// Service provides visibility into and control over an upstart service.
type Service struct {
	Name    string
	InitDir string // defaults to "/etc/init"
}

func NewService(name string) *Service {
	return &Service{Name: name, InitDir: InitDir}
}

// confPath returns the path to the service's configuration file.
func (s *Service) confPath() string {
	return path.Join(s.InitDir, s.Name+".conf")
}

// Installed returns whether the service configuration exists in the
// init directory.
func (s *Service) Installed() bool {
	_, err := os.Stat(s.confPath())
	return err == nil
}

// Running returns true if the Service appears to be running.
func (s *Service) Running() bool {
	cmd := exec.Command("status", "--system", s.Name)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return false
	}
	return startedRE.Match(out)
}

// Start starts the service.
func (s *Service) Start() error {
	if s.Running() {
		return nil
	}
	err := runCommand("start", "--system", s.Name)
	if err != nil {
		// Double check to see if we were started before our command ran.
		if s.Running() {
			return nil
		}
	}
	return err
}

func runCommand(args ...string) error {
	out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
	if err == nil {
		return nil
	}
	out = bytes.TrimSpace(out)
	if len(out) > 0 {
		return fmt.Errorf("exec %q: %v (%s)", args, err, out)
	}
	return fmt.Errorf("exec %q: %v", args, err)
}

// Stop stops the service.
func (s *Service) Stop() error {
	if !s.Running() {
		return nil
	}
	return runCommand("stop", "--system", s.Name)
}

// StopAndRemove stops the service and then deletes the service
// configuration from the init directory.
func (s *Service) StopAndRemove() error {
	if !s.Installed() {
		return nil
	}
	if err := s.Stop(); err != nil {
		return err
	}
	return os.Remove(s.confPath())
}

// Remove deletes the service configuration from the init directory.
func (s *Service) Remove() error {
	if !s.Installed() {
		return nil
	}
	return os.Remove(s.confPath())
}

// BUG: %q quoting does not necessarily match libnih quoting rules
// (as used by upstart); this may become an issue in the future.
var confT = template.Must(template.New("").Parse(`
description "{{.Desc}}"
author "Juju Team <juju@lists.ubuntu.com>"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
normal exit 0
{{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}}
{{end}}
{{range $k, $v := .Limit}}limit {{$k}} {{$v}}
{{end}}
exec {{.Cmd}}{{if .Out}} >> {{.Out}} 2>&1{{end}}
`[1:]))

// Conf is responsible for defining and installing upstart services. Its fields
// represent elements of an upstart service configuration file.
type Conf struct {
	Service
	// Desc is the upstart service's description.
	Desc string
	// Env holds the environment variables that will be set when the command runs.
	Env map[string]string
	// Limit holds the ulimit values that will be set when the command runs.
	Limit map[string]string
	// Cmd is the command (with arguments) that will be run.
	// The command will be restarted if it exits with a non-zero exit code.
	Cmd string
	// Out, if set, will redirect output to that path.
	Out string
}

// validate returns an error if the service is not adequately defined.
func (c *Conf) validate() error {
	if c.Name == "" {
		return errors.New("missing Name")
	}
	if c.InitDir == "" {
		return errors.New("missing InitDir")
	}
	if c.Desc == "" {
		return errors.New("missing Desc")
	}
	if c.Cmd == "" {
		return errors.New("missing Cmd")
	}
	return nil
}

// render returns the upstart configuration for the service as a slice of bytes.
func (c *Conf) render() ([]byte, error) {
	if err := c.validate(); err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	if err := confT.Execute(&buf, c); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// Install installs and starts the service.
func (c *Conf) Install() error {
	conf, err := c.render()
	if err != nil {
		return err
	}

	exists, err := c.removeOld(conf)
	if err != nil {
		return err
	}
	if !exists {
		if err := ioutil.WriteFile(c.confPath(), conf, 0644); err != nil {
			return err
		}
	}

	// On slower disks, upstart may take a short time to realise
	// that there is a service there.
	for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); {
		if err = c.Start(); err == nil {
			break
		}
	}
	return err
}

func (c *Conf) removeOld(expected []byte) (exists bool, err error) {
	current, err := ioutil.ReadFile(c.confPath())
	if os.IsNotExist(err) {
		// no existing config
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("upstart: could not read existing service config: %v", err)
	}

	// if we have a current config on disk, check to see if it's different
	if bytes.Equal(current, expected) {
		return true, nil
	}
	if err := c.StopAndRemove(); err != nil {
		return false, fmt.Errorf("upstart: could not remove installed service: %s", err)
	}
	return false, nil
}

// InstallCommands returns shell commands to install and start the service.
func (c *Conf) InstallCommands() ([]string, error) {
	conf, err := c.render()
	if err != nil {
		return nil, err
	}
	return []string{
		fmt.Sprintf("cat >> %s << 'EOF'\n%sEOF\n", c.confPath(), conf),
		"start " + c.Name,
	}, nil
}
<|endoftext|>


<commit_before>package oauth2

import (
	"net/url"
	"strings"
	"testing"

	"github.com/ory/fosite"
	"github.com/ory/fosite/storage"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func parseUrl(uu string) *url.URL {
	u, _ := url.Parse(uu)
	return u
}

func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) {
	for k, strategy := range map[string]CoreStrategy{
		"hmac": &hmacshaStrategy,
	} {
		t.Run("strategy="+k, func(t *testing.T) {
			store := storage.NewMemoryStore()
			h := AuthorizeExplicitGrantHandler{
				CoreStorage:           store,
				AuthorizeCodeStrategy: strategy,
				ScopeStrategy:         fosite.HierarchicScopeStrategy,
			}
			for _, c := range []struct {
				areq        *fosite.AuthorizeRequest
				description string
				expectErr   error
				expect      func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse)
			}{
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{""},
						Request:       *fosite.NewRequest(),
					},
					description: "should pass because not responsible for handling an empty response type",
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"foo"},
						Request:       *fosite.NewRequest(),
					},
					description: "should pass because not responsible for handling an invalid response type",
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"code"},
						Request: fosite.Request{
							Client: &fosite.DefaultClient{
								ResponseTypes: fosite.Arguments{"code"},
								RedirectURIs:  []string{"http://asdf.com/cb"},
							},
						},
						RedirectURI: parseUrl("http://asdf.com/cb"),
					},
					description: "should fail because redirect uri is not https",
					expectErr:   fosite.ErrInvalidRequest,
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"code"},
						Request: fosite.Request{
							Client: &fosite.DefaultClient{
								ResponseTypes: fosite.Arguments{"code"},
								RedirectURIs:  []string{"https://asdf.de/cb"},
							},
							GrantedScopes: fosite.Arguments{"a", "b"},
							Session:       &fosite.DefaultSession{},
						},
						State:       "superstate",
						RedirectURI: parseUrl("https://asdf.de/cb"),
					},
					description: "should pass",
					expect: func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) {
						code := aresp.GetQuery().Get("code")
						assert.NotEmpty(t, code)
						require.NoError(t, strategy.ValidateAuthorizeCode(nil, areq, code))

						assert.Equal(t, strings.Join(areq.GrantedScopes, " "), aresp.GetQuery().Get("scope"))
						assert.Equal(t, areq.State, aresp.GetQuery().Get("state"))
					},
				},
			} {
				t.Run("case="+c.description, func(t *testing.T) {
					aresp := fosite.NewAuthorizeResponse()
					err := h.HandleAuthorizeEndpointRequest(nil, c.areq, aresp)
					if c.expectErr != nil {
						require.EqualError(t, errors.Cause(err), c.expectErr.Error())
					} else {
						require.NoError(t, err)
					}

					if c.expect != nil {
						c.expect(t, c.areq, aresp)
					}
				})
			}
		})
	}
}
<commit_msg>handler/oauth2: set requested at date in auth code test<commit_after>package oauth2

import (
	"net/url"
	"strings"
	"testing"

	"github.com/ory/fosite"
	"github.com/ory/fosite/storage"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"time"
)

func parseUrl(uu string) *url.URL {
	u, _ := url.Parse(uu)
	return u
}

func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) {
	for k, strategy := range map[string]CoreStrategy{
		"hmac": &hmacshaStrategy,
	} {
		t.Run("strategy="+k, func(t *testing.T) {
			store := storage.NewMemoryStore()
			h := AuthorizeExplicitGrantHandler{
				CoreStorage:           store,
				AuthorizeCodeStrategy: strategy,
				ScopeStrategy:         fosite.HierarchicScopeStrategy,
			}
			for _, c := range []struct {
				areq        *fosite.AuthorizeRequest
				description string
				expectErr   error
				expect      func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse)
			}{
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{""},
						Request:       *fosite.NewRequest(),
					},
					description: "should pass because not responsible for handling an empty response type",
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"foo"},
						Request:       *fosite.NewRequest(),
					},
					description: "should pass because not responsible for handling an invalid response type",
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"code"},
						Request: fosite.Request{
							Client: &fosite.DefaultClient{
								ResponseTypes: fosite.Arguments{"code"},
								RedirectURIs:  []string{"http://asdf.com/cb"},
							},
						},
						RedirectURI: parseUrl("http://asdf.com/cb"),
					},
					description: "should fail because redirect uri is not https",
					expectErr:   fosite.ErrInvalidRequest,
				},
				{
					areq: &fosite.AuthorizeRequest{
						ResponseTypes: fosite.Arguments{"code"},
						Request: fosite.Request{
							Client: &fosite.DefaultClient{
								ResponseTypes: fosite.Arguments{"code"},
								RedirectURIs:  []string{"https://asdf.de/cb"},
							},
							GrantedScopes: fosite.Arguments{"a", "b"},
							Session:       &fosite.DefaultSession{},
							RequestedAt: time.Now(),
						},
						State:       "superstate",
						RedirectURI: parseUrl("https://asdf.de/cb"),
					},
					description: "should pass",
					expect: func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) {
						code := aresp.GetQuery().Get("code")
						assert.NotEmpty(t, code)
						require.NoError(t, strategy.ValidateAuthorizeCode(nil, areq, code))

						assert.Equal(t, strings.Join(areq.GrantedScopes, " "), aresp.GetQuery().Get("scope"))
						assert.Equal(t, areq.State, aresp.GetQuery().Get("state"))
					},
				},
			} {
				t.Run("case="+c.description, func(t *testing.T) {
					aresp := fosite.NewAuthorizeResponse()
					err := h.HandleAuthorizeEndpointRequest(nil, c.areq, aresp)
					if c.expectErr != nil {
						require.EqualError(t, errors.Cause(err), c.expectErr.Error())
					} else {
						require.NoError(t, err)
					}

					if c.expect != nil {
						c.expect(t, c.areq, aresp)
					}
				})
			}
		})
	}
}
<|endoftext|>


<commit_before>package resource

import (
	"fmt"

	"github.com/hashicorp/hcl/hcl/ast"
)

// Resource states
const (
	Present = "present"
	Absent  = "absent"
	Update  = "update"
)

// Provider is used to create new resources from an HCL AST object item
type Provider func(item *ast.ObjectItem) (Resource, error)

// Registry contains all known resource types and their providers
var registry = make(map[string]Provider)

// Register registers a resource type and it's provider
func Register(name string, p Provider) error {
	_, ok := registry[name]
	if ok {
		return fmt.Errorf("Resource '%s' is already registered", name)
	}

	registry[name] = p

	return nil
}

// Get retrieves the provider for a given resource type
func Get(name string) (Provider, bool) {
	p, ok := registry[name]

	return p, ok
}

// State type represents the current and wanted states of a resource
type State struct {
	// Current state of the resource
	Current string

	// Wanted state of the resource
	Want string
}

// Resource is the base interface type for all resources
type Resource interface {
	// ID returns the unique identifier of a resource
	ID() string

	// Returns the wanted resources
	Want() []string

	// Evaluates the resource and returns it's state
	Evaluate() (State, error)

	// Creates the resource
	Create() error

	// Deletes the resource
	Delete() error

	// Updates the resource
	Update() error
}
<commit_msg>Issue #1: Implement BaseResource type for embedding into other resources<commit_after>package resource

import (
	"fmt"

	"github.com/hashicorp/hcl/hcl/ast"
)

// Resource states
const (
	Present = "present"
	Absent  = "absent"
	Update  = "update"
)

// Provider is used to create new resources from an HCL AST object item
type Provider func(item *ast.ObjectItem) (Resource, error)

// Registry contains all known resource types and their providers
var registry = make(map[string]Provider)

// Register registers a resource type and it's provider
func Register(name string, p Provider) error {
	_, ok := registry[name]
	if ok {
		return fmt.Errorf("Resource '%s' is already registered", name)
	}

	registry[name] = p

	return nil
}

// Get retrieves the provider for a given resource type
func Get(name string) (Provider, bool) {
	p, ok := registry[name]

	return p, ok
}

// State type represents the current and wanted states of a resource
type State struct {
	// Current state of the resource
	Current string

	// Wanted state of the resource
	Want string
}

// Resource is the base interface type for all resources
type Resource interface {
	// ID returns the unique identifier of a resource
	ID() string

	// Returns the wanted resources/dependencies
	Want() []string

	// Evaluates the resource and returns it's state
	Evaluate() (State, error)

	// Creates the resource
	Create() error

	// Deletes the resource
	Delete() error

	// Updates the resource
	Update() error
}

// BaseResource partially implements the Resource interface
// It provides the common set of fields used by all resources
// The purpose of BaseResource is to be embedded into other resources
type BaseResource struct {
	// Name of the resource
	Name string `hcl:"name"`

	// State of the resource
	State string `hcl:"state"`

	// Wanted resources/dependencies
	WantResource []string `hcl:"want"`
}

// Want returns the wanted resources/dependencies
func (b *BaseResource) Want() []string {
	return b.WantResource
}
<|endoftext|>


<commit_before>package datastore

import (
	"testing"
	"os"
	"path"
	"io/ioutil"
	"github.com/photoshelf/photoshelf-storage/model"
	"github.com/stretchr/testify/assert"
	"github.com/boltdb/bolt"
)

var boltdb *BoltdbStorage
var testdata []byte

func TestMain(m *testing.M) {
	testdataPath := path.Join(os.Getenv("GOPATH"), "src/github.com/photoshelf/photoshelf-storage", "testdata")
	body, _ := os.Open(path.Join(testdataPath, "e3158990bdee63f8594c260cd51a011d"))
	testdata, _ = ioutil.ReadAll(body)

	dataPath := path.Join(os.TempDir(), "boltdb")
	boltdb, _ = NewBoltdbStorage(dataPath)

	code := m.Run()

	boltdb.db.Close()
	boltdb = nil
	os.Exit(code)
}

func TestEmptyBucket(t *testing.T) {
	boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		return nil
	})

	t.Run("same data between src and dst", func(t *testing.T) {
		photo := model.PhotoOf(*model.IdentifierOf("testdata"), testdata)
		_, err := boltdb.Save(*photo)

		if assert.NoError(t, err) {
			boltdb.db.View(func(tx *bolt.Tx) error {
				photos := tx.Bucket([]byte("photos"))
				actual := photos.Get([]byte("testdata"))

				assert.EqualValues(t, testdata, actual)
				assert.EqualValues(t, 1, photos.Stats().KeyN)

				return nil
			})
		}
	})
}

func TestExistData(t *testing.T) {
	err := boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		photos, err := tx.CreateBucketIfNotExists([]byte("photos"))
		if err != nil {
			return err
		}
		return photos.Put([]byte("testdata"), testdata)
	})
	assert.NoError(t, err, "failure testdata setting.")

	t.Run("same data between src and read", func(t *testing.T) {
		photo, err := boltdb.Read(*model.IdentifierOf("testdata"))
		if assert.NoError(t, err) {
			assert.EqualValues(t, testdata, photo.Image())
		}
	})

	t.Run("deleted data", func(t *testing.T) {
		err := boltdb.Delete(*model.IdentifierOf("testdata"))
		if assert.NoError(t, err) {
			boltdb.db.View(func(tx *bolt.Tx) error {
				photos := tx.Bucket([]byte("photos"))
				assert.EqualValues(t, 0, photos.Stats().KeyN)
				return nil
			})
		}
	})
}

<commit_msg>Add benchmark boltdb storage<commit_after>package datastore

import (
	"testing"
	"os"
	"path"
	"io/ioutil"
	"github.com/photoshelf/photoshelf-storage/model"
	"github.com/stretchr/testify/assert"
	"github.com/boltdb/bolt"
	"fmt"
)

var boltdb *BoltdbStorage
var testdata []byte

func TestMain(m *testing.M) {
	testdataPath := path.Join(os.Getenv("GOPATH"), "src/github.com/photoshelf/photoshelf-storage", "testdata")
	body, _ := os.Open(path.Join(testdataPath, "e3158990bdee63f8594c260cd51a011d"))
	testdata, _ = ioutil.ReadAll(body)

	dataPath := path.Join(os.TempDir(), "boltdb")
	boltdb, _ = NewBoltdbStorage(dataPath)

	code := m.Run()

	boltdb.db.Close()
	boltdb = nil
	os.Exit(code)
}

func TestEmptyBucket(t *testing.T) {
	boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		return nil
	})

	t.Run("same data between src and dst", func(t *testing.T) {
		photo := model.PhotoOf(*model.IdentifierOf("testdata"), testdata)
		_, err := boltdb.Save(*photo)

		if assert.NoError(t, err) {
			boltdb.db.View(func(tx *bolt.Tx) error {
				photos := tx.Bucket([]byte("photos"))
				actual := photos.Get([]byte("testdata"))

				assert.EqualValues(t, testdata, actual)
				assert.EqualValues(t, 1, photos.Stats().KeyN)

				return nil
			})
		}
	})
}

func TestExistData(t *testing.T) {
	err := boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		photos, err := tx.CreateBucketIfNotExists([]byte("photos"))
		if err != nil {
			return err
		}
		return photos.Put([]byte("testdata"), testdata)
	})
	assert.NoError(t, err, "failure testdata setting.")

	t.Run("same data between src and read", func(t *testing.T) {
		photo, err := boltdb.Read(*model.IdentifierOf("testdata"))
		if assert.NoError(t, err) {
			assert.EqualValues(t, testdata, photo.Image())
		}
	})

	t.Run("deleted data", func(t *testing.T) {
		err := boltdb.Delete(*model.IdentifierOf("testdata"))
		if assert.NoError(t, err) {
			boltdb.db.View(func(tx *bolt.Tx) error {
				photos := tx.Bucket([]byte("photos"))
				assert.EqualValues(t, 0, photos.Stats().KeyN)
				return nil
			})
		}
	})
}

func BenchmarkBoltdbStoragePerformanceWithEmptyData(b *testing.B) {
	err := boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		_, err := tx.CreateBucketIfNotExists([]byte("photos"))
		return err
	})
	assert.NoError(b, err, "failure testdata setting.")

	b.Run("write override", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			photo := model.PhotoOf(*model.IdentifierOf("testdata"), testdata)
			boltdb.Save(*photo)
		}
	})

	b.Run("write new", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			photo := model.PhotoOf(*model.IdentifierOf(fmt.Sprintf("testdata-%d", i)), testdata)
			boltdb.Save(*photo)
		}
	})
}

func BenchmarkBoltdbStoragePerformanceWithData(b *testing.B) {
	err := boltdb.db.Update(func(tx *bolt.Tx) error {
		tx.DeleteBucket([]byte("photos"))
		photos, err := tx.CreateBucketIfNotExists([]byte("photos"))
		if err != nil {
			return err
		}
		for i := 0; i < 100; i++ {
			key := []byte(fmt.Sprintf("testdata-%d", i))
			photos.Put(key, testdata)
		}
		return nil
	})
	assert.NoError(b, err, "failure testdata setting.")

	b.Run("read same data", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			boltdb.Read(*model.IdentifierOf("testdata"))
		}
	})

	b.Run("read different data", func(b *testing.B) {
		b.ResetTimer()
		for i := 0; i < b.N; i++ {
			boltdb.Read(*model.IdentifierOf(fmt.Sprintf("testdata-%d", i)))
		}
	})
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The Kubernetes Authors.

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

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

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

package awstasks

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"k8s.io/klog/v2"

	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/cloudformation"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
)

// ElasticIP manages an AWS Address (ElasticIP)
// +kops:fitask
type ElasticIP struct {
	Name      *string
	Lifecycle *fi.Lifecycle

	ID       *string
	PublicIP *string

	// Shared is set if this is a shared IP
	Shared *bool

	// ElasticIPs don't support tags.  We instead find it via a related resource.

	// TagOnSubnet tags a subnet with the ElasticIP.  Deprecated: doesn't round-trip with terraform.
	TagOnSubnet *Subnet

	Tags map[string]string

	// AssociatedNatGatewayRouteTable follows the RouteTable -> NatGateway -> ElasticIP
	AssociatedNatGatewayRouteTable *RouteTable
}

var _ fi.CompareWithID = &ElasticIP{}

func (e *ElasticIP) CompareWithID() *string {
	return e.ID
}

// Find returns the actual ElasticIP state, or nil if not found
func (e *ElasticIP) Find(context *fi.Context) (*ElasticIP, error) {
	return e.find(context.Cloud.(awsup.AWSCloud))
}

// find will attempt to look up the elastic IP from AWS
func (e *ElasticIP) find(cloud awsup.AWSCloud) (*ElasticIP, error) {
	publicIP := e.PublicIP
	allocationID := e.ID

	// Find via RouteTable -> NatGateway -> ElasticIP
	if allocationID == nil && publicIP == nil && e.AssociatedNatGatewayRouteTable != nil {
		ngw, err := findNatGatewayFromRouteTable(cloud, e.AssociatedNatGatewayRouteTable)
		if err != nil {
			return nil, fmt.Errorf("error finding AssociatedNatGatewayRouteTable: %v", err)
		}

		if ngw == nil {
			klog.V(2).Infof("AssociatedNatGatewayRouteTable not found")
		} else {
			if len(ngw.NatGatewayAddresses) == 0 {
				return nil, fmt.Errorf("NatGateway %q has no addresses", *ngw.NatGatewayId)
			}
			if len(ngw.NatGatewayAddresses) > 1 {
				return nil, fmt.Errorf("NatGateway %q has multiple addresses", *ngw.NatGatewayId)
			}
			allocationID = ngw.NatGatewayAddresses[0].AllocationId
			if allocationID == nil {
				return nil, fmt.Errorf("NatGateway %q has nil addresses", *ngw.NatGatewayId)
			} else {
				klog.V(2).Infof("Found ElasticIP AllocationID %q via NatGateway", *allocationID)
			}
		}
	}

	// Find via tag on subnet
	// TODO: Deprecated, because doesn't round-trip with terraform
	if allocationID == nil && publicIP == nil && e.TagOnSubnet != nil && e.TagOnSubnet.ID != nil {
		var filters []*ec2.Filter
		filters = append(filters, awsup.NewEC2Filter("key", "AssociatedElasticIp"))
		filters = append(filters, awsup.NewEC2Filter("resource-id", *e.TagOnSubnet.ID))

		request := &ec2.DescribeTagsInput{
			Filters: filters,
		}

		response, err := cloud.EC2().DescribeTags(request)
		if err != nil {
			return nil, fmt.Errorf("error listing tags: %v", err)
		}

		if response == nil || len(response.Tags) == 0 {
			return nil, nil
		}

		if len(response.Tags) != 1 {
			return nil, fmt.Errorf("found multiple tags for: %v", e)
		}
		t := response.Tags[0]
		publicIP = t.Value
		klog.V(2).Infof("Found public IP via tag: %v", *publicIP)
	}

	if publicIP != nil || allocationID != nil {
		request := &ec2.DescribeAddressesInput{}
		if allocationID != nil {
			request.AllocationIds = []*string{allocationID}
		} else if publicIP != nil {
			request.Filters = []*ec2.Filter{awsup.NewEC2Filter("public-ip", *publicIP)}
		}

		response, err := cloud.EC2().DescribeAddresses(request)
		if err != nil {
			return nil, fmt.Errorf("error listing ElasticIPs: %v", err)
		}

		if response == nil || len(response.Addresses) == 0 {
			return nil, nil
		}

		if len(response.Addresses) != 1 {
			return nil, fmt.Errorf("found multiple ElasticIPs for: %v", e)
		}
		a := response.Addresses[0]
		actual := &ElasticIP{
			ID:       a.AllocationId,
			PublicIP: a.PublicIp,
		}
		actual.TagOnSubnet = e.TagOnSubnet
		actual.AssociatedNatGatewayRouteTable = e.AssociatedNatGatewayRouteTable

		{
			tags, err := cloud.EC2().DescribeTags(&ec2.DescribeTagsInput{
				Filters: []*ec2.Filter{
					{
						Name:   aws.String("resource-id"),
						Values: aws.StringSlice([]string{*a.AllocationId}),
					},
				},
			})
			if err != nil {
				return nil, fmt.Errorf("error querying tags for ElasticIP: %v", err)
			}
			var ec2Tags []*ec2.Tag
			for _, t := range tags.Tags {
				ec2Tags = append(ec2Tags, &ec2.Tag{
					Key:   t.Key,
					Value: t.Value,
				})
			}
			actual.Tags = intersectTags(ec2Tags, e.Tags)
		}

		// ElasticIP don't have a Name (no tags), so we set the name to avoid spurious changes
		actual.Name = e.Name

		e.ID = actual.ID

		// Avoid spurious changes
		actual.Lifecycle = e.Lifecycle
		actual.Shared = e.Shared

		return actual, nil
	}
	return nil, nil
}

// Run is called to execute this task.
// This is the main entry point of the task, and will actually
// connect our internal resource representation to an actual
// resource in AWS
func (e *ElasticIP) Run(c *fi.Context) error {
	return fi.DefaultDeltaRunMethod(e, c)
}

// CheckChanges validates the resource. EIPs are simple, so virtually no
// validation
func (_ *ElasticIP) CheckChanges(a, e, changes *ElasticIP) error {
	// This is a new EIP
	if a == nil {
		// No logic for EIPs - they are just created
		return nil
	}

	// This is an existing EIP
	// We should never be changing this
	if a != nil {
		if changes.PublicIP != nil {
			return fi.CannotChangeField("PublicIP")
		}
		if changes.TagOnSubnet != nil {
			return fi.CannotChangeField("TagOnSubnet")
		}
		if changes.ID != nil {
			return fi.CannotChangeField("ID")
		}
	}
	return nil
}

// RenderAWS is where we actually apply changes to AWS
func (_ *ElasticIP) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *ElasticIP) error {
	var publicIp *string
	var eipId *string

	// If this is a new ElasticIP
	if a == nil {
		klog.V(2).Infof("Creating ElasticIP for VPC")

		request := &ec2.AllocateAddressInput{
			TagSpecifications: awsup.EC2TagSpecification(ec2.ResourceTypeElasticIp, e.Tags),
		}
		request.Domain = aws.String(ec2.DomainTypeVpc)

		response, err := t.Cloud.EC2().AllocateAddress(request)
		if err != nil {
			return fmt.Errorf("error creating ElasticIP: %v", err)
		}

		e.ID = response.AllocationId
		e.PublicIP = response.PublicIp
		publicIp = e.PublicIP
		eipId = response.AllocationId
	} else {
		publicIp = a.PublicIP
		eipId = a.ID
		if err := t.AddAWSTags(*e.ID, e.Tags); err != nil {
			return err
		}
	}

	// Tag the associated subnet
	if e.TagOnSubnet != nil {
		if e.TagOnSubnet.ID == nil {
			return fmt.Errorf("Subnet ID not set")
		}
		tags := make(map[string]string)
		tags["AssociatedElasticIp"] = *publicIp
		tags["AssociatedElasticIpAllocationId"] = *eipId // Leaving this in for reference, even though we don't use it
		err := t.AddAWSTags(*e.TagOnSubnet.ID, tags)
		if err != nil {
			return fmt.Errorf("Unable to tag subnet %v", err)
		}
	} else {
		// TODO: Figure out what we can do.  We're sort of stuck between wanting to have one code-path with
		// terraform, and having a bigger "window of loss" here before we create the NATGateway
		klog.V(2).Infof("ElasticIP %q not tagged on subnet; risk of leaking", fi.StringValue(publicIp))
	}

	return nil
}

type terraformElasticIP struct {
	VPC  *bool             `json:"vpc" cty:"vpc"`
	Tags map[string]string `json:"tags,omitempty" cty:"tags"`
}

func (_ *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			return fmt.Errorf("ID must be set, if ElasticIP is shared: %v", e)
		}
		klog.V(4).Infof("reusing existing ElasticIP with id %q", aws.StringValue(e.ID))
		return nil
	}

	tf := &terraformElasticIP{
		VPC:  aws.Bool(true),
		Tags: e.Tags,
	}

	return t.RenderResource("aws_eip", *e.Name, tf)
}

func (e *ElasticIP) TerraformLink() *terraform.Literal {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			klog.Fatalf("ID must be set, if ElasticIP is shared: %v", e)
		}
		return terraform.LiteralFromStringValue(*e.ID)
	}

	return terraform.LiteralProperty("aws_eip", *e.Name, "id")
}

type cloudformationElasticIP struct {
	Domain *string             `json:"Domain"`
	Tags   []cloudformationTag `json:"Tags,omitempty"`
}

func (_ *ElasticIP) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *ElasticIP) error {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			return fmt.Errorf("ID must be set, if ElasticIP is shared: %v", e)
		}
		klog.V(4).Infof("reusing existing ElasticIP with id %q", aws.StringValue(e.ID))
		return nil
	}

	tf := &cloudformationElasticIP{
		Domain: aws.String("vpc"),
		Tags:   buildCloudformationTags(e.Tags),
	}

	return t.RenderResource("AWS::EC2::EIP", *e.Name, tf)
}

// Removed because you normally want CloudformationAllocationID
//func (e *ElasticIP) CloudformationLink() *cloudformation.Literal {
//	return cloudformation.Ref("AWS::EC2::EIP", *e.Name)
//}

func (e *ElasticIP) CloudformationAllocationID() *cloudformation.Literal {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			klog.Fatalf("ID must be set, if ElasticIP is shared: %v", e)
		}
		return cloudformation.LiteralString(*e.ID)
	}

	return cloudformation.GetAtt("AWS::EC2::EIP", *e.Name, "AllocationId")
}
<commit_msg>If one tries to use eip with a public ip that doesn't exist, fail<commit_after>/*
Copyright 2019 The Kubernetes Authors.

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

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

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

package awstasks

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"k8s.io/klog/v2"

	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/cloudformation"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
)

// ElasticIP manages an AWS Address (ElasticIP)
// +kops:fitask
type ElasticIP struct {
	Name      *string
	Lifecycle *fi.Lifecycle

	ID       *string
	PublicIP *string

	// Shared is set if this is a shared IP
	Shared *bool

	// ElasticIPs don't support tags.  We instead find it via a related resource.

	// TagOnSubnet tags a subnet with the ElasticIP.  Deprecated: doesn't round-trip with terraform.
	TagOnSubnet *Subnet

	Tags map[string]string

	// AssociatedNatGatewayRouteTable follows the RouteTable -> NatGateway -> ElasticIP
	AssociatedNatGatewayRouteTable *RouteTable
}

var _ fi.CompareWithID = &ElasticIP{}

func (e *ElasticIP) CompareWithID() *string {
	return e.ID
}

// Find returns the actual ElasticIP state, or nil if not found
func (e *ElasticIP) Find(context *fi.Context) (*ElasticIP, error) {
	return e.find(context.Cloud.(awsup.AWSCloud))
}

// find will attempt to look up the elastic IP from AWS
func (e *ElasticIP) find(cloud awsup.AWSCloud) (*ElasticIP, error) {
	publicIP := e.PublicIP
	allocationID := e.ID

	// Find via RouteTable -> NatGateway -> ElasticIP
	if allocationID == nil && publicIP == nil && e.AssociatedNatGatewayRouteTable != nil {
		ngw, err := findNatGatewayFromRouteTable(cloud, e.AssociatedNatGatewayRouteTable)
		if err != nil {
			return nil, fmt.Errorf("error finding AssociatedNatGatewayRouteTable: %v", err)
		}

		if ngw == nil {
			klog.V(2).Infof("AssociatedNatGatewayRouteTable not found")
		} else {
			if len(ngw.NatGatewayAddresses) == 0 {
				return nil, fmt.Errorf("NatGateway %q has no addresses", *ngw.NatGatewayId)
			}
			if len(ngw.NatGatewayAddresses) > 1 {
				return nil, fmt.Errorf("NatGateway %q has multiple addresses", *ngw.NatGatewayId)
			}
			allocationID = ngw.NatGatewayAddresses[0].AllocationId
			if allocationID == nil {
				return nil, fmt.Errorf("NatGateway %q has nil addresses", *ngw.NatGatewayId)
			} else {
				klog.V(2).Infof("Found ElasticIP AllocationID %q via NatGateway", *allocationID)
			}
		}
	}

	// Find via tag on subnet
	// TODO: Deprecated, because doesn't round-trip with terraform
	if allocationID == nil && publicIP == nil && e.TagOnSubnet != nil && e.TagOnSubnet.ID != nil {
		var filters []*ec2.Filter
		filters = append(filters, awsup.NewEC2Filter("key", "AssociatedElasticIp"))
		filters = append(filters, awsup.NewEC2Filter("resource-id", *e.TagOnSubnet.ID))

		request := &ec2.DescribeTagsInput{
			Filters: filters,
		}

		response, err := cloud.EC2().DescribeTags(request)
		if err != nil {
			return nil, fmt.Errorf("error listing tags: %v", err)
		}

		if response == nil || len(response.Tags) == 0 {
			return nil, nil
		}

		if len(response.Tags) != 1 {
			return nil, fmt.Errorf("found multiple tags for: %v", e)
		}
		t := response.Tags[0]
		publicIP = t.Value
		klog.V(2).Infof("Found public IP via tag: %v", *publicIP)
	}

	if publicIP != nil || allocationID != nil {
		request := &ec2.DescribeAddressesInput{}
		if allocationID != nil {
			request.AllocationIds = []*string{allocationID}
		} else if publicIP != nil {
			request.Filters = []*ec2.Filter{awsup.NewEC2Filter("public-ip", *publicIP)}
		}

		response, err := cloud.EC2().DescribeAddresses(request)
		if err != nil {
			return nil, fmt.Errorf("error listing ElasticIPs: %v", err)
		}

		if response == nil || len(response.Addresses) == 0 {
			return nil, fmt.Errorf("found no ElasticIPs for: %v", e)
		}

		if len(response.Addresses) != 1 {
			return nil, fmt.Errorf("found multiple ElasticIPs for: %v", e)
		}
		a := response.Addresses[0]
		actual := &ElasticIP{
			ID:       a.AllocationId,
			PublicIP: a.PublicIp,
		}
		actual.TagOnSubnet = e.TagOnSubnet
		actual.AssociatedNatGatewayRouteTable = e.AssociatedNatGatewayRouteTable

		{
			tags, err := cloud.EC2().DescribeTags(&ec2.DescribeTagsInput{
				Filters: []*ec2.Filter{
					{
						Name:   aws.String("resource-id"),
						Values: aws.StringSlice([]string{*a.AllocationId}),
					},
				},
			})
			if err != nil {
				return nil, fmt.Errorf("error querying tags for ElasticIP: %v", err)
			}
			var ec2Tags []*ec2.Tag
			for _, t := range tags.Tags {
				ec2Tags = append(ec2Tags, &ec2.Tag{
					Key:   t.Key,
					Value: t.Value,
				})
			}
			actual.Tags = intersectTags(ec2Tags, e.Tags)
		}

		// ElasticIP don't have a Name (no tags), so we set the name to avoid spurious changes
		actual.Name = e.Name

		e.ID = actual.ID

		// Avoid spurious changes
		actual.Lifecycle = e.Lifecycle
		actual.Shared = e.Shared

		return actual, nil
	}
	return nil, nil
}

// Run is called to execute this task.
// This is the main entry point of the task, and will actually
// connect our internal resource representation to an actual
// resource in AWS
func (e *ElasticIP) Run(c *fi.Context) error {
	return fi.DefaultDeltaRunMethod(e, c)
}

// CheckChanges validates the resource. EIPs are simple, so virtually no
// validation
func (_ *ElasticIP) CheckChanges(a, e, changes *ElasticIP) error {
	// This is a new EIP
	if a == nil {
		// No logic for EIPs - they are just created
		return nil
	}

	// This is an existing EIP
	// We should never be changing this
	if a != nil {
		if changes.PublicIP != nil {
			return fi.CannotChangeField("PublicIP")
		}
		if changes.TagOnSubnet != nil {
			return fi.CannotChangeField("TagOnSubnet")
		}
		if changes.ID != nil {
			return fi.CannotChangeField("ID")
		}
	}
	return nil
}

// RenderAWS is where we actually apply changes to AWS
func (_ *ElasticIP) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *ElasticIP) error {
	var publicIp *string
	var eipId *string

	// If this is a new ElasticIP
	if a == nil {
		klog.V(2).Infof("Creating ElasticIP for VPC")

		request := &ec2.AllocateAddressInput{
			TagSpecifications: awsup.EC2TagSpecification(ec2.ResourceTypeElasticIp, e.Tags),
		}
		request.Domain = aws.String(ec2.DomainTypeVpc)

		response, err := t.Cloud.EC2().AllocateAddress(request)
		if err != nil {
			return fmt.Errorf("error creating ElasticIP: %v", err)
		}

		e.ID = response.AllocationId
		e.PublicIP = response.PublicIp
		publicIp = e.PublicIP
		eipId = response.AllocationId
	} else {
		publicIp = a.PublicIP
		eipId = a.ID
		if err := t.AddAWSTags(*e.ID, e.Tags); err != nil {
			return err
		}
	}

	// Tag the associated subnet
	if e.TagOnSubnet != nil {
		if e.TagOnSubnet.ID == nil {
			return fmt.Errorf("Subnet ID not set")
		}
		tags := make(map[string]string)
		tags["AssociatedElasticIp"] = *publicIp
		tags["AssociatedElasticIpAllocationId"] = *eipId // Leaving this in for reference, even though we don't use it
		err := t.AddAWSTags(*e.TagOnSubnet.ID, tags)
		if err != nil {
			return fmt.Errorf("Unable to tag subnet %v", err)
		}
	} else {
		// TODO: Figure out what we can do.  We're sort of stuck between wanting to have one code-path with
		// terraform, and having a bigger "window of loss" here before we create the NATGateway
		klog.V(2).Infof("ElasticIP %q not tagged on subnet; risk of leaking", fi.StringValue(publicIp))
	}

	return nil
}

type terraformElasticIP struct {
	VPC  *bool             `json:"vpc" cty:"vpc"`
	Tags map[string]string `json:"tags,omitempty" cty:"tags"`
}

func (_ *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			return fmt.Errorf("ID must be set, if ElasticIP is shared: %v", e)
		}
		klog.V(4).Infof("reusing existing ElasticIP with id %q", aws.StringValue(e.ID))
		return nil
	}

	tf := &terraformElasticIP{
		VPC:  aws.Bool(true),
		Tags: e.Tags,
	}

	return t.RenderResource("aws_eip", *e.Name, tf)
}

func (e *ElasticIP) TerraformLink() *terraform.Literal {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			klog.Fatalf("ID must be set, if ElasticIP is shared: %v", e)
		}
		return terraform.LiteralFromStringValue(*e.ID)
	}

	return terraform.LiteralProperty("aws_eip", *e.Name, "id")
}

type cloudformationElasticIP struct {
	Domain *string             `json:"Domain"`
	Tags   []cloudformationTag `json:"Tags,omitempty"`
}

func (_ *ElasticIP) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *ElasticIP) error {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			return fmt.Errorf("ID must be set, if ElasticIP is shared: %v", e)
		}
		klog.V(4).Infof("reusing existing ElasticIP with id %q", aws.StringValue(e.ID))
		return nil
	}

	tf := &cloudformationElasticIP{
		Domain: aws.String("vpc"),
		Tags:   buildCloudformationTags(e.Tags),
	}

	return t.RenderResource("AWS::EC2::EIP", *e.Name, tf)
}

// Removed because you normally want CloudformationAllocationID
//func (e *ElasticIP) CloudformationLink() *cloudformation.Literal {
//	return cloudformation.Ref("AWS::EC2::EIP", *e.Name)
//}

func (e *ElasticIP) CloudformationAllocationID() *cloudformation.Literal {
	if fi.BoolValue(e.Shared) {
		if e.ID == nil {
			klog.Fatalf("ID must be set, if ElasticIP is shared: %v", e)
		}
		return cloudformation.LiteralString(*e.ID)
	}

	return cloudformation.GetAtt("AWS::EC2::EIP", *e.Name, "AllocationId")
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package awstasks

import (
	//"fmt"
	//
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/golang/glog"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
)

//go:generate fitask -type=ElasticIP

// Elastic IP
// Representation the EIP AWS task
type ElasticIP struct {
	Name     *string
	ID       *string
	PublicIP *string

	// Allow support for associated subnets
	// If you need another resource to tag on (ebs volume)
	// you must add it
	Subnet *Subnet
}

var _ fi.CompareWithID = &ElasticIP{}

func (e *ElasticIP) CompareWithID() *string {
	return e.ID
}

var _ fi.HasAddress = &ElasticIP{}

func (e *ElasticIP) FindAddress(context *fi.Context) (*string, error) {
	actual, err := e.find(context.Cloud.(awsup.AWSCloud))
	if err != nil {
		return nil, fmt.Errorf("error querying for ElasticIP: %v", err)
	}
	if actual == nil {
		return nil, nil
	}
	return actual.PublicIP, nil
}

// Find is a public wrapper for find()
func (e *ElasticIP) Find(context *fi.Context) (*ElasticIP, error) {
	return e.find(context.Cloud.(awsup.AWSCloud))
}

// find will attempt to look up the elastic IP from AWS
func (e *ElasticIP) find(cloud awsup.AWSCloud) (*ElasticIP, error) {
	publicIP := e.PublicIP
	allocationID := e.ID

	// Find via tag on foreign resource
	if allocationID == nil && publicIP == nil && e.Subnet.ID != nil {
		var filters []*ec2.Filter
		filters = append(filters, awsup.NewEC2Filter("key", "AssociatedElasticIp"))
		filters = append(filters, awsup.NewEC2Filter("resource-id", *e.Subnet.ID))

		request := &ec2.DescribeTagsInput{
			Filters: filters,
		}

		response, err := cloud.EC2().DescribeTags(request)
		if err != nil {
			return nil, fmt.Errorf("error listing tags: %v", err)
		}

		if response == nil || len(response.Tags) == 0 {
			return nil, nil
		}

		if len(response.Tags) != 1 {
			return nil, fmt.Errorf("found multiple tags for: %v", e)
		}
		t := response.Tags[0]
		publicIP = t.Value
		glog.V(2).Infof("Found public IP via tag: %v", *publicIP)
	}

	if publicIP != nil || allocationID != nil {
		request := &ec2.DescribeAddressesInput{}
		if allocationID != nil {
			request.AllocationIds = []*string{allocationID}
		} else if publicIP != nil {
			request.Filters = []*ec2.Filter{awsup.NewEC2Filter("public-ip", *publicIP)}
		}

		response, err := cloud.EC2().DescribeAddresses(request)
		if err != nil {
			return nil, fmt.Errorf("error listing ElasticIPs: %v", err)
		}

		if response == nil || len(response.Addresses) == 0 {
			return nil, nil
		}

		if len(response.Addresses) != 1 {
			return nil, fmt.Errorf("found multiple ElasticIPs for: %v", e)
		}
		a := response.Addresses[0]
		actual := &ElasticIP{
			ID:       a.AllocationId,
			PublicIP: a.PublicIp,
		}
		actual.Subnet = e.Subnet

		// ElasticIP don't have a Name (no tags), so we set the name to avoid spurious changes
		actual.Name = e.Name

		e.ID = actual.ID

		return actual, nil
	}
	return nil, nil
}

// The Run() function is called to execute this task.
// This is the main entry point of the task, and will actually
// connect our internal resource representation to an actual
// resource in AWS
func (e *ElasticIP) Run(c *fi.Context) error {
	return fi.DefaultDeltaRunMethod(e, c)
}

// CheckChanges validates the resource. EIPs are simple, so virtually no
// validation
func (s *ElasticIP) CheckChanges(a, e, changes *ElasticIP) error {
	// This is a new EIP
	if a == nil {
		// No logic for EIPs - they are just created
	}

	// This is an existing EIP
	// We should never be changing this
	if a != nil {
		if changes.PublicIP != nil {
			return fi.CannotChangeField("PublicIP")
		}
		if changes.Subnet != nil {
			return fi.CannotChangeField("Subnet")
		}
		if changes.ID != nil {
			return fi.CannotChangeField("ID")
		}
	}
	return nil
}

// RenderAWS is where we actually apply changes to AWS
func (_ *ElasticIP) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *ElasticIP) error {

	var publicIp *string
	var eipId *string

	// If this is a new ElasticIP
	if a == nil {
		glog.V(2).Infof("Creating ElasticIP for VPC")

		request := &ec2.AllocateAddressInput{}
		request.Domain = aws.String(ec2.DomainTypeVpc)

		response, err := t.Cloud.EC2().AllocateAddress(request)
		if err != nil {
			return fmt.Errorf("error creating ElasticIP: %v", err)
		}

		e.ID = response.AllocationId
		e.PublicIP = response.PublicIp
		publicIp = e.PublicIP
		eipId = response.AllocationId
	} else {
		publicIp = a.PublicIP
		eipId = a.ID
	}

	// Tag the associated subnet
	if e.Subnet == nil {
		return fmt.Errorf("Subnet not set")
	} else if e.Subnet.ID == nil {
		return fmt.Errorf("Subnet ID not set")
	}
	tags := make(map[string]string)
	tags["AssociatedElasticIp"] = *publicIp
	tags["AssociatedElasticIpAllocationId"] = *eipId // Leaving this in for reference, even though we don't use it
	err := t.AddAWSTags(*e.Subnet.ID, tags)
	if err != nil {
		return fmt.Errorf("Unable to tag subnet %v", err)
	}
	return nil
}

type terraformElasticIP struct {
	VPC *bool `json:"vpc"`
}

func (_ *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error {
	tf := &terraformElasticIP{
		VPC: aws.Bool(true),
	}

	return t.RenderResource("aws_eip", *e.Name, tf)
}

func (e *ElasticIP) TerraformLink() *terraform.Literal {
	return terraform.LiteralProperty("aws_eip", *e.Name, "id")
}
<commit_msg>terraform import, thanks vsc<commit_after>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package awstasks

import (
	//"fmt"
	//
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/golang/glog"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
)

//go:generate fitask -type=ElasticIP

// Elastic IP
// Representation the EIP AWS task
type ElasticIP struct {
	Name     *string
	ID       *string
	PublicIP *string

	// Allow support for associated subnets
	// If you need another resource to tag on (ebs volume)
	// you must add it
	Subnet *Subnet
}

var _ fi.CompareWithID = &ElasticIP{}

func (e *ElasticIP) CompareWithID() *string {
	return e.ID
}

var _ fi.HasAddress = &ElasticIP{}

func (e *ElasticIP) FindAddress(context *fi.Context) (*string, error) {
	actual, err := e.find(context.Cloud.(awsup.AWSCloud))
	if err != nil {
		return nil, fmt.Errorf("error querying for ElasticIP: %v", err)
	}
	if actual == nil {
		return nil, nil
	}
	return actual.PublicIP, nil
}

// Find is a public wrapper for find()
func (e *ElasticIP) Find(context *fi.Context) (*ElasticIP, error) {
	return e.find(context.Cloud.(awsup.AWSCloud))
}

// find will attempt to look up the elastic IP from AWS
func (e *ElasticIP) find(cloud awsup.AWSCloud) (*ElasticIP, error) {
	publicIP := e.PublicIP
	allocationID := e.ID

	// Find via tag on foreign resource
	if allocationID == nil && publicIP == nil && e.Subnet.ID != nil {
		var filters []*ec2.Filter
		filters = append(filters, awsup.NewEC2Filter("key", "AssociatedElasticIp"))
		filters = append(filters, awsup.NewEC2Filter("resource-id", *e.Subnet.ID))

		request := &ec2.DescribeTagsInput{
			Filters: filters,
		}

		response, err := cloud.EC2().DescribeTags(request)
		if err != nil {
			return nil, fmt.Errorf("error listing tags: %v", err)
		}

		if response == nil || len(response.Tags) == 0 {
			return nil, nil
		}

		if len(response.Tags) != 1 {
			return nil, fmt.Errorf("found multiple tags for: %v", e)
		}
		t := response.Tags[0]
		publicIP = t.Value
		glog.V(2).Infof("Found public IP via tag: %v", *publicIP)
	}

	if publicIP != nil || allocationID != nil {
		request := &ec2.DescribeAddressesInput{}
		if allocationID != nil {
			request.AllocationIds = []*string{allocationID}
		} else if publicIP != nil {
			request.Filters = []*ec2.Filter{awsup.NewEC2Filter("public-ip", *publicIP)}
		}

		response, err := cloud.EC2().DescribeAddresses(request)
		if err != nil {
			return nil, fmt.Errorf("error listing ElasticIPs: %v", err)
		}

		if response == nil || len(response.Addresses) == 0 {
			return nil, nil
		}

		if len(response.Addresses) != 1 {
			return nil, fmt.Errorf("found multiple ElasticIPs for: %v", e)
		}
		a := response.Addresses[0]
		actual := &ElasticIP{
			ID:       a.AllocationId,
			PublicIP: a.PublicIp,
		}
		actual.Subnet = e.Subnet

		// ElasticIP don't have a Name (no tags), so we set the name to avoid spurious changes
		actual.Name = e.Name

		e.ID = actual.ID

		return actual, nil
	}
	return nil, nil
}

// The Run() function is called to execute this task.
// This is the main entry point of the task, and will actually
// connect our internal resource representation to an actual
// resource in AWS
func (e *ElasticIP) Run(c *fi.Context) error {
	return fi.DefaultDeltaRunMethod(e, c)
}

// CheckChanges validates the resource. EIPs are simple, so virtually no
// validation
func (s *ElasticIP) CheckChanges(a, e, changes *ElasticIP) error {
	// This is a new EIP
	if a == nil {
		// No logic for EIPs - they are just created
	}

	// This is an existing EIP
	// We should never be changing this
	if a != nil {
		if changes.PublicIP != nil {
			return fi.CannotChangeField("PublicIP")
		}
		if changes.Subnet != nil {
			return fi.CannotChangeField("Subnet")
		}
		if changes.ID != nil {
			return fi.CannotChangeField("ID")
		}
	}
	return nil
}

// RenderAWS is where we actually apply changes to AWS
func (_ *ElasticIP) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *ElasticIP) error {

	var publicIp *string
	var eipId *string

	// If this is a new ElasticIP
	if a == nil {
		glog.V(2).Infof("Creating ElasticIP for VPC")

		request := &ec2.AllocateAddressInput{}
		request.Domain = aws.String(ec2.DomainTypeVpc)

		response, err := t.Cloud.EC2().AllocateAddress(request)
		if err != nil {
			return fmt.Errorf("error creating ElasticIP: %v", err)
		}

		e.ID = response.AllocationId
		e.PublicIP = response.PublicIp
		publicIp = e.PublicIP
		eipId = response.AllocationId
	} else {
		publicIp = a.PublicIP
		eipId = a.ID
	}

	// Tag the associated subnet
	if e.Subnet == nil {
		return fmt.Errorf("Subnet not set")
	} else if e.Subnet.ID == nil {
		return fmt.Errorf("Subnet ID not set")
	}
	tags := make(map[string]string)
	tags["AssociatedElasticIp"] = *publicIp
	tags["AssociatedElasticIpAllocationId"] = *eipId // Leaving this in for reference, even though we don't use it
	err := t.AddAWSTags(*e.Subnet.ID, tags)
	if err != nil {
		return fmt.Errorf("Unable to tag subnet %v", err)
	}
	return nil
}

type terraformElasticIP struct {
	VPC *bool `json:"vpc"`
}

func (_ *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error {
	tf := &terraformElasticIP{
		VPC: aws.Bool(true),
	}

	return t.RenderResource("aws_eip", *e.Name, tf)
}

func (e *ElasticIP) TerraformLink() *terraform.Literal {
	return terraform.LiteralProperty("aws_eip", *e.Name, "id")
}
<|endoftext|>


<commit_before>package integration

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

	"github.com/alphagov/publishing-api"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/ghttp"
)

var _ = Describe("Content Item Requests", func() {
	contentItemWithAccessLimiting := map[string]interface{}{
		"base_path":      "/vat-rates",
		"title":          "VAT Rates",
		"description":    "VAT rates for goods and services",
		"format":         "guide",
		"publishing_app": "mainstream_publisher",
		"locale":         "en",
		"details": map[string]interface{}{
			"app":      "or format",
			"specific": "data...",
		},
		"access_limited": map[string]interface{}{
			"users": []string{
				"f17250b0-7540-0131-f036-005056030202",
				"74c7d700-5b4a-0131-7a8e-005056030037",
			},
		},
	}

	contentItem := make(map[string]interface{})

	for k, v := range contentItemWithAccessLimiting {
		if k != "access_limited" {
			contentItem[k] = v
		}
	}

	var testPublishingAPI *httptest.Server
	var testURLArbiter, testDraftContentStore, testLiveContentStore *ghttp.Server
	var endpoint string

	var expectedResponse HTTPTestResponse

	// Mock server configurations. A default is set in the BeforeEach, but can be
	// overridden if needed in your test.
	var urlArbiterResponseCode int
	var urlArbiterResponseBody string

	BeforeEach(func() {
		// URL arbiter mock server - default response (override in your test if needed)
		urlArbiterResponseCode = http.StatusOK
		urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher"}`

		TestRequestOrderTracker = make(chan TestRequestLabel, 3)

		testURLArbiter = ghttp.NewServer()
		testDraftContentStore = ghttp.NewServer()
		testLiveContentStore = ghttp.NewServer()

		testURLArbiter.AppendHandlers(ghttp.CombineHandlers(
			trackRequest(URLArbiterRequestLabel),
			ghttp.VerifyRequest("PUT", "/paths/vat-rates"),
			ghttp.VerifyJSON(`{"publishing_app": "mainstream_publisher"}`),
			ghttp.RespondWithPtr(&urlArbiterResponseCode, &urlArbiterResponseBody, http.Header{"Content-Type": []string{"application/json"}}),
		))

		testPublishingAPI = httptest.NewServer(main.BuildHTTPMux(testURLArbiter.URL(), testLiveContentStore.URL(), testDraftContentStore.URL(), nil))
		endpoint = testPublishingAPI.URL + "/content/vat-rates"
	})

	AfterEach(func() {
		testURLArbiter.Close()
		testDraftContentStore.Close()
		testLiveContentStore.Close()
		testPublishingAPI.Close()
		close(TestRequestOrderTracker)
	})

	Describe("PUT /content", func() {
		Context("when URL arbiter errs", func() {
			It("returns a 422 status with the original response", func() {
				urlArbiterResponseCode = 422
				urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher","errors":{"base_path":["is not valid"]}}`

				actualResponse := doJSONRequest("PUT", endpoint, contentItem)

				Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
				Expect(testDraftContentStore.ReceivedRequests()).To(BeEmpty())
				Expect(testLiveContentStore.ReceivedRequests()).To(BeEmpty())

				expectedResponse = HTTPTestResponse{Code: 422, Body: urlArbiterResponseBody}
				assertSameResponse(actualResponse, &expectedResponse)
			})

			It("returns a 409 status with the original response", func() {
				urlArbiterResponseCode = 409
				urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher","errors":{"base_path":["is already taken"]}}`

				actualResponse := doJSONRequest("PUT", endpoint, contentItem)

				Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
				Expect(testDraftContentStore.ReceivedRequests()).To(BeEmpty())
				Expect(testLiveContentStore.ReceivedRequests()).To(BeEmpty())

				expectedResponse = HTTPTestResponse{Code: 409, Body: urlArbiterResponseBody}
				assertSameResponse(actualResponse, &expectedResponse)
			})
		})

		It("registers a path with URL arbiter and then publishes the content to the live and draft content store", func() {
			testDraftContentStore.AppendHandlers(ghttp.CombineHandlers(
				trackRequest(DraftContentStoreRequestLabel),
				ghttp.VerifyRequest("PUT", "/content/vat-rates"),
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			testLiveContentStore.AppendHandlers(ghttp.CombineHandlers(
				trackRequest(LiveContentStoreRequestLabel),
				ghttp.VerifyRequest("PUT", "/content/vat-rates"),
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			actualResponse := doJSONRequest("PUT", endpoint, contentItem)

			Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
			Expect(testDraftContentStore.ReceivedRequests()).To(HaveLen(1))
			Expect(testLiveContentStore.ReceivedRequests()).To(HaveLen(1))

			expectedBody, _ := json.Marshal(contentItem)
			expectedResponse = HTTPTestResponse{Code: http.StatusOK, Body: string(expectedBody[:])}
			assertSameResponse(actualResponse, &expectedResponse)

			// assert that url-arbiter is called before making requests to content stores. communication
			// with live and draft content stores happens in parallel, so can't assert on their order.
			Expect(<-TestRequestOrderTracker).To(Equal(URLArbiterRequestLabel))
			Expect(<-TestRequestOrderTracker > URLArbiterRequestLabel).To(BeTrue())
			Expect(<-TestRequestOrderTracker > URLArbiterRequestLabel).To(BeTrue())
		})

		It("returns a 400 error if given invalid JSON", func() {
			actualResponse := doRequest("PUT", endpoint, []byte("i'm not json"))

			Expect(testURLArbiter.ReceivedRequests()).To(BeZero())
			Expect(testDraftContentStore.ReceivedRequests()).To(BeZero())
			Expect(testLiveContentStore.ReceivedRequests()).To(BeZero())

			expectedResponseBody := `{"message": "Invalid JSON in request body: invalid character 'i' looking for beginning of value"}`
			expectedResponse = HTTPTestResponse{Code: http.StatusBadRequest, Body: expectedResponseBody}
			assertSameResponse(actualResponse, &expectedResponse)
		})

		It("returns Content-Type header as received from content-store", func() {
			testDraftContentStore.AppendHandlers(
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			)
			testLiveContentStore.AppendHandlers(
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem, http.Header{"Content-Type": []string{"text/html"}}),
			)

			actualResponse := doJSONRequest("PUT", endpoint, contentItem)

			Expect(testLiveContentStore.ReceivedRequests()).To(HaveLen(1))
			Expect(actualResponse.Header.Get("Content-Type")).To(Equal("text/html"))
		})
	})
})
<commit_msg>Test that access limiting is stripped on live endpoint.<commit_after>package integration

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

	"github.com/alphagov/publishing-api"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/ghttp"
)

var _ = Describe("Content Item Requests", func() {
	contentItemWithAccessLimiting := map[string]interface{}{
		"base_path":      "/vat-rates",
		"title":          "VAT Rates",
		"description":    "VAT rates for goods and services",
		"format":         "guide",
		"publishing_app": "mainstream_publisher",
		"locale":         "en",
		"details": map[string]interface{}{
			"app":      "or format",
			"specific": "data...",
		},
		"access_limited": map[string]interface{}{
			"users": []string{
				"f17250b0-7540-0131-f036-005056030202",
				"74c7d700-5b4a-0131-7a8e-005056030037",
			},
		},
	}

	contentItem := make(map[string]interface{})

	for k, v := range contentItemWithAccessLimiting {
		if k != "access_limited" {
			contentItem[k] = v
		}
	}

	var testPublishingAPI *httptest.Server
	var testURLArbiter, testDraftContentStore, testLiveContentStore *ghttp.Server
	var endpoint string

	var expectedResponse HTTPTestResponse

	// Mock server configurations. A default is set in the BeforeEach, but can be
	// overridden if needed in your test.
	var urlArbiterResponseCode int
	var urlArbiterResponseBody string

	BeforeEach(func() {
		// URL arbiter mock server - default response (override in your test if needed)
		urlArbiterResponseCode = http.StatusOK
		urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher"}`

		TestRequestOrderTracker = make(chan TestRequestLabel, 3)

		testURLArbiter = ghttp.NewServer()
		testDraftContentStore = ghttp.NewServer()
		testLiveContentStore = ghttp.NewServer()

		testURLArbiter.AppendHandlers(ghttp.CombineHandlers(
			trackRequest(URLArbiterRequestLabel),
			ghttp.VerifyRequest("PUT", "/paths/vat-rates"),
			ghttp.VerifyJSON(`{"publishing_app": "mainstream_publisher"}`),
			ghttp.RespondWithPtr(&urlArbiterResponseCode, &urlArbiterResponseBody, http.Header{"Content-Type": []string{"application/json"}}),
		))

		testPublishingAPI = httptest.NewServer(main.BuildHTTPMux(testURLArbiter.URL(), testLiveContentStore.URL(), testDraftContentStore.URL(), nil))
		endpoint = testPublishingAPI.URL + "/content/vat-rates"
	})

	AfterEach(func() {
		testURLArbiter.Close()
		testDraftContentStore.Close()
		testLiveContentStore.Close()
		testPublishingAPI.Close()
		close(TestRequestOrderTracker)
	})

	Describe("PUT /content", func() {
		Context("when URL arbiter errs", func() {
			It("returns a 422 status with the original response", func() {
				urlArbiterResponseCode = 422
				urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher","errors":{"base_path":["is not valid"]}}`

				actualResponse := doJSONRequest("PUT", endpoint, contentItem)

				Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
				Expect(testDraftContentStore.ReceivedRequests()).To(BeEmpty())
				Expect(testLiveContentStore.ReceivedRequests()).To(BeEmpty())

				expectedResponse = HTTPTestResponse{Code: 422, Body: urlArbiterResponseBody}
				assertSameResponse(actualResponse, &expectedResponse)
			})

			It("returns a 409 status with the original response", func() {
				urlArbiterResponseCode = 409
				urlArbiterResponseBody = `{"path":"/vat-rates","publishing_app":"mainstream_publisher","errors":{"base_path":["is already taken"]}}`

				actualResponse := doJSONRequest("PUT", endpoint, contentItem)

				Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
				Expect(testDraftContentStore.ReceivedRequests()).To(BeEmpty())
				Expect(testLiveContentStore.ReceivedRequests()).To(BeEmpty())

				expectedResponse = HTTPTestResponse{Code: 409, Body: urlArbiterResponseBody}
				assertSameResponse(actualResponse, &expectedResponse)
			})
		})

		It("registers a path with URL arbiter and then publishes the content to the live and draft content store", func() {
			testDraftContentStore.AppendHandlers(ghttp.CombineHandlers(
				trackRequest(DraftContentStoreRequestLabel),
				ghttp.VerifyRequest("PUT", "/content/vat-rates"),
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			testLiveContentStore.AppendHandlers(ghttp.CombineHandlers(
				trackRequest(LiveContentStoreRequestLabel),
				ghttp.VerifyRequest("PUT", "/content/vat-rates"),
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			actualResponse := doJSONRequest("PUT", endpoint, contentItem)

			Expect(testURLArbiter.ReceivedRequests()).To(HaveLen(1))
			Expect(testDraftContentStore.ReceivedRequests()).To(HaveLen(1))
			Expect(testLiveContentStore.ReceivedRequests()).To(HaveLen(1))

			expectedBody, _ := json.Marshal(contentItem)
			expectedResponse = HTTPTestResponse{Code: http.StatusOK, Body: string(expectedBody[:])}
			assertSameResponse(actualResponse, &expectedResponse)

			// assert that url-arbiter is called before making requests to content stores. communication
			// with live and draft content stores happens in parallel, so can't assert on their order.
			Expect(<-TestRequestOrderTracker).To(Equal(URLArbiterRequestLabel))
			Expect(<-TestRequestOrderTracker > URLArbiterRequestLabel).To(BeTrue())
			Expect(<-TestRequestOrderTracker > URLArbiterRequestLabel).To(BeTrue())
		})

		It("returns a 400 error if given invalid JSON", func() {
			actualResponse := doRequest("PUT", endpoint, []byte("i'm not json"))

			Expect(testURLArbiter.ReceivedRequests()).To(BeZero())
			Expect(testDraftContentStore.ReceivedRequests()).To(BeZero())
			Expect(testLiveContentStore.ReceivedRequests()).To(BeZero())

			expectedResponseBody := `{"message": "Invalid JSON in request body: invalid character 'i' looking for beginning of value"}`
			expectedResponse = HTTPTestResponse{Code: http.StatusBadRequest, Body: expectedResponseBody}
			assertSameResponse(actualResponse, &expectedResponse)
		})

		It("returns Content-Type header as received from content-store", func() {
			testDraftContentStore.AppendHandlers(
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			)
			testLiveContentStore.AppendHandlers(
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem, http.Header{"Content-Type": []string{"text/html"}}),
			)

			actualResponse := doJSONRequest("PUT", endpoint, contentItem)

			Expect(testLiveContentStore.ReceivedRequests()).To(HaveLen(1))
			Expect(actualResponse.Header.Get("Content-Type")).To(Equal("text/html"))
		})

		It("strips access limiting metadata from the document", func() {
			testDraftContentStore.AppendHandlers(ghttp.CombineHandlers(
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			testLiveContentStore.AppendHandlers(ghttp.CombineHandlers(
				ghttp.VerifyJSONRepresenting(contentItem),
				ghttp.RespondWithJSONEncoded(http.StatusOK, contentItem),
			))

			actualResponse := doJSONRequest("PUT", endpoint, contentItemWithAccessLimiting)

			Expect(testDraftContentStore.ReceivedRequests()).To(HaveLen(1))
			Expect(testLiveContentStore.ReceivedRequests()).To(HaveLen(1))

			expectedBody, _ := json.Marshal(contentItem)
			expectedResponse = HTTPTestResponse{Code: http.StatusOK, Body: string(expectedBody[:])}
			assertSameResponse(actualResponse, &expectedResponse)
		})
	})
})
<|endoftext|>


<commit_before>// +build linux

package cniprovider

import (
	"os"
	"path/filepath"
	"syscall"
	"unsafe"

	"github.com/containerd/containerd/oci"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

func createNetNS(c *cniProvider, id string) (string, error) {
	p := filepath.Join(c.root, "net/cni", id)
	if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
		deleteNetNS(p)
		return "", err
	}

	f, err := os.Create(p)
	if err != nil {
		deleteNetNS(p)
		return "", err
	}
	if err := f.Close(); err != nil {
		deleteNetNS(p)
		return "", err
	}
	procNetNSBytes, err := syscall.BytePtrFromString("/proc/self/ns/net")
	if err != nil {
		deleteNetNS(p)
		return "", err
	}
	pBytes, err := syscall.BytePtrFromString(p)
	if err != nil {
		deleteNetNS(p)
		return "", err
	}
	beforeFork()

	pid, _, errno := syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD)|unix.CLONE_NEWNET, 0, 0, 0, 0, 0)
	if errno != 0 {
		afterFork()
		deleteNetNS(p)
		return "", errno
	}

	if pid != 0 {
		afterFork()
		var ws unix.WaitStatus
		_, err = unix.Wait4(int(pid), &ws, 0, nil)
		for err == syscall.EINTR {
			_, err = unix.Wait4(int(pid), &ws, 0, nil)
		}

		if err != nil {
			deleteNetNS(p)
			return "", errors.Wrapf(err, "failed to find pid=%d process", pid)
		}
		errno = syscall.Errno(ws.ExitStatus())
		if errno != 0 {
			deleteNetNS(p)
			return "", errors.Wrapf(errno, "failed to mount %s (pid=%d)", p, pid)
		}
		return p, nil
	}
	afterForkInChild()
	_, _, errno = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(procNetNSBytes)), uintptr(unsafe.Pointer(pBytes)), 0, uintptr(unix.MS_BIND), 0, 0)
	syscall.RawSyscall(syscall.SYS_EXIT, uintptr(errno), 0, 0)
	panic("unreachable")
}

func setNetNS(s *specs.Spec, nativeID string) error {
	return oci.WithLinuxNamespace(specs.LinuxNamespace{
		Type: specs.NetworkNamespace,
		Path: nativeID,
	})(nil, nil, nil, s)
}

func unmountNetNS(nativeID string) error {
	if err := unix.Unmount(nativeID, unix.MNT_DETACH); err != nil {
		if err != syscall.EINVAL && err != syscall.ENOENT {
			return errors.Wrap(err, "error unmounting network namespace")
		}
	}
	return nil
}

func deleteNetNS(nativeID string) error {
	if err := os.RemoveAll(nativeID); err != nil && !errors.Is(err, os.ErrNotExist) {
		return errors.Wrapf(err, "error removing network namespace %s", nativeID)
	}
	return nil
}
<commit_msg>Rename nativeID to nsPath. Simplify.<commit_after>// +build linux

package cniprovider

import (
	"os"
	"path/filepath"
	"syscall"
	"unsafe"

	"github.com/containerd/containerd/oci"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

func createNetNS(c *cniProvider, id string) (string, error) {
	nsPath := filepath.Join(c.root, "net/cni", id)
	if err := os.MkdirAll(filepath.Dir(nsPath), 0700); err != nil {
		return "", err
	}

	f, err := os.Create(nsPath)
	if err != nil {
		deleteNetNS(nsPath)
		return "", err
	}
	if err := f.Close(); err != nil {
		deleteNetNS(nsPath)
		return "", err
	}
	procNetNSBytes, err := syscall.BytePtrFromString("/proc/self/ns/net")
	if err != nil {
		deleteNetNS(nsPath)
		return "", err
	}
	nsPathBytes, err := syscall.BytePtrFromString(nsPath)
	if err != nil {
		deleteNetNS(nsPath)
		return "", err
	}
	beforeFork()

	pid, _, errno := syscall.RawSyscall6(syscall.SYS_CLONE, uintptr(syscall.SIGCHLD)|unix.CLONE_NEWNET, 0, 0, 0, 0, 0)
	if errno != 0 {
		afterFork()
		deleteNetNS(nsPath)
		return "", errno
	}

	if pid != 0 {
		afterFork()
		var ws unix.WaitStatus
		_, err = unix.Wait4(int(pid), &ws, 0, nil)
		for err == syscall.EINTR {
			_, err = unix.Wait4(int(pid), &ws, 0, nil)
		}

		if err != nil {
			deleteNetNS(nsPath)
			return "", errors.Wrapf(err, "failed to find pid=%d process", pid)
		}
		errno = syscall.Errno(ws.ExitStatus())
		if errno != 0 {
			deleteNetNS(nsPath)
			return "", errors.Wrapf(errno, "failed to mount %s (pid=%d)", nsPath, pid)
		}
		return nsPath, nil
	}
	afterForkInChild()
	_, _, errno = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(procNetNSBytes)), uintptr(unsafe.Pointer(nsPathBytes)), 0, uintptr(unix.MS_BIND), 0, 0)
	syscall.RawSyscall(syscall.SYS_EXIT, uintptr(errno), 0, 0)
	panic("unreachable")
}

func setNetNS(s *specs.Spec, nsPath string) error {
	return oci.WithLinuxNamespace(specs.LinuxNamespace{
		Type: specs.NetworkNamespace,
		Path: nsPath,
	})(nil, nil, nil, s)
}

func unmountNetNS(nsPath string) error {
	if err := unix.Unmount(nsPath, unix.MNT_DETACH); err != nil {
		if err != syscall.EINVAL && err != syscall.ENOENT {
			return errors.Wrap(err, "error unmounting network namespace")
		}
	}
	return nil
}

func deleteNetNS(nsPath string) error {
	if err := os.Remove(nsPath); err != nil && !errors.Is(err, os.ErrNotExist) {
		return errors.Wrapf(err, "error removing network namespace %s", nsPath)
	}
	return nil
}
<|endoftext|>