<commit_before>package startCmd

import (
	// Stdlib
	"bytes"
	"fmt"
	"io"
	"os"
	"regexp"
	"text/tabwriter"

	// Internal
	"github.com/salsita/salsaflow/app"
	"github.com/salsita/salsaflow/config"
	"github.com/salsita/salsaflow/errs"
	"github.com/salsita/salsaflow/git"
	"github.com/salsita/salsaflow/log"
	"github.com/salsita/salsaflow/modules"
	"github.com/salsita/salsaflow/modules/common"
	"github.com/salsita/salsaflow/prompt"

	// Other
	"github.com/extemporalgenome/slug"
	"gopkg.in/tchap/gocli.v1"
)

var Command = &gocli.Command{
	UsageLine: `
  start`,
	Short: "start a new story",
	Long: `
  Starts a new story, i.e., create a new branch off ` + config.TrunkBranch + `
  or check out an existing branch containing the story id if there's already one.
  Then updates the story state in PM tool and sets you as the story owner.
	`,
	Action: run,
}

func run(cmd *gocli.Command, args []string) {
	if len(args) != 0 {
		cmd.Usage()
		os.Exit(2)
	}

	app.MustInit()

	if err := runMain(); err != nil {
		log.Fatalln("\nError: " + err.Error())
	}
}

func handleError(task string, err error, stderr *bytes.Buffer) error {
	errs.NewError(task, stderr, err).Log(log.V(log.Info))
	return err
}

func runMain() (err error) {
	var (
		selectedStory common.Story
		currentBranch string
	)

	defer func() {
		// Checkout the original branch.
		if currentBranch == "" {
			return
		}
		msg := fmt.Sprintf("Checkout the original branch (%s)", currentBranch)
		log.Run(msg)
		stderr, err := git.Checkout(currentBranch)
		if err != nil {
			handleError(msg, err, stderr)
			return
		}
	}()

	// Remember the current branch.
	msg := "Remember the current branch"
	log.Run(msg)
	currentBranch, stderr, err := git.CurrentBranch()
	if err != nil {
		return handleError(msg, err, stderr)
	}

	// Fetch stories from the issue tracker.
	msg = "Fetch stories from the issue tracker"
	log.Run(msg)
	stories, err := modules.GetIssueTracker().StartableStories()
	if err != nil {
		return handleError(msg, err, nil)
	}

	// List stories that can be started.
	tw := tabwriter.NewWriter(os.Stdout, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\nYou can start working on one of the following stories:\n\n")
	io.WriteString(tw, "Index\tStory ID\tStory Title\n")
	io.WriteString(tw, "=====\t========\t===========\n")
	for i, story := range stories {
		fmt.Fprintf(tw, "%v\t%v\t%v\n", i, story.ReadableId(), story.Title())
	}
	io.WriteString(tw, "\n")
	tw.Flush()

	// Prompt user to choose.
	index, err := prompt.PromptIndex("Choose a story by inserting its index: ", 0, len(stories)-1)
	if err != nil {
		log.Fatalln(err.Error())
	}
	fmt.Println()

	selectedStory = stories[index]

	// Fetch the remote repository.
	msg = "Fetch the remote repository"
	log.Run(msg)
	stderr, err = git.UpdateRemotes(config.OriginName)
	if err != nil {
		return handleError(msg, err, nil)
	}

	// Get all branches.
	msg = "Collect all story branches"
	localRefs, remoteRefs, stderr, err := git.ListStoryRefs()
	if err != nil {
		return handleError(msg, err, nil)
	}

	// Get all story branches connected to `selectedStory` id.
	msg = "Check existing branches"
	log.Run(msg)
	matchingBranches := map[string]struct{}{}

	stripRemotePrefixRe := regexp.MustCompile(".*/(story/.+/.+)$")
	for _, ref := range append(localRefs, remoteRefs...) {
		storyId, err := git.RefToStoryId(ref)
		if err != nil {
			return handleError(msg, err, nil)
		}
		if storyId == selectedStory.ReadableId() {
			// We found a matching story branch. Let's strip off the `remote/origin` etc
			// part so that we have just the branch name.
			branchName := stripRemotePrefixRe.ReplaceAllString(ref, "$1")
			matchingBranches[branchName] = struct{}{}
		}
	}

	if len(matchingBranches) == 1 {
		// There is only one branch => checkout and work on that one.
		for branch := range matchingBranches {
			log.Log("Found one existing story branch: " + branch)
			msg := "Checkout branch " + branch
			log.Run(msg)
			if stderr, err = git.Checkout(branch); err != nil {
				return handleError(msg, err, stderr)
			}
		}

	} else if len(matchingBranches) > 1 {
		// There are multiple branches. Let the user choose which one to work on.
		logger := log.V(log.Info)
		logger.Lock()
		logger.UnsafeLog("Found multiple existing story branches:")
		for branch := range matchingBranches {
			logger.UnsafeNewLine("  - " + branch)
		}
		logger.UnsafeNewLine("Please checkout one of them and work there.")
		logger.Unlock()

	} else {
		// There is no branch => create a new one.
		msg := "Prompt for the new story branch name"
		line, err := prompt.Prompt("Please insert branch slug: ")
		if err != nil {
			return handleError(msg, err, nil)
		}
		branchName := fmt.Sprintf("story/%s/%s", slug.Slug(line), selectedStory.ReadableId())
		ok, err := prompt.Confirm(
			fmt.Sprintf("The branch will be called '%s', OK?", branchName))
		if err != nil {
			return handleError(msg, err, nil)
		}
		if !ok {
			log.Fatalln("I will exit now. If you want to try again, just run me again!")
		}
		fmt.Println()

		createMsg := "Create branch " + branchName
		log.Run(msg)
		if _, stderr, err = git.Git("branch", branchName, config.TrunkBranch); err != nil {
			return handleError(msg, err, stderr)
		}

		msg = "Checkout branch " + branchName
		if _, stderr, err = git.Git("checkout", branchName); err != nil {
			return handleError(msg, err, stderr)
		}

		// Delete the newly created branch on rollback.
		defer func(msg string) {
			if err != nil {
				log.Rollback(msg)
				if stderr, err := git.Branch("-D", branchName); err != nil {
					handleError("Delete branch "+branchName, err, stderr)
				}
			}
		}(createMsg)
	}

	msg = fmt.Sprintf("Start the selected story (%v)", selectedStory.ReadableId())
	log.Run(msg)
	if err := selectedStory.Start(); err != nil {
		return handleError(msg, err, err.Stderr)
	}

	msg = "Set you as the story owner"
	log.Run(msg)
	user, err := modules.GetIssueTracker().CurrentUser()
	if err != nil {
		return handleError(msg, err, nil)
	}
	// TODO: We should update, not overwrite the owners.
	if err := selectedStory.SetOwners([]common.User{user}); err != nil {
		return handleError(msg, err, nil)
	}

	// Do not checkout the original branch, the story branch is active now.
	currentBranch = ""
	return nil
}
<commit_msg>story start: Make the output less verbose<commit_after>package startCmd

import (
	// Stdlib
	"bytes"
	"fmt"
	"io"
	"os"
	"regexp"
	"text/tabwriter"

	// Internal
	"github.com/salsita/salsaflow/app"
	"github.com/salsita/salsaflow/config"
	"github.com/salsita/salsaflow/errs"
	"github.com/salsita/salsaflow/git"
	"github.com/salsita/salsaflow/log"
	"github.com/salsita/salsaflow/modules"
	"github.com/salsita/salsaflow/modules/common"
	"github.com/salsita/salsaflow/prompt"

	// Other
	"github.com/extemporalgenome/slug"
	"gopkg.in/tchap/gocli.v1"
)

var Command = &gocli.Command{
	UsageLine: `
  start`,
	Short: "start a new story",
	Long: `
  Starts a new story, i.e., create a new branch off ` + config.TrunkBranch + `
  or check out an existing branch containing the story id if there's already one.
  Then updates the story state in PM tool and sets you as the story owner.
	`,
	Action: run,
}

func run(cmd *gocli.Command, args []string) {
	if len(args) != 0 {
		cmd.Usage()
		os.Exit(2)
	}

	app.MustInit()

	if err := runMain(); err != nil {
		log.Fatalln("\nError: " + err.Error())
	}
}

func handleError(task string, err error, stderr *bytes.Buffer) error {
	errs.NewError(task, stderr, err).Log(log.V(log.Info))
	return err
}

func runMain() (err error) {
	var (
		selectedStory common.Story
		currentBranch string
	)

	defer func() {
		// Checkout the original branch.
		if currentBranch == "" {
			return
		}
		msg := fmt.Sprintf("Checkout the original branch (%s)", currentBranch)
		log.Run(msg)
		stderr, err := git.Checkout(currentBranch)
		if err != nil {
			handleError(msg, err, stderr)
			return
		}
	}()

	// Remember the current branch.
	msg := "Remember the current branch"
	currentBranch, stderr, err := git.CurrentBranch()
	if err != nil {
		return handleError(msg, err, stderr)
	}

	// Fetch stories from the issue tracker.
	msg = "Fetch stories from the issue tracker"
	log.Run(msg)
	stories, err := modules.GetIssueTracker().StartableStories()
	if err != nil {
		return handleError(msg, err, nil)
	}

	// List stories that can be started.
	tw := tabwriter.NewWriter(os.Stdout, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\nYou can start working on one of the following stories:\n\n")
	io.WriteString(tw, "Index\tStory ID\tStory Title\n")
	io.WriteString(tw, "=====\t========\t===========\n")
	for i, story := range stories {
		fmt.Fprintf(tw, "%v\t%v\t%v\n", i, story.ReadableId(), story.Title())
	}
	io.WriteString(tw, "\n")
	tw.Flush()

	// Prompt user to choose.
	index, err := prompt.PromptIndex("Choose a story by inserting its index: ", 0, len(stories)-1)
	if err != nil {
		log.Fatalln(err.Error())
	}
	fmt.Println()

	selectedStory = stories[index]

	// Fetch the remote repository.
	msg = "Fetch the remote repository"
	log.Run(msg)
	stderr, err = git.UpdateRemotes(config.OriginName)
	if err != nil {
		return handleError(msg, err, nil)
	}

	// Get all branches.
	msg = "Collect all story branches"
	localRefs, remoteRefs, stderr, err := git.ListStoryRefs()
	if err != nil {
		return handleError(msg, err, nil)
	}

	// Get all story branches connected to `selectedStory` id.
	msg = "Check existing branches"
	log.Run(msg)
	matchingBranches := map[string]struct{}{}

	stripRemotePrefixRe := regexp.MustCompile(".*/(story/.+/.+)$")
	for _, ref := range append(localRefs, remoteRefs...) {
		storyId, err := git.RefToStoryId(ref)
		if err != nil {
			return handleError(msg, err, nil)
		}
		if storyId == selectedStory.ReadableId() {
			// We found a matching story branch. Let's strip off the `remote/origin` etc
			// part so that we have just the branch name.
			branchName := stripRemotePrefixRe.ReplaceAllString(ref, "$1")
			matchingBranches[branchName] = struct{}{}
		}
	}

	if len(matchingBranches) == 1 {
		// There is only one branch => checkout and work on that one.
		for branch := range matchingBranches {
			log.Log("Found one existing story branch: " + branch)
			msg := "Checkout branch " + branch
			log.Run(msg)
			if stderr, err = git.Checkout(branch); err != nil {
				return handleError(msg, err, stderr)
			}
		}

	} else if len(matchingBranches) > 1 {
		// There are multiple branches. Let the user choose which one to work on.
		logger := log.V(log.Info)
		logger.Lock()
		logger.UnsafeLog("Found multiple existing story branches:")
		for branch := range matchingBranches {
			logger.UnsafeNewLine("  - " + branch)
		}
		logger.UnsafeNewLine("Please checkout one of them and work there.")
		logger.Unlock()

	} else {
		// There is no branch => create a new one.
		msg := "Prompt for the new story branch name"
		line, err := prompt.Prompt("Please insert branch slug: ")
		if err != nil {
			return handleError(msg, err, nil)
		}
		branchName := fmt.Sprintf("story/%s/%s", slug.Slug(line), selectedStory.ReadableId())
		ok, err := prompt.Confirm(
			fmt.Sprintf("The branch will be called '%s', OK?", branchName))
		if err != nil {
			return handleError(msg, err, nil)
		}
		if !ok {
			log.Fatalln("I will exit now. If you want to try again, just run me again!")
		}
		fmt.Println()

		createMsg := "Create branch " + branchName
		log.Run(msg)
		if _, stderr, err = git.Git("branch", branchName, config.TrunkBranch); err != nil {
			return handleError(msg, err, stderr)
		}

		msg = "Checkout branch " + branchName
		if _, stderr, err = git.Git("checkout", branchName); err != nil {
			return handleError(msg, err, stderr)
		}

		// Delete the newly created branch on rollback.
		defer func(msg string) {
			if err != nil {
				log.Rollback(msg)
				if stderr, err := git.Branch("-D", branchName); err != nil {
					handleError("Delete branch "+branchName, err, stderr)
				}
			}
		}(createMsg)
	}

	msg = fmt.Sprintf("Start the selected story (%v)", selectedStory.ReadableId())
	log.Run(msg)
	if err := selectedStory.Start(); err != nil {
		return handleError(msg, err, err.Stderr)
	}

	msg = "Set you as the story owner"
	log.Run(msg)
	user, err := modules.GetIssueTracker().CurrentUser()
	if err != nil {
		return handleError(msg, err, nil)
	}
	// TODO: We should update, not overwrite the owners.
	if err := selectedStory.SetOwners([]common.User{user}); err != nil {
		return handleError(msg, err, nil)
	}

	// Do not checkout the original branch, the story branch is active now.
	currentBranch = ""
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2017 The LUCI 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 notify

import (
	"net/http"
	"time"

	"golang.org/x/net/context"

	"go.chromium.org/luci/common/api/gitiles"
	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	gitpb "go.chromium.org/luci/common/proto/git"
	gitilespb "go.chromium.org/luci/common/proto/gitiles"
	"go.chromium.org/luci/server/auth"
)

// HistoryFunc is a function that gets a list of commits from Gitiles for a
// specific repository, between oldRevision and newRevision, inclusive.
//
// If oldRevision is not reachable from newRevision, returns an empty slice
// and nil error.
type HistoryFunc func(c context.Context, host, project, oldRevision, newRevision string) ([]*gitpb.Commit, error)

// gitilesHistory is an implementation of a HistoryFunc intended to be used
// in production (not for testing).
func gitilesHistory(c context.Context, host, project, oldRevision, newRevision string) ([]*gitpb.Commit, error) {
	c, _ = context.WithTimeout(c, 30*time.Second)

	transport, err := auth.GetRPCTransport(c, auth.AsSelf, auth.WithScopes(gitiles.OAuthScope))
	if err != nil {
		return nil, errors.Annotate(err, "getting RPC Transport").Err()
	}
	client, err := gitiles.NewRESTClient(&http.Client{Transport: transport}, host, true)
	if err != nil {
		return nil, errors.Annotate(err, "creating Gitiles client").Err()
	}

	req := &gitilespb.LogRequest{
		Project: project,
		// With ~1, if newCommit.Revision == oldRevision,
		// we'll get one commit,
		// otherwise we will get 0 commits which is indistinguishable from
		// oldRevision not being reachable from newRevision.
		Ancestor: oldRevision + "~1",
		Treeish:  newRevision,
	}
	logging.Infof(c, "Gitiles request to host %q: %q", host, req)
	res, err := client.Log(c, req)
	switch {
	case err != nil:
		return nil, errors.Annotate(err, "fetching commit from Gitiles").Err()
	case len(res.Log) > 0 && res.Log[0].Id != newRevision: // Sanity check.
		return nil, errors.Reason("gitiles returned inconsistent results").Err()
	default:
		return res.Log, nil
	}
}
<commit_msg>[luci-notify] Use gitiles.PagingLog when retrieving commits<commit_after>// Copyright 2017 The LUCI 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 notify

import (
	"net/http"
	"time"

	"golang.org/x/net/context"

	"go.chromium.org/luci/common/api/gitiles"
	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	gitpb "go.chromium.org/luci/common/proto/git"
	gitilespb "go.chromium.org/luci/common/proto/gitiles"
	"go.chromium.org/luci/server/auth"
)

// HistoryFunc is a function that gets a list of commits from Gitiles for a
// specific repository, between oldRevision and newRevision, inclusive.
//
// If oldRevision is not reachable from newRevision, returns an empty slice
// and nil error.
type HistoryFunc func(c context.Context, host, project, oldRevision, newRevision string) ([]*gitpb.Commit, error)

// gitilesHistory is an implementation of a HistoryFunc intended to be used
// in production (not for testing).
func gitilesHistory(c context.Context, host, project, oldRevision, newRevision string) ([]*gitpb.Commit, error) {
	c, _ = context.WithTimeout(c, 30*time.Second)

	transport, err := auth.GetRPCTransport(c, auth.AsSelf, auth.WithScopes(gitiles.OAuthScope))
	if err != nil {
		return nil, errors.Annotate(err, "getting RPC Transport").Err()
	}
	client, err := gitiles.NewRESTClient(&http.Client{Transport: transport}, host, true)
	if err != nil {
		return nil, errors.Annotate(err, "creating Gitiles client").Err()
	}

	req := gitilespb.LogRequest{
		Project: project,
		// With ~1, if newCommit.Revision == oldRevision,
		// we'll get one commit,
		// otherwise we will get 0 commits which is indistinguishable from
		// oldRevision not being reachable from newRevision.
		Ancestor: oldRevision + "~1",
		Treeish:  newRevision,
	}
	logging.Infof(c, "Gitiles request to host %q: %q", host, &req)
	res, err := gitiles.PagingLog(c, client, req, 0)
	switch {
	case err != nil:
		return nil, errors.Annotate(err, "fetching commit from Gitiles").Err()
	case len(res) > 0 && res[0].Id != newRevision: // Sanity check.
		return nil, errors.Reason("gitiles returned inconsistent results").Err()
	default:
		return res, nil
	}
}
<|endoftext|>


<commit_before>package controlapi

import (
	"errors"
	"reflect"
	"strconv"

	"github.com/docker/engine-api/types/reference"
	"github.com/docker/swarmkit/api"
	"github.com/docker/swarmkit/identity"
	"github.com/docker/swarmkit/manager/scheduler"
	"github.com/docker/swarmkit/manager/state/store"
	"github.com/docker/swarmkit/protobuf/ptypes"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
)

var (
	errNetworkUpdateNotSupported = errors.New("changing network in service is not supported")
	errModeChangeNotAllowed      = errors.New("service mode change is not allowed")
)

func validateResources(r *api.Resources) error {
	if r == nil {
		return nil
	}

	if r.NanoCPUs != 0 && r.NanoCPUs < 1e6 {
		return grpc.Errorf(codes.InvalidArgument, "invalid cpu value %g: Must be at least %g", float64(r.NanoCPUs)/1e9, 1e6/1e9)
	}

	if r.MemoryBytes != 0 && r.MemoryBytes < 4*1024*1024 {
		return grpc.Errorf(codes.InvalidArgument, "invalid memory value %d: Must be at least 4MiB", r.MemoryBytes)
	}
	return nil
}

func validateResourceRequirements(r *api.ResourceRequirements) error {
	if r == nil {
		return nil
	}
	if err := validateResources(r.Limits); err != nil {
		return err
	}
	if err := validateResources(r.Reservations); err != nil {
		return err
	}
	return nil
}

func validateRestartPolicy(rp *api.RestartPolicy) error {
	if rp == nil {
		return nil
	}

	if rp.Delay != nil {
		delay, err := ptypes.Duration(rp.Delay)
		if err != nil {
			return err
		}
		if delay < 0 {
			return grpc.Errorf(codes.InvalidArgument, "TaskSpec: restart-delay cannot be negative")
		}
	}

	if rp.Window != nil {
		win, err := ptypes.Duration(rp.Window)
		if err != nil {
			return err
		}
		if win < 0 {
			return grpc.Errorf(codes.InvalidArgument, "TaskSpec: restart-window cannot be negative")
		}
	}

	return nil
}

func validatePlacement(placement *api.Placement) error {
	if placement == nil {
		return nil
	}
	_, err := scheduler.ParseExprs(placement.Constraints)
	return err
}

func validateUpdate(uc *api.UpdateConfig) error {
	if uc == nil {
		return nil
	}

	delay, err := ptypes.Duration(&uc.Delay)
	if err != nil {
		return err
	}

	if delay < 0 {
		return grpc.Errorf(codes.InvalidArgument, "TaskSpec: update-delay cannot be negative")
	}

	return nil
}

func validateTask(taskSpec api.TaskSpec) error {
	if err := validateResourceRequirements(taskSpec.Resources); err != nil {
		return err
	}

	if err := validateRestartPolicy(taskSpec.Restart); err != nil {
		return err
	}

	if err := validatePlacement(taskSpec.Placement); err != nil {
		return err
	}

	if taskSpec.GetRuntime() == nil {
		return grpc.Errorf(codes.InvalidArgument, "TaskSpec: missing runtime")
	}

	_, ok := taskSpec.GetRuntime().(*api.TaskSpec_Container)
	if !ok {
		return grpc.Errorf(codes.Unimplemented, "RuntimeSpec: unimplemented runtime in service spec")
	}

	container := taskSpec.GetContainer()
	if container == nil {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: missing in service spec")
	}

	if container.Image == "" {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: image reference must be provided")
	}

	if _, _, err := reference.Parse(container.Image); err != nil {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: %q is not a valid repository/tag", container.Image)
	}
	return nil
}

func validateEndpointSpec(epSpec *api.EndpointSpec) error {
	// Endpoint spec is optional
	if epSpec == nil {
		return nil
	}

	if len(epSpec.Ports) > 0 && epSpec.Mode == api.ResolutionModeDNSRoundRobin {
		return grpc.Errorf(codes.InvalidArgument, "EndpointSpec: ports can't be used with dnsrr mode")
	}

	portSet := make(map[api.PortConfig]struct{})
	for _, port := range epSpec.Ports {
		if _, ok := portSet[*port]; ok {
			return grpc.Errorf(codes.InvalidArgument, "EndpointSpec: duplicate ports provided")
		}

		portSet[*port] = struct{}{}
	}

	return nil
}

func validateServiceSpec(spec *api.ServiceSpec) error {
	if spec == nil {
		return grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}
	if err := validateAnnotations(spec.Annotations); err != nil {
		return err
	}
	if err := validateTask(spec.Task); err != nil {
		return err
	}
	if err := validateUpdate(spec.Update); err != nil {
		return err
	}
	if err := validateEndpointSpec(spec.Endpoint); err != nil {
		return err
	}
	return nil
}

// checkPortConflicts does a best effort to find if the passed in spec has port
// conflicts with existing services.
func (s *Server) checkPortConflicts(spec *api.ServiceSpec) error {
	if spec.Endpoint == nil {
		return nil
	}

	pcToString := func(pc *api.PortConfig) string {
		port := strconv.FormatUint(uint64(pc.PublishedPort), 10)
		return port + "/" + pc.Protocol.String()
	}

	reqPorts := make(map[string]bool)
	for _, pc := range spec.Endpoint.Ports {
		if pc.PublishedPort > 0 {
			reqPorts[pcToString(pc)] = true
		}
	}
	if len(reqPorts) == 0 {
		return nil
	}

	var (
		services []*api.Service
		err      error
	)

	s.store.View(func(tx store.ReadTx) {
		services, err = store.FindServices(tx, store.All)
	})
	if err != nil {
		return err
	}

	for _, service := range services {
		if service.Spec.Endpoint != nil {
			for _, pc := range service.Spec.Endpoint.Ports {
				if reqPorts[pcToString(pc)] {
					return grpc.Errorf(codes.InvalidArgument, "port '%d' is already in use by service '%s' (%s)", pc.PublishedPort, service.Spec.Annotations.Name, service.ID)
				}
			}
		}
		if service.Endpoint != nil {
			for _, pc := range service.Endpoint.Ports {
				if reqPorts[pcToString(pc)] {
					return grpc.Errorf(codes.InvalidArgument, "port '%d' is already in use by service '%s' (%s)", pc.PublishedPort, service.Spec.Annotations.Name, service.ID)
				}
			}
		}
	}
	return nil
}

// CreateService creates and return a Service based on the provided ServiceSpec.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns `AlreadyExists` if the ServiceID conflicts.
// - Returns an error if the creation fails.
func (s *Server) CreateService(ctx context.Context, request *api.CreateServiceRequest) (*api.CreateServiceResponse, error) {
	if err := validateServiceSpec(request.Spec); err != nil {
		return nil, err
	}

	if err := s.checkPortConflicts(request.Spec); err != nil {
		return nil, err
	}

	// TODO(aluzzardi): Consider using `Name` as a primary key to handle
	// duplicate creations. See #65
	service := &api.Service{
		ID:   identity.NewID(),
		Spec: *request.Spec,
	}

	err := s.store.Update(func(tx store.Tx) error {
		return store.CreateService(tx, service)
	})
	if err != nil {
		return nil, err
	}

	return &api.CreateServiceResponse{
		Service: service,
	}, nil
}

// GetService returns a Service given a ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
func (s *Server) GetService(ctx context.Context, request *api.GetServiceRequest) (*api.GetServiceResponse, error) {
	if request.ServiceID == "" {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}

	var service *api.Service
	s.store.View(func(tx store.ReadTx) {
		service = store.GetService(tx, request.ServiceID)
	})
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}

	return &api.GetServiceResponse{
		Service: service,
	}, nil
}

// UpdateService updates a Service referenced by ServiceID with the given ServiceSpec.
// - Returns `NotFound` if the Service is not found.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns an error if the update fails.
func (s *Server) UpdateService(ctx context.Context, request *api.UpdateServiceRequest) (*api.UpdateServiceResponse, error) {
	if request.ServiceID == "" || request.ServiceVersion == nil {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}
	if err := validateServiceSpec(request.Spec); err != nil {
		return nil, err
	}

	var service *api.Service
	s.store.View(func(tx store.ReadTx) {
		service = store.GetService(tx, request.ServiceID)
	})
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}

	if request.Spec.Endpoint != nil && !reflect.DeepEqual(request.Spec.Endpoint, service.Spec.Endpoint) {
		if err := s.checkPortConflicts(request.Spec); err != nil {
			return nil, err
		}
	}

	err := s.store.Update(func(tx store.Tx) error {
		service = store.GetService(tx, request.ServiceID)
		if service == nil {
			return nil
		}
		// temporary disable network update
		if request.Spec != nil && !reflect.DeepEqual(request.Spec.Networks, service.Spec.Networks) {
			return errNetworkUpdateNotSupported
		}

		// orchestrator is designed to be stateless, so it should not deal
		// with service mode change (comparing current config with previous config).
		// proper way to change service mode is to delete and re-add.
		if request.Spec != nil && reflect.TypeOf(service.Spec.Mode) != reflect.TypeOf(request.Spec.Mode) {
			return errModeChangeNotAllowed
		}
		service.Meta.Version = *request.ServiceVersion
		service.Spec = *request.Spec.Copy()

		// Reset update status
		service.UpdateStatus = nil

		return store.UpdateService(tx, service)
	})
	if err != nil {
		return nil, err
	}
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}
	return &api.UpdateServiceResponse{
		Service: service,
	}, nil
}

// RemoveService removes a Service referenced by ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
// - Returns an error if the deletion fails.
func (s *Server) RemoveService(ctx context.Context, request *api.RemoveServiceRequest) (*api.RemoveServiceResponse, error) {
	if request.ServiceID == "" {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}

	err := s.store.Update(func(tx store.Tx) error {
		return store.DeleteService(tx, request.ServiceID)
	})
	if err != nil {
		if err == store.ErrNotExist {
			return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
		}
		return nil, err
	}
	return &api.RemoveServiceResponse{}, nil
}

func filterServices(candidates []*api.Service, filters ...func(*api.Service) bool) []*api.Service {
	result := []*api.Service{}

	for _, c := range candidates {
		match := true
		for _, f := range filters {
			if !f(c) {
				match = false
				break
			}
		}
		if match {
			result = append(result, c)
		}
	}

	return result
}

// ListServices returns a list of all services.
func (s *Server) ListServices(ctx context.Context, request *api.ListServicesRequest) (*api.ListServicesResponse, error) {
	var (
		services []*api.Service
		err      error
	)

	s.store.View(func(tx store.ReadTx) {
		switch {
		case request.Filters != nil && len(request.Filters.Names) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByName, request.Filters.Names))
		case request.Filters != nil && len(request.Filters.NamePrefixes) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByNamePrefix, request.Filters.NamePrefixes))
		case request.Filters != nil && len(request.Filters.IDPrefixes) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByIDPrefix, request.Filters.IDPrefixes))
		default:
			services, err = store.FindServices(tx, store.All)
		}
	})
	if err != nil {
		return nil, err
	}

	if request.Filters != nil {
		services = filterServices(services,
			func(e *api.Service) bool {
				return filterContains(e.Spec.Annotations.Name, request.Filters.Names)
			},
			func(e *api.Service) bool {
				return filterContainsPrefix(e.Spec.Annotations.Name, request.Filters.NamePrefixes)
			},
			func(e *api.Service) bool {
				return filterContainsPrefix(e.ID, request.Filters.IDPrefixes)
			},
			func(e *api.Service) bool {
				return filterMatchLabels(e.Spec.Annotations.Labels, request.Filters.Labels)
			},
		)
	}

	return &api.ListServicesResponse{
		Services: services,
	}, nil
}
<commit_msg>Skip same service PortConfig check to allow PortConfig updating<commit_after>package controlapi

import (
	"errors"
	"reflect"
	"strconv"

	"github.com/docker/engine-api/types/reference"
	"github.com/docker/swarmkit/api"
	"github.com/docker/swarmkit/identity"
	"github.com/docker/swarmkit/manager/scheduler"
	"github.com/docker/swarmkit/manager/state/store"
	"github.com/docker/swarmkit/protobuf/ptypes"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
)

var (
	errNetworkUpdateNotSupported = errors.New("changing network in service is not supported")
	errModeChangeNotAllowed      = errors.New("service mode change is not allowed")
)

func validateResources(r *api.Resources) error {
	if r == nil {
		return nil
	}

	if r.NanoCPUs != 0 && r.NanoCPUs < 1e6 {
		return grpc.Errorf(codes.InvalidArgument, "invalid cpu value %g: Must be at least %g", float64(r.NanoCPUs)/1e9, 1e6/1e9)
	}

	if r.MemoryBytes != 0 && r.MemoryBytes < 4*1024*1024 {
		return grpc.Errorf(codes.InvalidArgument, "invalid memory value %d: Must be at least 4MiB", r.MemoryBytes)
	}
	return nil
}

func validateResourceRequirements(r *api.ResourceRequirements) error {
	if r == nil {
		return nil
	}
	if err := validateResources(r.Limits); err != nil {
		return err
	}
	if err := validateResources(r.Reservations); err != nil {
		return err
	}
	return nil
}

func validateRestartPolicy(rp *api.RestartPolicy) error {
	if rp == nil {
		return nil
	}

	if rp.Delay != nil {
		delay, err := ptypes.Duration(rp.Delay)
		if err != nil {
			return err
		}
		if delay < 0 {
			return grpc.Errorf(codes.InvalidArgument, "TaskSpec: restart-delay cannot be negative")
		}
	}

	if rp.Window != nil {
		win, err := ptypes.Duration(rp.Window)
		if err != nil {
			return err
		}
		if win < 0 {
			return grpc.Errorf(codes.InvalidArgument, "TaskSpec: restart-window cannot be negative")
		}
	}

	return nil
}

func validatePlacement(placement *api.Placement) error {
	if placement == nil {
		return nil
	}
	_, err := scheduler.ParseExprs(placement.Constraints)
	return err
}

func validateUpdate(uc *api.UpdateConfig) error {
	if uc == nil {
		return nil
	}

	delay, err := ptypes.Duration(&uc.Delay)
	if err != nil {
		return err
	}

	if delay < 0 {
		return grpc.Errorf(codes.InvalidArgument, "TaskSpec: update-delay cannot be negative")
	}

	return nil
}

func validateTask(taskSpec api.TaskSpec) error {
	if err := validateResourceRequirements(taskSpec.Resources); err != nil {
		return err
	}

	if err := validateRestartPolicy(taskSpec.Restart); err != nil {
		return err
	}

	if err := validatePlacement(taskSpec.Placement); err != nil {
		return err
	}

	if taskSpec.GetRuntime() == nil {
		return grpc.Errorf(codes.InvalidArgument, "TaskSpec: missing runtime")
	}

	_, ok := taskSpec.GetRuntime().(*api.TaskSpec_Container)
	if !ok {
		return grpc.Errorf(codes.Unimplemented, "RuntimeSpec: unimplemented runtime in service spec")
	}

	container := taskSpec.GetContainer()
	if container == nil {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: missing in service spec")
	}

	if container.Image == "" {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: image reference must be provided")
	}

	if _, _, err := reference.Parse(container.Image); err != nil {
		return grpc.Errorf(codes.InvalidArgument, "ContainerSpec: %q is not a valid repository/tag", container.Image)
	}
	return nil
}

func validateEndpointSpec(epSpec *api.EndpointSpec) error {
	// Endpoint spec is optional
	if epSpec == nil {
		return nil
	}

	if len(epSpec.Ports) > 0 && epSpec.Mode == api.ResolutionModeDNSRoundRobin {
		return grpc.Errorf(codes.InvalidArgument, "EndpointSpec: ports can't be used with dnsrr mode")
	}

	portSet := make(map[api.PortConfig]struct{})
	for _, port := range epSpec.Ports {
		if _, ok := portSet[*port]; ok {
			return grpc.Errorf(codes.InvalidArgument, "EndpointSpec: duplicate ports provided")
		}

		portSet[*port] = struct{}{}
	}

	return nil
}

func validateServiceSpec(spec *api.ServiceSpec) error {
	if spec == nil {
		return grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}
	if err := validateAnnotations(spec.Annotations); err != nil {
		return err
	}
	if err := validateTask(spec.Task); err != nil {
		return err
	}
	if err := validateUpdate(spec.Update); err != nil {
		return err
	}
	if err := validateEndpointSpec(spec.Endpoint); err != nil {
		return err
	}
	return nil
}

// checkPortConflicts does a best effort to find if the passed in spec has port
// conflicts with existing services.
func (s *Server) checkPortConflicts(spec *api.ServiceSpec, serviceID string) error {
	if spec.Endpoint == nil {
		return nil
	}

	pcToString := func(pc *api.PortConfig) string {
		port := strconv.FormatUint(uint64(pc.PublishedPort), 10)
		return port + "/" + pc.Protocol.String()
	}

	reqPorts := make(map[string]bool)
	for _, pc := range spec.Endpoint.Ports {
		if pc.PublishedPort > 0 {
			reqPorts[pcToString(pc)] = true
		}
	}
	if len(reqPorts) == 0 {
		return nil
	}

	var (
		services []*api.Service
		err      error
	)

	s.store.View(func(tx store.ReadTx) {
		services, err = store.FindServices(tx, store.All)
	})
	if err != nil {
		return err
	}

	for _, service := range services {
		// If service ID is the same (and not "") then this is an update
		if serviceID != "" && serviceID == service.ID {
			continue
		}
		if service.Spec.Endpoint != nil {
			for _, pc := range service.Spec.Endpoint.Ports {
				if reqPorts[pcToString(pc)] {
					return grpc.Errorf(codes.InvalidArgument, "port '%d' is already in use by service '%s' (%s)", pc.PublishedPort, service.Spec.Annotations.Name, service.ID)
				}
			}
		}
		if service.Endpoint != nil {
			for _, pc := range service.Endpoint.Ports {
				if reqPorts[pcToString(pc)] {
					return grpc.Errorf(codes.InvalidArgument, "port '%d' is already in use by service '%s' (%s)", pc.PublishedPort, service.Spec.Annotations.Name, service.ID)
				}
			}
		}
	}
	return nil
}

// CreateService creates and return a Service based on the provided ServiceSpec.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns `AlreadyExists` if the ServiceID conflicts.
// - Returns an error if the creation fails.
func (s *Server) CreateService(ctx context.Context, request *api.CreateServiceRequest) (*api.CreateServiceResponse, error) {
	if err := validateServiceSpec(request.Spec); err != nil {
		return nil, err
	}

	if err := s.checkPortConflicts(request.Spec, ""); err != nil {
		return nil, err
	}

	// TODO(aluzzardi): Consider using `Name` as a primary key to handle
	// duplicate creations. See #65
	service := &api.Service{
		ID:   identity.NewID(),
		Spec: *request.Spec,
	}

	err := s.store.Update(func(tx store.Tx) error {
		return store.CreateService(tx, service)
	})
	if err != nil {
		return nil, err
	}

	return &api.CreateServiceResponse{
		Service: service,
	}, nil
}

// GetService returns a Service given a ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
func (s *Server) GetService(ctx context.Context, request *api.GetServiceRequest) (*api.GetServiceResponse, error) {
	if request.ServiceID == "" {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}

	var service *api.Service
	s.store.View(func(tx store.ReadTx) {
		service = store.GetService(tx, request.ServiceID)
	})
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}

	return &api.GetServiceResponse{
		Service: service,
	}, nil
}

// UpdateService updates a Service referenced by ServiceID with the given ServiceSpec.
// - Returns `NotFound` if the Service is not found.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns an error if the update fails.
func (s *Server) UpdateService(ctx context.Context, request *api.UpdateServiceRequest) (*api.UpdateServiceResponse, error) {
	if request.ServiceID == "" || request.ServiceVersion == nil {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}
	if err := validateServiceSpec(request.Spec); err != nil {
		return nil, err
	}

	var service *api.Service
	s.store.View(func(tx store.ReadTx) {
		service = store.GetService(tx, request.ServiceID)
	})
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}

	if request.Spec.Endpoint != nil && !reflect.DeepEqual(request.Spec.Endpoint, service.Spec.Endpoint) {
		if err := s.checkPortConflicts(request.Spec, request.ServiceID); err != nil {
			return nil, err
		}
	}

	err := s.store.Update(func(tx store.Tx) error {
		service = store.GetService(tx, request.ServiceID)
		if service == nil {
			return nil
		}
		// temporary disable network update
		if request.Spec != nil && !reflect.DeepEqual(request.Spec.Networks, service.Spec.Networks) {
			return errNetworkUpdateNotSupported
		}

		// orchestrator is designed to be stateless, so it should not deal
		// with service mode change (comparing current config with previous config).
		// proper way to change service mode is to delete and re-add.
		if request.Spec != nil && reflect.TypeOf(service.Spec.Mode) != reflect.TypeOf(request.Spec.Mode) {
			return errModeChangeNotAllowed
		}
		service.Meta.Version = *request.ServiceVersion
		service.Spec = *request.Spec.Copy()

		// Reset update status
		service.UpdateStatus = nil

		return store.UpdateService(tx, service)
	})
	if err != nil {
		return nil, err
	}
	if service == nil {
		return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
	}
	return &api.UpdateServiceResponse{
		Service: service,
	}, nil
}

// RemoveService removes a Service referenced by ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
// - Returns an error if the deletion fails.
func (s *Server) RemoveService(ctx context.Context, request *api.RemoveServiceRequest) (*api.RemoveServiceResponse, error) {
	if request.ServiceID == "" {
		return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
	}

	err := s.store.Update(func(tx store.Tx) error {
		return store.DeleteService(tx, request.ServiceID)
	})
	if err != nil {
		if err == store.ErrNotExist {
			return nil, grpc.Errorf(codes.NotFound, "service %s not found", request.ServiceID)
		}
		return nil, err
	}
	return &api.RemoveServiceResponse{}, nil
}

func filterServices(candidates []*api.Service, filters ...func(*api.Service) bool) []*api.Service {
	result := []*api.Service{}

	for _, c := range candidates {
		match := true
		for _, f := range filters {
			if !f(c) {
				match = false
				break
			}
		}
		if match {
			result = append(result, c)
		}
	}

	return result
}

// ListServices returns a list of all services.
func (s *Server) ListServices(ctx context.Context, request *api.ListServicesRequest) (*api.ListServicesResponse, error) {
	var (
		services []*api.Service
		err      error
	)

	s.store.View(func(tx store.ReadTx) {
		switch {
		case request.Filters != nil && len(request.Filters.Names) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByName, request.Filters.Names))
		case request.Filters != nil && len(request.Filters.NamePrefixes) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByNamePrefix, request.Filters.NamePrefixes))
		case request.Filters != nil && len(request.Filters.IDPrefixes) > 0:
			services, err = store.FindServices(tx, buildFilters(store.ByIDPrefix, request.Filters.IDPrefixes))
		default:
			services, err = store.FindServices(tx, store.All)
		}
	})
	if err != nil {
		return nil, err
	}

	if request.Filters != nil {
		services = filterServices(services,
			func(e *api.Service) bool {
				return filterContains(e.Spec.Annotations.Name, request.Filters.Names)
			},
			func(e *api.Service) bool {
				return filterContainsPrefix(e.Spec.Annotations.Name, request.Filters.NamePrefixes)
			},
			func(e *api.Service) bool {
				return filterContainsPrefix(e.ID, request.Filters.IDPrefixes)
			},
			func(e *api.Service) bool {
				return filterMatchLabels(e.Spec.Annotations.Labels, request.Filters.Labels)
			},
		)
	}

	return &api.ListServicesResponse{
		Services: services,
	}, nil
}
<|endoftext|>


<commit_before>package config

import (
	"reflect"
	"testing"

	"github.com/mitchellh/reflectwalk"
)

func TestInterpolationWalker_detect(t *testing.T) {
	cases := []struct {
		Input  interface{}
		Result []Interpolation
	}{
		{
			Input: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Result: nil,
		},

		{
			Input: map[string]interface{}{
				"foo": "${var.foo}",
			},
			Result: []Interpolation{
				&VariableInterpolation{
					Variable: &UserVariable{
						Name: "foo",
						key:  "var.foo",
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": "${aws_instance.foo.*.num}",
			},
			Result: []Interpolation{
				&VariableInterpolation{
					Variable: &ResourceVariable{
						Type:  "aws_instance",
						Name:  "foo",
						Field: "num",

						Multi: true,
						Index: -1,

						key: "aws_instance.foo.*.num",
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": "${lookup(var.foo)}",
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&VariableInterpolation{
							Variable: &UserVariable{
								Name: "foo",
								key:  "var.foo",
							},
						},
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": `${file("test.txt")}`,
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&LiteralInterpolation{
							Literal: "test.txt",
						},
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": `${file("foo/bar.txt")}`,
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&LiteralInterpolation{
							Literal: "foo/bar.txt",
						},
					},
				},
			},
		},
	}

	for i, tc := range cases {
		var actual []Interpolation

		detectFn := func(i Interpolation) (string, error) {
			actual = append(actual, i)
			return "", nil
		}

		w := &interpolationWalker{F: detectFn}
		if err := reflectwalk.Walk(tc.Input, w); err != nil {
			t.Fatalf("err: %s", err)
		}

		for _, a := range actual {
			// This is jank, but reflect.DeepEqual never has functions
			// being the same.
			if f, ok := a.(*FunctionInterpolation); ok {
				f.Func = nil
			}
		}

		if !reflect.DeepEqual(actual, tc.Result) {
			t.Fatalf("%d: bad:\n\n%#v", i, actual)
		}
	}
}

func TestInterpolationWalker_replace(t *testing.T) {
	cases := []struct {
		Input  interface{}
		Output interface{}
		Value  string
	}{
		{
			Input: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Output: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": "hello, ${var.foo}",
			},
			Output: map[string]interface{}{
				"foo": "hello, bar",
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": map[string]interface{}{
					"${var.foo}": "bar",
				},
			},
			Output: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": "bar",
				},
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": []interface{}{
					"${var.foo}",
					"bing",
				},
			},
			Output: map[string]interface{}{
				"foo": []interface{}{
					"bar",
					"baz",
					"bing",
				},
			},
			Value: "bar,baz",
		},
	}

	for i, tc := range cases {
		fn := func(i Interpolation) (string, error) {
			return tc.Value, nil
		}

		w := &interpolationWalker{F: fn, Replace: true}
		if err := reflectwalk.Walk(tc.Input, w); err != nil {
			t.Fatalf("err: %s", err)
		}

		if !reflect.DeepEqual(tc.Input, tc.Output) {
			t.Fatalf("%d: bad:\n\n%#v", i, tc.Input)
		}
	}
}
<commit_msg>config: make the tests more robust<commit_after>package config

import (
	"reflect"
	"testing"

	"github.com/mitchellh/reflectwalk"
)

func TestInterpolationWalker_detect(t *testing.T) {
	cases := []struct {
		Input  interface{}
		Result []Interpolation
	}{
		{
			Input: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Result: nil,
		},

		{
			Input: map[string]interface{}{
				"foo": "${var.foo}",
			},
			Result: []Interpolation{
				&VariableInterpolation{
					Variable: &UserVariable{
						Name: "foo",
						key:  "var.foo",
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": "${aws_instance.foo.*.num}",
			},
			Result: []Interpolation{
				&VariableInterpolation{
					Variable: &ResourceVariable{
						Type:  "aws_instance",
						Name:  "foo",
						Field: "num",

						Multi: true,
						Index: -1,

						key: "aws_instance.foo.*.num",
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": "${lookup(var.foo)}",
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&VariableInterpolation{
							Variable: &UserVariable{
								Name: "foo",
								key:  "var.foo",
							},
						},
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": `${file("test.txt")}`,
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&LiteralInterpolation{
							Literal: "test.txt",
						},
					},
				},
			},
		},

		{
			Input: map[string]interface{}{
				"foo": `${file("foo/bar.txt")}`,
			},
			Result: []Interpolation{
				&FunctionInterpolation{
					Func: nil,
					Args: []Interpolation{
						&LiteralInterpolation{
							Literal: "foo/bar.txt",
						},
					},
				},
			},
		},
	}

	for i, tc := range cases {
		var actual []Interpolation

		detectFn := func(i Interpolation) (string, error) {
			actual = append(actual, i)
			return "", nil
		}

		w := &interpolationWalker{F: detectFn}
		if err := reflectwalk.Walk(tc.Input, w); err != nil {
			t.Fatalf("err: %s", err)
		}

		for _, a := range actual {
			// This is jank, but reflect.DeepEqual never has functions
			// being the same.
			if f, ok := a.(*FunctionInterpolation); ok {
				f.Func = nil
			}
		}

		if !reflect.DeepEqual(actual, tc.Result) {
			t.Fatalf("%d: bad:\n\n%#v", i, actual)
		}
	}
}

func TestInterpolationWalker_replace(t *testing.T) {
	cases := []struct {
		Input  interface{}
		Output interface{}
		Value  string
	}{
		{
			Input: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Output: map[string]interface{}{
				"foo": "$${var.foo}",
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": "hello, ${var.foo}",
			},
			Output: map[string]interface{}{
				"foo": "hello, bar",
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": map[string]interface{}{
					"${var.foo}": "bar",
				},
			},
			Output: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": "bar",
				},
			},
			Value: "bar",
		},

		{
			Input: map[string]interface{}{
				"foo": []interface{}{
					"${var.foo}",
					"bing",
				},
			},
			Output: map[string]interface{}{
				"foo": []interface{}{
					"bar",
					"baz",
					"bing",
				},
			},
			Value: "bar" + InterpSplitDelim + "baz",
		},
	}

	for i, tc := range cases {
		fn := func(i Interpolation) (string, error) {
			return tc.Value, nil
		}

		w := &interpolationWalker{F: fn, Replace: true}
		if err := reflectwalk.Walk(tc.Input, w); err != nil {
			t.Fatalf("err: %s", err)
		}

		if !reflect.DeepEqual(tc.Input, tc.Output) {
			t.Fatalf("%d: bad:\n\n%#v", i, tc.Input)
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 Istio 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 config handles configuration ingestion and processing.
// Validator
// 1. Accepts new configuration from user
// 2. Validates configuration
// 3. Produces a "ValidatedConfig"
// Runtime
// 1. It is validated and actionable configuration
// 2. It resolves the configuration to a list of Combined {aspect, adapter} configs
//    given an attribute.Bag.
// 3. Combined config has complete information needed to dispatch aspect
package config

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/ghodss/yaml"
	"github.com/gogo/protobuf/jsonpb"
	"github.com/golang/protobuf/proto"

	"istio.io/mixer/pkg/adapter"
	"istio.io/mixer/pkg/config/descriptor"
	pb "istio.io/mixer/pkg/config/proto"
	"istio.io/mixer/pkg/expr"
)

type (
	// AspectParams describes configuration parameters for an aspect.
	AspectParams proto.Message

	// AspectValidator describes a type that is able to validate Aspect configuration.
	AspectValidator interface {
		// DefaultConfig returns a default configuration struct for this
		// adapter. This will be used by the configuration system to establish
		// the shape of the block of configuration state passed to the NewAspect method.
		DefaultConfig() (c AspectParams)

		// ValidateConfig determines whether the given configuration meets all correctness requirements.
		ValidateConfig(c AspectParams, validator expr.Validator, finder descriptor.Finder) *adapter.ConfigErrors
	}

	// AdapterValidatorFinder is used to find specific underlying validators.
	// Manager registry and adapter registry should implement this interface
	// so ConfigValidators can be uniformly accessed.
	AdapterValidatorFinder func(name string) (adapter.ConfigValidator, bool)

	// AspectValidatorFinder is used to find specific underlying validators.
	// Manager registry and adapter registry should implement this interface
	// so ConfigValidators can be uniformly accessed.
	AspectValidatorFinder func(name string) (AspectValidator, bool)

	// AdapterToAspectMapper given an pb.Adapter.Impl
	// This is specifically *not* querying by pb.Adapter.Name
	// returns a list of aspects the adapter provides.
	// For many adapter this will be exactly 1 aspect.
	AdapterToAspectMapper func(impl string) []string
)

// NewValidator returns a validator given component validators.
func NewValidator(managerFinder AspectValidatorFinder, adapterFinder AdapterValidatorFinder,
	findAspects AdapterToAspectMapper, strict bool, exprValidator expr.Validator) *Validator {
	return &Validator{
		managerFinder: managerFinder,
		adapterFinder: adapterFinder,
		findAspects:   findAspects,
		strict:        strict,
		exprValidator: exprValidator,
		validated:     &Validated{},
	}
}

type (
	// Validator is the Configuration validator.
	Validator struct {
		managerFinder    AspectValidatorFinder
		adapterFinder    AdapterValidatorFinder
		findAspects      AdapterToAspectMapper
		descriptorFinder descriptor.Finder
		strict           bool
		exprValidator    expr.Validator
		validated        *Validated
	}

	adapterKey struct {
		kind string
		name string
	}
	// Validated store validated configuration.
	// It has been validated as internally consistent and correct.
	Validated struct {
		adapterByName map[adapterKey]*pb.Adapter
		globalConfig  *pb.GlobalConfig
		serviceConfig *pb.ServiceConfig
		numAspects    int
	}
)

func (a adapterKey) String() string {
	return fmt.Sprintf("%s//%s", a.kind, a.name)
}

// validateGlobalConfig consumes a yml config string with adapter config.
// It is validated in presence of validators.
func (p *Validator) validateGlobalConfig(cfg string) (ce *adapter.ConfigErrors) {
	var err error
	m := &pb.GlobalConfig{}

	if err = yaml.Unmarshal([]byte(cfg), m); err != nil {
		ce = ce.Append("AdapterConfig", err)
		return
	}
	p.validated.adapterByName = make(map[adapterKey]*pb.Adapter)
	var acfg adapter.Config
	for _, aa := range m.GetAdapters() {
		if acfg, err = ConvertAdapterParams(p.adapterFinder, aa.Impl, aa.Params, p.strict); err != nil {
			ce = ce.Append("Adapter: "+aa.Impl, err)
			continue
		}
		aa.Params = acfg
		// check which kinds aa.Impl provides
		// Then register it for all of them.
		for _, kind := range p.findAspects(aa.Impl) {
			p.validated.adapterByName[adapterKey{kind, aa.Name}] = aa
		}
	}
	p.validated.globalConfig = m
	return
}

// ValidateSelector ensures that the selector is valid per expression language.
func (p *Validator) validateSelector(selector string) (err error) {
	// empty selector always selects
	if len(selector) == 0 {
		return nil
	}
	return p.exprValidator.Validate(selector)
}

// validateAspectRules validates the recursive configuration data structure.
// It is primarily used by validate ServiceConfig.
func (p *Validator) validateAspectRules(rules []*pb.AspectRule, path string, validatePresence bool) (ce *adapter.ConfigErrors) {
	var acfg adapter.Config
	var err error
	for _, rule := range rules {
		if err = p.validateSelector(rule.GetSelector()); err != nil {
			ce = ce.Append(path+":Selector "+rule.GetSelector(), err)
		}
		path = path + "/" + rule.GetSelector()
		for idx, aa := range rule.GetAspects() {
			if acfg, err = ConvertAspectParams(p.managerFinder, aa.Kind, aa.GetParams(), p.strict, p.descriptorFinder); err != nil {
				ce = ce.Append(fmt.Sprintf("%s:%s[%d]", path, aa.Kind, idx), err)
				continue
			}
			aa.Params = acfg
			p.validated.numAspects++
			if validatePresence {
				if aa.Adapter == "" {
					aa.Adapter = "default"
				}
				// ensure that aa.Kind has a registered adapter
				ak := adapterKey{aa.Kind, aa.Adapter}
				if p.validated.adapterByName[ak] == nil {
					ce = ce.Appendf("NamedAdapter", "%s not available", ak)
				}
			}
		}
		rs := rule.GetRules()
		if len(rs) == 0 {
			continue
		}
		if verr := p.validateAspectRules(rs, path, validatePresence); verr != nil {
			ce = ce.Extend(verr)
		}
	}
	return ce
}

// Validate validates a single serviceConfig and globalConfig together.
// It returns a fully validated Config if no errors are found.
func (p *Validator) Validate(serviceCfg string, globalCfg string) (rt *Validated, ce *adapter.ConfigErrors) {
	var cerr *adapter.ConfigErrors
	if re := p.validateGlobalConfig(globalCfg); re != nil {
		cerr = ce.Appendf("GlobalConfig", "failed validation")
		return rt, cerr.Extend(re)
	}
	// The order is important here, because serviceConfig refers to global config
	p.descriptorFinder = descriptor.NewFinder(p.validated.globalConfig)

	if re := p.validateServiceConfig(serviceCfg, true); re != nil {
		cerr = ce.Appendf("ServiceConfig", "failed validation")
		return rt, cerr.Extend(re)
	}

	return p.validated, nil
}

// ValidateServiceConfig validates service config.
// if validatePresence is true it will ensure that the named adapter and Kinds
// have an available and configured adapter.
func (p *Validator) validateServiceConfig(cfg string, validatePresence bool) (ce *adapter.ConfigErrors) {
	var err error
	m := &pb.ServiceConfig{}
	if err = yaml.Unmarshal([]byte(cfg), m); err != nil {
		ce = ce.Append("ServiceConfig", err)
		return
	}
	if ce = p.validateAspectRules(m.GetRules(), "", validatePresence); ce != nil {
		return ce
	}
	p.validated.serviceConfig = m
	return
}

// UnknownValidator returns error for the given name.
func UnknownValidator(name string) error {
	return fmt.Errorf("unknown type [%s]", name)
}

// ConvertAdapterParams converts returns a typed proto message based on available Validator.
func ConvertAdapterParams(find AdapterValidatorFinder, name string, params interface{}, strict bool) (adapter.Config, error) {
	var avl adapter.ConfigValidator
	var found bool

	if avl, found = find(name); !found {
		return nil, UnknownValidator(name)
	}

	acfg := avl.DefaultConfig()
	if err := Decode(params, acfg, strict); err != nil {
		return nil, err
	}
	if verr := avl.ValidateConfig(acfg); verr != nil {
		return nil, verr
	}
	return acfg, nil
}

// ConvertAspectParams converts returns a typed proto message based on available Validator.
func ConvertAspectParams(find AspectValidatorFinder, name string, params interface{}, strict bool, df descriptor.Finder) (AspectParams, error) {
	var avl AspectValidator
	var found bool

	if avl, found = find(name); !found {
		return nil, UnknownValidator(name)
	}

	acfg := avl.DefaultConfig()
	if err := Decode(params, acfg, strict); err != nil {
		return nil, err
	}
	if verr := avl.ValidateConfig(acfg, expr.NewCEXLEvaluator(), df); verr != nil {
		return nil, verr
	}
	return acfg, nil
}

// Decode interprets src interface{} as the specified proto message.
// if strict is true returns error on unknown fields.
func Decode(src interface{}, dst adapter.Config, strict bool) (err error) {
	var ba []byte
	ba, err = json.Marshal(src)
	if err != nil {
		return err
	}
	um := jsonpb.Unmarshaler{AllowUnknownFields: !strict}
	return um.Unmarshal(bytes.NewReader(ba), dst)
}
<commit_msg>Add more context to errors returned by config validation (#459)<commit_after>// Copyright 2017 Istio 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 config handles configuration ingestion and processing.
// Validator
// 1. Accepts new configuration from user
// 2. Validates configuration
// 3. Produces a "ValidatedConfig"
// Runtime
// 1. It is validated and actionable configuration
// 2. It resolves the configuration to a list of Combined {aspect, adapter} configs
//    given an attribute.Bag.
// 3. Combined config has complete information needed to dispatch aspect
package config

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/ghodss/yaml"
	"github.com/gogo/protobuf/jsonpb"
	"github.com/golang/protobuf/proto"

	"istio.io/mixer/pkg/adapter"
	"istio.io/mixer/pkg/config/descriptor"
	pb "istio.io/mixer/pkg/config/proto"
	"istio.io/mixer/pkg/expr"
)

type (
	// AspectParams describes configuration parameters for an aspect.
	AspectParams proto.Message

	// AspectValidator describes a type that is able to validate Aspect configuration.
	AspectValidator interface {
		// DefaultConfig returns a default configuration struct for this
		// adapter. This will be used by the configuration system to establish
		// the shape of the block of configuration state passed to the NewAspect method.
		DefaultConfig() (c AspectParams)

		// ValidateConfig determines whether the given configuration meets all correctness requirements.
		ValidateConfig(c AspectParams, validator expr.Validator, finder descriptor.Finder) *adapter.ConfigErrors
	}

	// AdapterValidatorFinder is used to find specific underlying validators.
	// Manager registry and adapter registry should implement this interface
	// so ConfigValidators can be uniformly accessed.
	AdapterValidatorFinder func(name string) (adapter.ConfigValidator, bool)

	// AspectValidatorFinder is used to find specific underlying validators.
	// Manager registry and adapter registry should implement this interface
	// so ConfigValidators can be uniformly accessed.
	AspectValidatorFinder func(name string) (AspectValidator, bool)

	// AdapterToAspectMapper given an pb.Adapter.Impl
	// This is specifically *not* querying by pb.Adapter.Name
	// returns a list of aspects the adapter provides.
	// For many adapter this will be exactly 1 aspect.
	AdapterToAspectMapper func(impl string) []string
)

// NewValidator returns a validator given component validators.
func NewValidator(managerFinder AspectValidatorFinder, adapterFinder AdapterValidatorFinder,
	findAspects AdapterToAspectMapper, strict bool, exprValidator expr.Validator) *Validator {
	return &Validator{
		managerFinder: managerFinder,
		adapterFinder: adapterFinder,
		findAspects:   findAspects,
		strict:        strict,
		exprValidator: exprValidator,
		validated:     &Validated{},
	}
}

type (
	// Validator is the Configuration validator.
	Validator struct {
		managerFinder    AspectValidatorFinder
		adapterFinder    AdapterValidatorFinder
		findAspects      AdapterToAspectMapper
		descriptorFinder descriptor.Finder
		strict           bool
		exprValidator    expr.Validator
		validated        *Validated
	}

	adapterKey struct {
		kind string
		name string
	}
	// Validated store validated configuration.
	// It has been validated as internally consistent and correct.
	Validated struct {
		adapterByName map[adapterKey]*pb.Adapter
		globalConfig  *pb.GlobalConfig
		serviceConfig *pb.ServiceConfig
		numAspects    int
	}
)

func (a adapterKey) String() string {
	return fmt.Sprintf("%s//%s", a.kind, a.name)
}

// validateGlobalConfig consumes a yml config string with adapter config.
// It is validated in presence of validators.
func (p *Validator) validateGlobalConfig(cfg string) (ce *adapter.ConfigErrors) {
	var m = &pb.GlobalConfig{}
	if err := yaml.Unmarshal([]byte(cfg), m); err != nil {
		return ce.Appendf("GlobalConfig", "failed to unmarshal config into proto with err: %v", err)
	}

	p.validated.adapterByName = make(map[adapterKey]*pb.Adapter)
	var acfg adapter.Config
	var err *adapter.ConfigErrors
	for _, aa := range m.GetAdapters() {
		if acfg, err = ConvertAdapterParams(p.adapterFinder, aa.Impl, aa.Params, p.strict); err != nil {
			ce = ce.Appendf("Adapter: "+aa.Impl, "failed to convert aspect params to proto with err: %v", err)
			continue
		}
		aa.Params = acfg
		// check which kinds aa.Impl provides
		// Then register it for all of them.
		for _, kind := range p.findAspects(aa.Impl) {
			p.validated.adapterByName[adapterKey{kind, aa.Name}] = aa
		}
	}
	p.validated.globalConfig = m
	return
}

// ValidateSelector ensures that the selector is valid per expression language.
func (p *Validator) validateSelector(selector string) (err error) {
	// empty selector always selects
	if len(selector) == 0 {
		return nil
	}
	return p.exprValidator.Validate(selector)
}

// validateAspectRules validates the recursive configuration data structure.
// It is primarily used by validate ServiceConfig.
func (p *Validator) validateAspectRules(rules []*pb.AspectRule, path string, validatePresence bool) (ce *adapter.ConfigErrors) {
	var acfg adapter.Config
	for _, rule := range rules {
		if err := p.validateSelector(rule.GetSelector()); err != nil {
			ce = ce.Append(path+":Selector "+rule.GetSelector(), err)
		}
		var err *adapter.ConfigErrors
		path = path + "/" + rule.GetSelector()
		for idx, aa := range rule.GetAspects() {
			if acfg, err = ConvertAspectParams(p.managerFinder, aa.Kind, aa.GetParams(), p.strict, p.descriptorFinder); err != nil {
				ce = ce.Appendf(fmt.Sprintf("%s:%s[%d]", path, aa.Kind, idx), "failed to parse params with err: %#v", err)
				continue
			}
			aa.Params = acfg
			p.validated.numAspects++
			if validatePresence {
				if aa.Adapter == "" {
					aa.Adapter = "default"
				}
				// ensure that aa.Kind has a registered adapter
				ak := adapterKey{aa.Kind, aa.Adapter}
				if p.validated.adapterByName[ak] == nil {
					ce = ce.Appendf("NamedAdapter", "%s not available", ak)
				}
			}
		}
		rs := rule.GetRules()
		if len(rs) == 0 {
			continue
		}
		if verr := p.validateAspectRules(rs, path, validatePresence); verr != nil {
			ce = ce.Extend(verr)
		}
	}
	return ce
}

// Validate validates a single serviceConfig and globalConfig together.
// It returns a fully validated Config if no errors are found.
func (p *Validator) Validate(serviceCfg string, globalCfg string) (rt *Validated, ce *adapter.ConfigErrors) {
	if re := p.validateGlobalConfig(globalCfg); re != nil {
		return rt, ce.Appendf("GlobalConfig", "failed validation").Extend(re)
	}
	// The order is important here, because serviceConfig refers to global config
	p.descriptorFinder = descriptor.NewFinder(p.validated.globalConfig)

	if re := p.validateServiceConfig(serviceCfg, true); re != nil {
		return rt, ce.Appendf("ServiceConfig", "failed validation").Extend(re)
	}
	return p.validated, nil
}

// ValidateServiceConfig validates service config.
// if validatePresence is true it will ensure that the named adapter and Kinds
// have an available and configured adapter.
func (p *Validator) validateServiceConfig(cfg string, validatePresence bool) (ce *adapter.ConfigErrors) {
	var err error
	m := &pb.ServiceConfig{}
	if err = yaml.Unmarshal([]byte(cfg), m); err != nil {
		return ce.Appendf("ServiceConfig", "failed to unmarshal config into proto with err: %v", err)
	}
	if ce = p.validateAspectRules(m.GetRules(), "", validatePresence); ce != nil {
		return ce
	}
	p.validated.serviceConfig = m
	return
}

// UnknownValidator returns error for the given name.
func UnknownValidator(name string) error {
	return fmt.Errorf("unknown type [%s]", name)
}

// ConvertAdapterParams converts returns a typed proto message based on available Validator.
func ConvertAdapterParams(f AdapterValidatorFinder, name string, params interface{}, strict bool) (ac adapter.Config, ce *adapter.ConfigErrors) {
	var avl adapter.ConfigValidator
	var found bool

	if avl, found = f(name); !found {
		return nil, ce.Append(name, UnknownValidator(name))
	}

	ac = avl.DefaultConfig()
	if err := Decode(params, ac, strict); err != nil {
		return nil, ce.Appendf(name, "failed to decode adapter params with err: %v", err)
	}
	if err := avl.ValidateConfig(ac); err != nil {
		return nil, ce.Appendf(name, "adapter validation failed with err: %v", err)
	}
	return ac, nil
}

// ConvertAspectParams converts returns a typed proto message based on available Validator.
func ConvertAspectParams(f AspectValidatorFinder, name string, params interface{}, strict bool, df descriptor.Finder) (AspectParams, *adapter.ConfigErrors) {
	var ce *adapter.ConfigErrors
	var avl AspectValidator
	var found bool

	if avl, found = f(name); !found {
		return nil, ce.Append(name, UnknownValidator(name))
	}

	ap := avl.DefaultConfig()
	if err := Decode(params, ap, strict); err != nil {
		return nil, ce.Appendf(name, "failed to decode aspect params with err: %v", err)
	}
	if err := avl.ValidateConfig(ap, expr.NewCEXLEvaluator(), df); err != nil {
		return nil, ce.Appendf(name, "aspect validation failed with err: %v", err)
	}
	return ap, nil
}

// Decode interprets src interface{} as the specified proto message.
// if strict is true returns error on unknown fields.
func Decode(src interface{}, dst adapter.Config, strict bool) error {
	ba, err := json.Marshal(src)
	if err != nil {
		return fmt.Errorf("failed to marshal config into json with err: %v", err)
	}
	um := jsonpb.Unmarshaler{AllowUnknownFields: !strict}
	if err := um.Unmarshal(bytes.NewReader(ba), dst); err != nil {
		return fmt.Errorf("failed to unmarshal config into proto with err: %v", err)
	}
	return nil
}
<|endoftext|>


<commit_before>package lib

import (
	"reflect"
	"testing"
)

func TestParse(t *testing.T) {
	testcase := `<http://example.com/v2/_catalog?n=20&last=b>; rel="next"`

	url, err := parseLinkToNextHeader(testcase)

	if err != nil {
		t.Fatal(err)
	}

	if url.Scheme != "http" ||
		url.Host != "example.com" ||
		url.Path != "/v2/_catalog" ||
		!reflect.DeepEqual((map[string][]string)(url.Query()), map[string][]string{
			"n":    []string{"20"},
			"last": []string{"b"},
		}) {

		t.Fatalf("parsing failed; got %s", url.String())
	}
}

func TestOarseInvalid(t *testing.T) {
	testcase := `<http://example.com/v2/_catalog?n=20&last=b; rel="next"`

	_, err := parseLinkToNextHeader(testcase)

	if err == nil {
		t.Fatal("parsing an invalid header should fail")
	}
}
<commit_msg>Typo.<commit_after>package lib

import (
	"reflect"
	"testing"
)

func TestParse(t *testing.T) {
	testcase := `<http://example.com/v2/_catalog?n=20&last=b>; rel="next"`

	url, err := parseLinkToNextHeader(testcase)

	if err != nil {
		t.Fatal(err)
	}

	if url.Scheme != "http" ||
		url.Host != "example.com" ||
		url.Path != "/v2/_catalog" ||
		!reflect.DeepEqual((map[string][]string)(url.Query()), map[string][]string{
			"n":    []string{"20"},
			"last": []string{"b"},
		}) {

		t.Fatalf("parsing failed; got %s", url.String())
	}
}

func TestParseInvalid(t *testing.T) {
	testcase := `<http://example.com/v2/_catalog?n=20&last=b; rel="next"`

	_, err := parseLinkToNextHeader(testcase)

	if err == nil {
		t.Fatal("parsing an invalid header should fail")
	}
}
<|endoftext|>


<commit_before>package libvirt

import (
	"bytes"
	"text/template"

	"github.com/mistifyio/mistify-agent/client"
)

var domainTemplate *template.Template
var networkTemplate *template.Template

func init() {
	const domainXML = `
<domain type="{{.Type}}">
  <name>{{.Id}}</name>
  <memory unit="MiB">{{.Memory}}</memory>
  <vcpu>{{.Cpu}}</vcpu>

  {{if .Metadata}}
  <metadata>
    {{range .Metadata}}
    {{end}}
  </metadata>
  {{end}}

  <os>
    <type>hvm</type>
  </os>
  <devices>
    {{range .Nics}}
    <interface type="network">
      <source network='{{.Mac}}' porgroup='vlan-all' />
	  {{if .Name}}<guest dev="{{.Name}}" />{{end}}
      {{if .Mac}}<mac address="{{.Mac}}" />{{end}}
      {{if .Model}}<model type="{{.Model}}" />{{end}}
    </interface>
    {{end}}

    {{range .Disks}}
    <disk type="block" device="disk">
      <driver name="qemu" type="raw" />
      <source dev="{{.Source}}" />
      <target dev="{{.Device}}" bus="{{.Bus}}" />
    </disk>
    {{end}}
  </devices>
</domain>
`
	domainTemplate = template.Must(template.New("domainXML").Parse(domainXML))

	const networkXML = `
<network>
	<name>{{.Mac}}</name>
	<forward mode='bridge' />
	<bridge name='{{.Network}}' />
	<virtualport type='openvswitch' />
	<portgroup name='vlan-all'>
		<vlan>
		{{range .VLANs}}
			<tag id ='{{.}}' />
		{{end}}
		</vlan>
	</portgroup>
</network>
`
	networkTemplate = template.Must(template.New("networkXML").Parse(networkXML))
}

// DomainXML populates a libvirt domain xml template with guest properties
func (lv *Libvirt) DomainXML(guest *client.Guest) (string, error) {
	buf := new(bytes.Buffer)
	err := domainTemplate.Execute(buf, guest)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

// NetworkXML populates a libvirt network xml template with guest properties
func (lv *Libvirt) NetworkXML(nic client.Nic) (string, error) {
	buf := new(bytes.Buffer)
	err := networkTemplate.Execute(buf, nic)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}
<commit_msg>Typo in portgroup<commit_after>package libvirt

import (
	"bytes"
	"text/template"

	"github.com/mistifyio/mistify-agent/client"
)

var domainTemplate *template.Template
var networkTemplate *template.Template

func init() {
	const domainXML = `
<domain type="{{.Type}}">
  <name>{{.Id}}</name>
  <memory unit="MiB">{{.Memory}}</memory>
  <vcpu>{{.Cpu}}</vcpu>

  {{if .Metadata}}
  <metadata>
    {{range .Metadata}}
    {{end}}
  </metadata>
  {{end}}

  <os>
    <type>hvm</type>
  </os>
  <devices>
    {{range .Nics}}
    <interface type="network">
      <source network='{{.Mac}}' portgroup='vlan-all' />
	  {{if .Name}}<guest dev="{{.Name}}" />{{end}}
      {{if .Mac}}<mac address="{{.Mac}}" />{{end}}
      {{if .Model}}<model type="{{.Model}}" />{{end}}
    </interface>
    {{end}}

    {{range .Disks}}
    <disk type="block" device="disk">
      <driver name="qemu" type="raw" />
      <source dev="{{.Source}}" />
      <target dev="{{.Device}}" bus="{{.Bus}}" />
    </disk>
    {{end}}
  </devices>
</domain>
`
	domainTemplate = template.Must(template.New("domainXML").Parse(domainXML))

	const networkXML = `
<network>
	<name>{{.Mac}}</name>
	<forward mode='bridge' />
	<bridge name='{{.Network}}' />
	<virtualport type='openvswitch' />
	<portgroup name='vlan-all'>
		<vlan>
		{{range .VLANs}}
			<tag id ='{{.}}' />
		{{end}}
		</vlan>
	</portgroup>
</network>
`
	networkTemplate = template.Must(template.New("networkXML").Parse(networkXML))
}

// DomainXML populates a libvirt domain xml template with guest properties
func (lv *Libvirt) DomainXML(guest *client.Guest) (string, error) {
	buf := new(bytes.Buffer)
	err := domainTemplate.Execute(buf, guest)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

// NetworkXML populates a libvirt network xml template with guest properties
func (lv *Libvirt) NetworkXML(nic client.Nic) (string, error) {
	buf := new(bytes.Buffer)
	err := networkTemplate.Execute(buf, nic)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}
<|endoftext|>


<commit_before>package gcache

import (
	"fmt"
	"io/ioutil"
	"net/http"

	"golang.org/x/oauth2"
	"google.golang.org/api/drive/v3"
	"google.golang.org/api/googleapi"
)

var (
	oauth2TokenSource oauth2.TokenSource // The token is valid for 30 minutes.
)

// GetGDriveFile returns a file on Google Drive.
func GetGDriveFile(
	r *http.Request,
	name string,
	field googleapi.Field,
) (
	*drive.File,
	[]byte, // payload
	error,
) {
	var payload []byte

retry:
	service, err := drive.New(createGDriveClient(r))
	if err != nil {
		if IsInvalidSecurityTicket(err) {
			oauth2TokenSource = nil
			goto retry
		}
		return nil, nil, err
	}

	fileList, err := service.Files.List().PageSize(1).Spaces("drive").Q(fmt.Sprintf("name='%s'", name)).Fields(field).Do()
	if err != nil {
		return nil, nil, err
	}
	if len(fileList.Files) <= 0 {
		return nil, nil, &DriveFileDoesNotExistError{}
	}
	file := fileList.Files[0]
	fileID := file.Id
	httpResponse, err := service.Files.Export(fileID, mimeTxt).Download()
	if IsFileNotExportableError(err) {
		err = service.Files.Delete(fileID).Do()
		if err != nil {
			// pass
		}
		return nil, nil, &DriveFileDoesNotExistError{}
	}
	if err != nil {
		return nil, nil, err
	}

	defer httpResponse.Body.Close()
	payload, err = ioutil.ReadAll(httpResponse.Body)
	if err != nil {
		return nil, nil, err
	}
	return file, payload, err
}
<commit_msg>add GetGDriveService<commit_after>package gcache

import (
	"fmt"
	"io/ioutil"
	"net/http"

	"golang.org/x/oauth2"
	"google.golang.org/api/drive/v3"
	"google.golang.org/api/googleapi"
)

var (
	oauth2TokenSource oauth2.TokenSource // The token is valid for 30 minutes.
)

// GetGDriveService returns the API service of Google Drive.
func GetGDriveService(
	r *http.Request,
) (
	*drive.Service,
	error,
) {
retry:
	service, err := drive.New(createGDriveClient(r))
	if err != nil {
		if IsInvalidSecurityTicket(err) {
			oauth2TokenSource = nil
			goto retry
		}
		return nil, err
	}
	return service, nil
}

// GetGDriveFile returns a file on Google Drive.
func GetGDriveFile(
	r *http.Request,
	name string,
	field googleapi.Field,
) (
	*drive.File,
	[]byte, // payload
	error,
) {
	var payload []byte

retry:
	service, err := drive.New(createGDriveClient(r))
	if err != nil {
		if IsInvalidSecurityTicket(err) {
			oauth2TokenSource = nil
			goto retry
		}
		return nil, nil, err
	}

	fileList, err := service.Files.List().PageSize(1).Spaces("drive").Q(fmt.Sprintf("name='%s'", name)).Fields(field).Do()
	if err != nil {
		return nil, nil, err
	}
	if len(fileList.Files) <= 0 {
		return nil, nil, &DriveFileDoesNotExistError{}
	}
	file := fileList.Files[0]
	fileID := file.Id
	httpResponse, err := service.Files.Export(fileID, mimeTxt).Download()
	if IsFileNotExportableError(err) {
		err = service.Files.Delete(fileID).Do()
		if err != nil {
			// pass
		}
		return nil, nil, &DriveFileDoesNotExistError{}
	}
	if err != nil {
		return nil, nil, err
	}

	defer httpResponse.Body.Close()
	payload, err = ioutil.ReadAll(httpResponse.Body)
	if err != nil {
		return nil, nil, err
	}
	return file, payload, err
}
<|endoftext|>


<commit_before>package arn

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/animenotifier/arn/autocorrect"
	"github.com/animenotifier/arn/validator"
	gravatar "github.com/ungerik/go-gravatar"
)

var setNickMutex sync.Mutex
var setEmailMutex sync.Mutex

// User ...
type User struct {
	ID         string       `json:"id"`
	Nick       string       `json:"nick" editable:"true"`
	FirstName  string       `json:"firstName"`
	LastName   string       `json:"lastName"`
	Email      string       `json:"email"`
	Role       string       `json:"role"`
	Registered string       `json:"registered"`
	LastLogin  string       `json:"lastLogin"`
	LastSeen   string       `json:"lastSeen"`
	ProExpires string       `json:"proExpires"`
	Gender     string       `json:"gender"`
	Language   string       `json:"language"`
	Tagline    string       `json:"tagline" editable:"true"`
	Website    string       `json:"website" editable:"true"`
	IP         string       `json:"ip"`
	UserAgent  string       `json:"agent"`
	Balance    int          `json:"balance"`
	Avatar     UserAvatar   `json:"avatar"`
	AgeRange   UserAgeRange `json:"ageRange"`
	Location   UserLocation `json:"location"`
	Accounts   UserAccounts `json:"accounts"`
	Browser    UserBrowser  `json:"browser"`
	OS         UserOS       `json:"os"`
	Following  []string     `json:"following"`

	settings   *Settings
	animeList  *AnimeList
	follows    *UserFollows
	draftIndex *DraftIndex
}

// NewUser creates an empty user object with a unique ID.
func NewUser() *User {
	user := &User{
		ID: GenerateID("User"),

		// Avoid nil value fields
		Following: make([]string, 0),
	}

	return user
}

// RegisterUser registers a new user in the database and sets up all the required references.
func RegisterUser(user *User) {
	user.Registered = DateTimeUTC()
	user.LastLogin = user.Registered
	user.LastSeen = user.Registered

	// Save nick in NickToUser table
	DB.Set("NickToUser", user.Nick, &NickToUser{
		Nick:   user.Nick,
		UserID: user.ID,
	})

	// Save email in EmailToUser table
	DB.Set("EmailToUser", user.Email, &EmailToUser{
		Email:  user.Email,
		UserID: user.ID,
	})

	// Create default settings
	NewSettings(user.ID).Save()

	// Add empty anime list
	DB.Set("AnimeList", user.ID, &AnimeList{
		UserID: user.ID,
		Items:  []*AnimeListItem{},
	})

	// Add empty inventory
	NewInventory(user.ID).Save()

	// Add draft index
	NewDraftIndex(user.ID).Save()

	// Add empty push subscriptions
	DB.Set("PushSubscriptions", user.ID, &PushSubscriptions{
		UserID: user.ID,
		Items:  []*PushSubscription{},
	})

	// Add empty follow list
	follows := &UserFollows{}
	follows.UserID = user.ID
	follows.Items = []string{}

	DB.Set("UserFollows", user.ID, follows)

	// Refresh avatar async
	go user.RefreshAvatar()
}

// SendNotification ...
func (user *User) SendNotification(notification *Notification) {
	// Don't ever send notifications in development mode
	if IsDevelopment() && user.ID != "4J6qpK1ve" {
		return
	}

	subs := user.PushSubscriptions()
	expired := []*PushSubscription{}

	for _, sub := range subs.Items {
		err := sub.SendNotification(notification)

		if err != nil {
			if err.Error() == "Subscription expired" {
				expired = append(expired, sub)
			}
		}
	}

	// Remove expired items
	if len(expired) > 0 {
		for _, sub := range expired {
			subs.Remove(sub.ID())
		}

		subs.Save()
	}
}

// RealName returns the real name of the user.
func (user *User) RealName() string {
	if user.LastName == "" {
		return user.FirstName
	}

	if user.FirstName == "" {
		return user.LastName
	}

	return user.FirstName + " " + user.LastName
}

// RegisteredTime ...
func (user *User) RegisteredTime() time.Time {
	reg, _ := time.Parse(time.RFC3339, user.Registered)
	return reg
}

// IsActive ...
func (user *User) IsActive() bool {
	// Exclude people who didn't change their nickname.
	if !user.HasNick() {
		return false
	}

	lastSeen, _ := time.Parse(time.RFC3339, user.LastSeen)
	oneWeekAgo := time.Now().Add(-7 * 24 * time.Hour)

	if lastSeen.Unix() < oneWeekAgo.Unix() {
		return false
	}

	if len(user.AnimeList().Items) == 0 {
		return false
	}

	return true
}

// IsPro ...
func (user *User) IsPro() bool {
	if user.ProExpires == "" {
		return false
	}

	return DateTimeUTC() < user.ProExpires
}

// ExtendProDuration ...
func (user *User) ExtendProDuration(duration time.Duration) {
	var startDate time.Time

	if user.ProExpires == "" {
		startDate = time.Now().UTC()
	} else {
		startDate, _ = time.Parse(time.RFC3339, user.ProExpires)
	}

	user.ProExpires = startDate.Add(duration).Format(time.RFC3339)
}

// TimeSinceRegistered ...
func (user *User) TimeSinceRegistered() time.Duration {
	registered, _ := time.Parse(time.RFC3339, user.Registered)
	return time.Since(registered)
}

// HasNick returns whether the user has a custom nickname.
func (user *User) HasNick() bool {
	return !strings.HasPrefix(user.Nick, "g") && !strings.HasPrefix(user.Nick, "fb") && !strings.HasPrefix(user.Nick, "t") && user.Nick != ""
}

// WebsiteURL adds https:// to the URL.
func (user *User) WebsiteURL() string {
	return "https://" + user.WebsiteShortURL()
}

// WebsiteShortURL ...
func (user *User) WebsiteShortURL() string {
	return strings.Replace(strings.Replace(user.Website, "https://", "", 1), "http://", "", 1)
}

// Link returns the URI to the user page.
func (user *User) Link() string {
	return "/+" + user.Nick
}

// CoverImageURL ...
func (user *User) CoverImageURL() string {
	return "/images/cover/default.png"
}

// HasAvatar ...
func (user *User) HasAvatar() bool {
	return user.Avatar.Extension != ""
}

// SmallAvatar ...
func (user *User) SmallAvatar() string {
	return "//media.notify.moe/images/avatars/small/" + user.ID + user.Avatar.Extension
}

// LargeAvatar ...
func (user *User) LargeAvatar() string {
	return "//media.notify.moe/images/avatars/large/" + user.ID + user.Avatar.Extension
}

// Gravatar ...
func (user *User) Gravatar() string {
	if user.Email == "" {
		return ""
	}

	return gravatar.SecureUrl(user.Email) + "?s=" + fmt.Sprint(AvatarMaxSize)
}

// PushSubscriptions ...
func (user *User) PushSubscriptions() *PushSubscriptions {
	subs, _ := GetPushSubscriptions(user.ID)
	return subs
}

// Inventory ...
func (user *User) Inventory() *Inventory {
	inventory, _ := GetInventory(user.ID)
	return inventory
}

// ActivateItemEffect ...
func (user *User) ActivateItemEffect(itemID string) error {
	month := 30 * 24 * time.Hour

	switch itemID {
	case "pro-account-3":
		user.ExtendProDuration(3 * month)
		user.Save()
		return nil

	case "pro-account-6":
		user.ExtendProDuration(6 * month)
		user.Save()
		return nil

	case "pro-account-12":
		user.ExtendProDuration(12 * month)
		user.Save()
		return nil

	case "pro-account-24":
		user.ExtendProDuration(24 * month)
		user.Save()
		return nil

	default:
		return errors.New("Can't activate unknown item: " + itemID)
	}
}

// SetNick changes the user's nickname safely.
func (user *User) SetNick(newName string) error {
	setNickMutex.Lock()
	defer setNickMutex.Unlock()

	newName = autocorrect.FixUserNick(newName)

	if !validator.IsValidNick(newName) {
		return errors.New("Invalid nickname")
	}

	if newName == user.Nick {
		return nil
	}

	// Make sure the nickname doesn't exist already
	_, err := GetUserByNick(newName)

	// If there was no error: the username exists.
	// If "not found" is not included in the error message it's a different error type.
	if err == nil || strings.Index(err.Error(), "not found") == -1 {
		return errors.New("Username '" + newName + "' is taken already")
	}

	user.ForceSetNick(newName)
	return nil
}

// ForceSetNick forces a nickname overwrite.
func (user *User) ForceSetNick(newName string) {
	// Delete old nick reference
	DB.Delete("NickToUser", user.Nick)

	// Set new nick
	user.Nick = newName

	DB.Set("NickToUser", user.Nick, &NickToUser{
		Nick:   user.Nick,
		UserID: user.ID,
	})
}

// SetEmail changes the user's email safely.
func (user *User) SetEmail(newName string) error {
	setEmailMutex.Lock()
	defer setEmailMutex.Unlock()

	if !validator.IsValidEmail(user.Email) {
		return errors.New("Invalid email address")
	}

	// Delete old email reference
	DB.Delete("EmailToUser", user.Email)

	// Set new email
	user.Email = newName

	DB.Set("EmailToUser", user.Email, &EmailToUser{
		Email:  user.Email,
		UserID: user.ID,
	})

	return nil
}
<commit_msg>Fixed image URL<commit_after>package arn

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/animenotifier/arn/autocorrect"
	"github.com/animenotifier/arn/validator"
	gravatar "github.com/ungerik/go-gravatar"
)

var setNickMutex sync.Mutex
var setEmailMutex sync.Mutex

// User ...
type User struct {
	ID         string       `json:"id"`
	Nick       string       `json:"nick" editable:"true"`
	FirstName  string       `json:"firstName"`
	LastName   string       `json:"lastName"`
	Email      string       `json:"email"`
	Role       string       `json:"role"`
	Registered string       `json:"registered"`
	LastLogin  string       `json:"lastLogin"`
	LastSeen   string       `json:"lastSeen"`
	ProExpires string       `json:"proExpires"`
	Gender     string       `json:"gender"`
	Language   string       `json:"language"`
	Tagline    string       `json:"tagline" editable:"true"`
	Website    string       `json:"website" editable:"true"`
	IP         string       `json:"ip"`
	UserAgent  string       `json:"agent"`
	Balance    int          `json:"balance"`
	Avatar     UserAvatar   `json:"avatar"`
	AgeRange   UserAgeRange `json:"ageRange"`
	Location   UserLocation `json:"location"`
	Accounts   UserAccounts `json:"accounts"`
	Browser    UserBrowser  `json:"browser"`
	OS         UserOS       `json:"os"`
	Following  []string     `json:"following"`

	settings   *Settings
	animeList  *AnimeList
	follows    *UserFollows
	draftIndex *DraftIndex
}

// NewUser creates an empty user object with a unique ID.
func NewUser() *User {
	user := &User{
		ID: GenerateID("User"),

		// Avoid nil value fields
		Following: make([]string, 0),
	}

	return user
}

// RegisterUser registers a new user in the database and sets up all the required references.
func RegisterUser(user *User) {
	user.Registered = DateTimeUTC()
	user.LastLogin = user.Registered
	user.LastSeen = user.Registered

	// Save nick in NickToUser table
	DB.Set("NickToUser", user.Nick, &NickToUser{
		Nick:   user.Nick,
		UserID: user.ID,
	})

	// Save email in EmailToUser table
	DB.Set("EmailToUser", user.Email, &EmailToUser{
		Email:  user.Email,
		UserID: user.ID,
	})

	// Create default settings
	NewSettings(user.ID).Save()

	// Add empty anime list
	DB.Set("AnimeList", user.ID, &AnimeList{
		UserID: user.ID,
		Items:  []*AnimeListItem{},
	})

	// Add empty inventory
	NewInventory(user.ID).Save()

	// Add draft index
	NewDraftIndex(user.ID).Save()

	// Add empty push subscriptions
	DB.Set("PushSubscriptions", user.ID, &PushSubscriptions{
		UserID: user.ID,
		Items:  []*PushSubscription{},
	})

	// Add empty follow list
	follows := &UserFollows{}
	follows.UserID = user.ID
	follows.Items = []string{}

	DB.Set("UserFollows", user.ID, follows)

	// Refresh avatar async
	go user.RefreshAvatar()
}

// SendNotification ...
func (user *User) SendNotification(notification *Notification) {
	// Don't ever send notifications in development mode
	if IsDevelopment() && user.ID != "4J6qpK1ve" {
		return
	}

	subs := user.PushSubscriptions()
	expired := []*PushSubscription{}

	for _, sub := range subs.Items {
		err := sub.SendNotification(notification)

		if err != nil {
			if err.Error() == "Subscription expired" {
				expired = append(expired, sub)
			}
		}
	}

	// Remove expired items
	if len(expired) > 0 {
		for _, sub := range expired {
			subs.Remove(sub.ID())
		}

		subs.Save()
	}
}

// RealName returns the real name of the user.
func (user *User) RealName() string {
	if user.LastName == "" {
		return user.FirstName
	}

	if user.FirstName == "" {
		return user.LastName
	}

	return user.FirstName + " " + user.LastName
}

// RegisteredTime ...
func (user *User) RegisteredTime() time.Time {
	reg, _ := time.Parse(time.RFC3339, user.Registered)
	return reg
}

// IsActive ...
func (user *User) IsActive() bool {
	// Exclude people who didn't change their nickname.
	if !user.HasNick() {
		return false
	}

	lastSeen, _ := time.Parse(time.RFC3339, user.LastSeen)
	oneWeekAgo := time.Now().Add(-7 * 24 * time.Hour)

	if lastSeen.Unix() < oneWeekAgo.Unix() {
		return false
	}

	if len(user.AnimeList().Items) == 0 {
		return false
	}

	return true
}

// IsPro ...
func (user *User) IsPro() bool {
	if user.ProExpires == "" {
		return false
	}

	return DateTimeUTC() < user.ProExpires
}

// ExtendProDuration ...
func (user *User) ExtendProDuration(duration time.Duration) {
	var startDate time.Time

	if user.ProExpires == "" {
		startDate = time.Now().UTC()
	} else {
		startDate, _ = time.Parse(time.RFC3339, user.ProExpires)
	}

	user.ProExpires = startDate.Add(duration).Format(time.RFC3339)
}

// TimeSinceRegistered ...
func (user *User) TimeSinceRegistered() time.Duration {
	registered, _ := time.Parse(time.RFC3339, user.Registered)
	return time.Since(registered)
}

// HasNick returns whether the user has a custom nickname.
func (user *User) HasNick() bool {
	return !strings.HasPrefix(user.Nick, "g") && !strings.HasPrefix(user.Nick, "fb") && !strings.HasPrefix(user.Nick, "t") && user.Nick != ""
}

// WebsiteURL adds https:// to the URL.
func (user *User) WebsiteURL() string {
	return "https://" + user.WebsiteShortURL()
}

// WebsiteShortURL ...
func (user *User) WebsiteShortURL() string {
	return strings.Replace(strings.Replace(user.Website, "https://", "", 1), "http://", "", 1)
}

// Link returns the URI to the user page.
func (user *User) Link() string {
	return "/+" + user.Nick
}

// CoverImageURL ...
func (user *User) CoverImageURL() string {
	return "/images/cover/default.jpg"
}

// HasAvatar ...
func (user *User) HasAvatar() bool {
	return user.Avatar.Extension != ""
}

// SmallAvatar ...
func (user *User) SmallAvatar() string {
	return "//media.notify.moe/images/avatars/small/" + user.ID + user.Avatar.Extension
}

// LargeAvatar ...
func (user *User) LargeAvatar() string {
	return "//media.notify.moe/images/avatars/large/" + user.ID + user.Avatar.Extension
}

// Gravatar ...
func (user *User) Gravatar() string {
	if user.Email == "" {
		return ""
	}

	return gravatar.SecureUrl(user.Email) + "?s=" + fmt.Sprint(AvatarMaxSize)
}

// PushSubscriptions ...
func (user *User) PushSubscriptions() *PushSubscriptions {
	subs, _ := GetPushSubscriptions(user.ID)
	return subs
}

// Inventory ...
func (user *User) Inventory() *Inventory {
	inventory, _ := GetInventory(user.ID)
	return inventory
}

// ActivateItemEffect ...
func (user *User) ActivateItemEffect(itemID string) error {
	month := 30 * 24 * time.Hour

	switch itemID {
	case "pro-account-3":
		user.ExtendProDuration(3 * month)
		user.Save()
		return nil

	case "pro-account-6":
		user.ExtendProDuration(6 * month)
		user.Save()
		return nil

	case "pro-account-12":
		user.ExtendProDuration(12 * month)
		user.Save()
		return nil

	case "pro-account-24":
		user.ExtendProDuration(24 * month)
		user.Save()
		return nil

	default:
		return errors.New("Can't activate unknown item: " + itemID)
	}
}

// SetNick changes the user's nickname safely.
func (user *User) SetNick(newName string) error {
	setNickMutex.Lock()
	defer setNickMutex.Unlock()

	newName = autocorrect.FixUserNick(newName)

	if !validator.IsValidNick(newName) {
		return errors.New("Invalid nickname")
	}

	if newName == user.Nick {
		return nil
	}

	// Make sure the nickname doesn't exist already
	_, err := GetUserByNick(newName)

	// If there was no error: the username exists.
	// If "not found" is not included in the error message it's a different error type.
	if err == nil || strings.Index(err.Error(), "not found") == -1 {
		return errors.New("Username '" + newName + "' is taken already")
	}

	user.ForceSetNick(newName)
	return nil
}

// ForceSetNick forces a nickname overwrite.
func (user *User) ForceSetNick(newName string) {
	// Delete old nick reference
	DB.Delete("NickToUser", user.Nick)

	// Set new nick
	user.Nick = newName

	DB.Set("NickToUser", user.Nick, &NickToUser{
		Nick:   user.Nick,
		UserID: user.ID,
	})
}

// SetEmail changes the user's email safely.
func (user *User) SetEmail(newName string) error {
	setEmailMutex.Lock()
	defer setEmailMutex.Unlock()

	if !validator.IsValidEmail(user.Email) {
		return errors.New("Invalid email address")
	}

	// Delete old email reference
	DB.Delete("EmailToUser", user.Email)

	// Set new email
	user.Email = newName

	DB.Set("EmailToUser", user.Email, &EmailToUser{
		Email:  user.Email,
		UserID: user.ID,
	})

	return nil
}
<|endoftext|>