<commit_before>package backupservice

import (
	"log"
	"os"

	"fmt"

	"path/filepath"

	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/mleonard87/frosty/config"
)

const (
	BUCKET_NAME                            string = "frosty.backups"
	ERROR_CODE_INVALID_BUCKET_NAME         string = "InvalidBucketName"
	ERROR_CODE_BUCKET_ALREADY_OWNED_BY_YOU string = "BucketAlreadyOwnedByYou"
)

type AmazonS3BackupService struct {
	AccessKeyId     string
	SecretAccessKey string
	Region          string
	AccountId       string
	BucketName      string
	S3Service       *s3.S3
}

// Return the backup service type this must match the string as used as the JSON property in the frosty backup config.
func (agss *AmazonS3BackupService) Name() string {
	return config.BACKUP_SERVICE_AMAZON_S3
}

// Initialise any variable needed for backups.
func (asbs *AmazonS3BackupService) SetConfig(backupConfig *config.BackupConfig) {
	asbs.AccessKeyId = backupConfig.BackupConfig["accessKeyId"].(string)
	asbs.SecretAccessKey = backupConfig.BackupConfig["secretAccessKey"].(string)
	asbs.Region = backupConfig.BackupConfig["region"].(string)
	asbs.AccountId = backupConfig.BackupConfig["accountId"].(string)
	asbs.BucketName = BUCKET_NAME
}

// Initialise anything in the backup service that needs to be created prior to uploading files. In this instance we need
// to create a bucket to store the backups if one does not already exist. This always uses a bucket
// called "frosty.backups".
func (asbs *AmazonS3BackupService) Init() error {
	asbs.setEnvvars()
	asbs.S3Service = s3.New(session.New(), &aws.Config{})

	err := asbs.createBucket(asbs.BucketName)
	if err != nil {
		fmt.Println("error creating bucket")
		fmt.Println(err)
		return err
	}

	return nil
}

// Store the file in pathToFile in the bucket in S3.
func (asbs *AmazonS3BackupService) StoreFile(pathToFile string) error {
	_, fileName := filepath.Split(pathToFile)

	key := getObjectKey(fileName)

	f, err := os.Open(pathToFile)
	if err != nil {
		log.Printf("Failed to open file to store: %s", pathToFile)
		log.Println(err)
		return err
	}

	params := &s3.PutObjectInput{
		Body:   f,
		Bucket: &asbs.BucketName,
		Key:    &key,
	}

	_, err = asbs.S3Service.PutObject(params)
	if err != nil {
		log.Printf("Failed to put object %s into bucket %s with a key of %s\n", pathToFile, asbs.BucketName, fileName)
		log.Println(err)
		return err
	}

	return nil
}

// Get the name to be used for the .zip archive without the .zip extension.
func (asbs *AmazonS3BackupService) ArtifactFilename(jobName string) string {
	return jobName
}

// Get a friendly name for the email template of where this backup was stored. In this case, the name of the S3 bucket.
func (asbs *AmazonS3BackupService) BackupLocation() string {
	return fmt.Sprintf("S3 Bucket: %s", asbs.BucketName)
}

// Create the S3 bucket.
func (asbs *AmazonS3BackupService) createBucket(bucketName string) error {
	asbs.setEnvvars()
	params := &s3.CreateBucketInput{
		Bucket: aws.String(bucketName),
	}

	_, err := asbs.S3Service.CreateBucket(params)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			if aerr.Code() == ERROR_CODE_INVALID_BUCKET_NAME {
				log.Printf("The specified bucket is not valid. Bucket name: %s\n", bucketName)
				log.Println(err)
				return err
			}
			// If the BucketAlreadyOwnedByYou error is raised then this bucket already exists.
			if aerr.Code() == ERROR_CODE_BUCKET_ALREADY_OWNED_BY_YOU {
				return nil
			}

		}
		return err
	}

	if err = asbs.S3Service.WaitUntilBucketExists(&s3.HeadBucketInput{Bucket: &bucketName}); err != nil {
		log.Printf("Failed to wait for bucket to exist %s, %s\n", bucketName, err)
		return err
	}

	return nil
}

// Set the required AWS environment variables.
func (asbs *AmazonS3BackupService) setEnvvars() {
	os.Setenv(ENVVAR_AWS_ACCESS_KEY_ID, asbs.AccessKeyId)
	os.Setenv(ENVVAR_AWS_SECRET_ACCESS_KEY, asbs.SecretAccessKey)
	os.Setenv(ENVVAR_AWS_REGION, asbs.Region)
}

// Get the name to use for the file being stored in S3.
func getObjectKey(fileName string) string {
	hostname, err := os.Hostname()
	if err != nil {
		log.Fatal("Could not determine hostname.", err)
		return ""
	}

	return fmt.Sprintf("%s/%s/%s_%s", hostname, time.Now().Format("20060102"), time.Now().Format("15:04:05"), fileName)
}
<commit_msg>Frosty backup now sets a lifecycle configuration for the bucket it creates. The lifecycle deletes objects in the bucket after a number of days that is configurable in frosty config file.<commit_after>package backupservice

import (
	"log"
	"os"

	"fmt"

	"path/filepath"

	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/mleonard87/frosty/config"
)

const (
	BUCKET_NAME                            string = "frosty.backups"
	ERROR_CODE_INVALID_BUCKET_NAME         string = "InvalidBucketName"
	ERROR_CODE_BUCKET_ALREADY_OWNED_BY_YOU string = "BucketAlreadyOwnedByYou"
)

type AmazonS3BackupService struct {
	AccessKeyId     string
	SecretAccessKey string
	Region          string
	AccountId       string
	RetentionDays   int64
	BucketName      string
	S3Service       *s3.S3
}

// Return the backup service type this must match the string as used as the JSON property in the frosty backup config.
func (agss *AmazonS3BackupService) Name() string {
	return config.BACKUP_SERVICE_AMAZON_S3
}

// Initialise any variable needed for backups.
func (asbs *AmazonS3BackupService) SetConfig(backupConfig *config.BackupConfig) {
	asbs.AccessKeyId = backupConfig.BackupConfig["accessKeyId"].(string)
	asbs.SecretAccessKey = backupConfig.BackupConfig["secretAccessKey"].(string)
	asbs.Region = backupConfig.BackupConfig["region"].(string)
	asbs.AccountId = backupConfig.BackupConfig["accountId"].(string)
	asbs.RetentionDays = int64(backupConfig.BackupConfig["retentionDays"].(float64))
	asbs.BucketName = BUCKET_NAME
}

// Initialise anything in the backup service that needs to be created prior to uploading files. In this instance we need
// to create a bucket to store the backups if one does not already exist. This always uses a bucket
// called "frosty.backups".
func (asbs *AmazonS3BackupService) Init() error {
	asbs.setEnvvars()
	asbs.S3Service = s3.New(session.New(), &aws.Config{})

	err := asbs.createBucket(asbs.BucketName)
	if err != nil {
		fmt.Println("Error creating bucket")
		fmt.Println(err)
		return err
	}

	err = asbs.putBucketLifecycleConfiguration()
	if err != nil {
		fmt.Println("Error creating bucket lifecycle")
		fmt.Println(err)
		return err
	}

	return nil
}

// Store the file in pathToFile in the bucket in S3.
func (asbs *AmazonS3BackupService) StoreFile(pathToFile string) error {
	_, fileName := filepath.Split(pathToFile)

	key := getObjectKey(fileName)

	f, err := os.Open(pathToFile)
	if err != nil {
		log.Printf("Failed to open file to store: %s", pathToFile)
		log.Println(err)
		return err
	}

	params := &s3.PutObjectInput{
		Body:   f,
		Bucket: &asbs.BucketName,
		Key:    &key,
	}

	_, err = asbs.S3Service.PutObject(params)
	if err != nil {
		log.Printf("Failed to put object %s into bucket %s with a key of %s\n", pathToFile, asbs.BucketName, fileName)
		log.Println(err)
		return err
	}

	return nil
}

// Get the name to be used for the .zip archive without the .zip extension.
func (asbs *AmazonS3BackupService) ArtifactFilename(jobName string) string {
	return jobName
}

// Get a friendly name for the email template of where this backup was stored. In this case, the name of the S3 bucket.
func (asbs *AmazonS3BackupService) BackupLocation() string {
	return fmt.Sprintf("S3 Bucket: %s", asbs.BucketName)
}

// Create the S3 bucket.
func (asbs *AmazonS3BackupService) createBucket(bucketName string) error {
	asbs.setEnvvars()
	params := &s3.CreateBucketInput{
		Bucket: aws.String(bucketName),
	}

	_, err := asbs.S3Service.CreateBucket(params)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			if aerr.Code() == ERROR_CODE_INVALID_BUCKET_NAME {
				log.Printf("The specified bucket is not valid. Bucket name: %s\n", bucketName)
				log.Println(err)
				return err
			}
			// If the BucketAlreadyOwnedByYou error is raised then this bucket already exists.
			if aerr.Code() == ERROR_CODE_BUCKET_ALREADY_OWNED_BY_YOU {
				return nil
			}

		}
		return err
	}

	if err = asbs.S3Service.WaitUntilBucketExists(&s3.HeadBucketInput{Bucket: &bucketName}); err != nil {
		log.Printf("Failed to wait for bucket to exist %s, %s\n", bucketName, err)
		return err
	}

	return nil
}

func (asbs *AmazonS3BackupService) putBucketLifecycleConfiguration() error {
	params := &s3.PutBucketLifecycleConfigurationInput{
		Bucket: aws.String(asbs.BucketName),
		LifecycleConfiguration: &s3.BucketLifecycleConfiguration{
			Rules: []*s3.LifecycleRule{
				{
					Prefix: aws.String(""),
					Status: aws.String("Enabled"),
					ID:     aws.String("frosty-backup-retention-policy"),
					Expiration: &s3.LifecycleExpiration{
						Days: &asbs.RetentionDays,
					},
				},
			},
		},
	}

	_, err := asbs.S3Service.PutBucketLifecycleConfiguration(params)
	if err != nil {
		log.Printf("Failed to create bucket lifecycle configuration, %s.", err)
		return err
	}

	return nil
}

// Set the required AWS environment variables.
func (asbs *AmazonS3BackupService) setEnvvars() {
	os.Setenv(ENVVAR_AWS_ACCESS_KEY_ID, asbs.AccessKeyId)
	os.Setenv(ENVVAR_AWS_SECRET_ACCESS_KEY, asbs.SecretAccessKey)
	os.Setenv(ENVVAR_AWS_REGION, asbs.Region)
}

// Get the name to use for the file being stored in S3.
func getObjectKey(fileName string) string {
	hostname, err := os.Hostname()
	if err != nil {
		log.Fatal("Could not determine hostname.", err)
		return ""
	}

	return fmt.Sprintf("%s/%s/%s_%s", hostname, time.Now().Format("20060102"), time.Now().Format("15:04:05"), fileName)
}
<|endoftext|>


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

package jujuc_test

import (
	"fmt"
	"io/ioutil"
	"path/filepath"

	"github.com/juju/cmd"
	jc "github.com/juju/testing/checkers"
	gc "gopkg.in/check.v1"

	"github.com/juju/juju/testing"
	"github.com/juju/juju/worker/uniter/runner/jujuc"
)

type RelationSetSuite struct {
	ContextSuite
}

var _ = gc.Suite(&RelationSetSuite{})

var helpTests = []struct {
	relid  int
	expect string
}{{-1, ""}, {0, "peer0:0"}}

func (s *RelationSetSuite) TestHelp(c *gc.C) {
	for i, t := range helpTests {
		c.Logf("test %d", i)
		hctx := s.GetHookContext(c, t.relid, "")
		com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
		c.Assert(err, jc.ErrorIsNil)
		ctx := testing.Context(c)
		code := cmd.Main(com, ctx, []string{"--help"})
		c.Assert(code, gc.Equals, 0)
		c.Assert(bufferString(ctx.Stdout), gc.Equals, fmt.Sprintf(`
usage: relation-set [options] key=value [key=value ...]
purpose: set relation settings

options:
--file (= "")
    file containing key-value pairs
--format (= "")
    deprecated format flag
-r, --relation  (= %s)
    specify a relation by id

"relation-set" writes the local unit's settings for some relation.
If no relation is specified then the current relation is used. The
setting values are not inspected and are stored as strings. Setting
an empty string causes the setting to be removed. Duplicate settings
are not allowed.

The --file option should be used when one or more key-value pairs are
too long to fit within the command length limit of the shell or
operating system. The file should contain key-value pairs in the same
format as on the commandline. They may also span multiple lines. Blank
lines and lines starting with # are ignored. Settings in the file will
be overridden by any duplicate key-value arguments.
`[1:], t.expect))
		c.Assert(bufferString(ctx.Stderr), gc.Equals, "")
	}
}

type relationSetInitTest struct {
	summary  string
	ctxrelid int
	args     []string
	content  string
	err      string
	relid    int
	settings map[string]string
}

func (t relationSetInitTest) log(c *gc.C, i int) {
	var summary string
	if t.summary != "" {
		summary = " - " + t.summary
	}
	c.Logf("test %d%s", i, summary)
}

func (t relationSetInitTest) filename() (string, int) {
	for i, arg := range t.args {
		next := i + 1
		if arg == "--file" && next < len(t.args) {
			return t.args[next], next
		}
	}
	return "", -1
}

func (t relationSetInitTest) init(c *gc.C, s *RelationSetSuite) (cmd.Command, []string) {
	args := make([]string, len(t.args))
	copy(args, t.args)
	if filename, i := t.filename(); filename != "" {
		filename = filepath.Join(c.MkDir(), filename)
		args[i] = filename
		err := ioutil.WriteFile(filename, []byte(t.content), 0644)
		c.Assert(err, jc.ErrorIsNil)
	}

	hctx := s.GetHookContext(c, t.ctxrelid, "")
	com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
	c.Assert(err, jc.ErrorIsNil)

	return com, args
}

func (t relationSetInitTest) check(c *gc.C, com cmd.Command, err error) {
	if t.err == "" {
		if !c.Check(err, jc.ErrorIsNil) {
			return
		}

		rset := com.(*jujuc.RelationSetCommand)
		c.Check(rset.RelationId, gc.Equals, t.relid)

		settings := t.settings
		if settings == nil {
			settings = map[string]string{}
		}
		c.Check(rset.Settings, jc.DeepEquals, settings)
	} else {
		c.Check(err, gc.ErrorMatches, t.err)
	}
}

var relationSetInitTests = []relationSetInitTest{
	{
	// compatibility: 0 args is valid.
	}, {
		ctxrelid: -1,
		err:      `no relation id specified`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "one"},
		err:      `invalid value "one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "one"},
		err:      `invalid value "one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "ignored:one"},
		err:      `invalid value "ignored:one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:one"},
		err:      `invalid value "ignored:one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "2"},
		err:      `invalid value "2" for flag -r: unknown relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:2"},
		err:      `invalid value "ignored:2" for flag -r: unknown relation id`,
	}, {
		ctxrelid: -1,
		err:      `no relation id specified`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:0"},
		relid:    0,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "0"},
		relid:    0,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "1"},
		relid:    1,
	}, {
		ctxrelid: 0,
		args:     []string{"-r", "1"},
		relid:    1,
	}, {
		ctxrelid: 1,
		args:     []string{"haha"},
		err:      `expected "key=value", got "haha"`,
	}, {
		ctxrelid: 1,
		args:     []string{"=haha"},
		err:      `expected "key=value", got "=haha"`,
	}, {
		ctxrelid: 1,
		args:     []string{"foo="},
		relid:    1,
		settings: map[string]string{"foo": ""},
	}, {
		ctxrelid: 1,
		args:     []string{"foo='"},
		relid:    1,
		settings: map[string]string{"foo": "'"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=bar"},
		relid:    1,
		settings: map[string]string{"foo": "bar"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=bar=baz=qux"},
		relid:    1,
		settings: map[string]string{"foo": "bar=baz=qux"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=foo: bar"},
		relid:    1,
		settings: map[string]string{"foo": "foo: bar"},
	}, {
		ctxrelid: 0,
		args:     []string{"-r", "1", "foo=bar"},
		relid:    1,
		settings: map[string]string{"foo": "bar"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=123", "bar=true", "baz=4.5", "qux="},
		relid:    1,
		settings: map[string]string{"foo": "123", "bar": "true", "baz": "4.5", "qux": ""},
	}, {
		summary:  "file with a valid setting",
		args:     []string{"--file", "spam"},
		content:  "foo=bar",
		settings: map[string]string{"foo": "bar"},
	}, {
		summary:  "an empty file",
		args:     []string{"--file", "spam"},
		content:  "",
		settings: map[string]string{},
	}, {
		summary: "an invalid file",
		args:    []string{"--file", "spam"},
		content: "haha",
		err:     `expected "key=value", got "haha"`,
	}, {
		summary: "an invalid file",
		args:    []string{"--file", "spam"},
		content: "=haha",
		err:     `expected "key=value", got "=haha"`,
	}, {
		summary:  "a messy file",
		args:     []string{"--file", "spam"},
		content:  "  \n # a comment \n\n  \nfoo=bar  \nham=eggs\n\n good=bad\n",
		settings: map[string]string{"foo": "bar", "ham": "eggs", "good": "bad"},
	}, {
		summary:  "file + settings",
		args:     []string{"--file", "spam", "foo=bar"},
		content:  "ham=eggs",
		settings: map[string]string{"ham": "eggs", "foo": "bar"},
	}, {
		summary:  "file overridden by settings",
		args:     []string{"--file", "spam", "foo=bar"},
		content:  "foo=baz",
		settings: map[string]string{"foo": "bar"},
	},
}

func (s *RelationSetSuite) TestInit(c *gc.C) {
	for i, t := range relationSetInitTests {
		t.log(c, i)
		com, args := t.init(c, s)

		err := testing.InitCommand(com, args)

		t.check(c, com, err)
	}
}

// Tests start with a relation with the settings {"base": "value"}
var relationSetRunTests = []struct {
	change map[string]string
	expect Settings
}{
	{
		map[string]string{"base": ""},
		Settings{},
	}, {
		map[string]string{"foo": "bar"},
		Settings{"base": "value", "foo": "bar"},
	}, {
		map[string]string{"base": "changed"},
		Settings{"base": "changed"},
	},
}

func (s *RelationSetSuite) TestRun(c *gc.C) {
	hctx := s.GetHookContext(c, 0, "")
	for i, t := range relationSetRunTests {
		c.Logf("test %d", i)

		pristine := Settings{"pristine": "untouched"}
		hctx.rels[0].units["u/0"] = pristine
		basic := Settings{"base": "value"}
		hctx.rels[1].units["u/0"] = basic

		// Run the command.
		com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
		c.Assert(err, jc.ErrorIsNil)
		rset := com.(*jujuc.RelationSetCommand)
		rset.RelationId = 1
		rset.Settings = t.change
		ctx := testing.Context(c)
		err = com.Run(ctx)
		c.Assert(err, jc.ErrorIsNil)

		// Check changes.
		c.Assert(hctx.rels[0].units["u/0"], gc.DeepEquals, pristine)
		c.Assert(hctx.rels[1].units["u/0"], gc.DeepEquals, t.expect)
	}
}

func (s *RelationSetSuite) TestRunDeprecationWarning(c *gc.C) {
	hctx := s.GetHookContext(c, 0, "")
	com, _ := jujuc.NewCommand(hctx, cmdString("relation-set"))

	// The rel= is needed to make this a valid command.
	ctx, err := testing.RunCommand(c, com, "--format", "foo", "rel=")

	c.Assert(err, jc.ErrorIsNil)
	c.Assert(testing.Stdout(ctx), gc.Equals, "")
	c.Assert(testing.Stderr(ctx), gc.Equals, "--format flag deprecated for command \"relation-set\"")
}
<commit_msg>Add one more test.<commit_after>// Copyright 2012, 2013 Canonical Ltd.
// Copyright 2014 Cloudbase Solutions SRL
// Licensed under the AGPLv3, see LICENCE file for details.

package jujuc_test

import (
	"fmt"
	"io/ioutil"
	"path/filepath"

	"github.com/juju/cmd"
	jc "github.com/juju/testing/checkers"
	gc "gopkg.in/check.v1"

	"github.com/juju/juju/testing"
	"github.com/juju/juju/worker/uniter/runner/jujuc"
)

type RelationSetSuite struct {
	ContextSuite
}

var _ = gc.Suite(&RelationSetSuite{})

var helpTests = []struct {
	relid  int
	expect string
}{{-1, ""}, {0, "peer0:0"}}

func (s *RelationSetSuite) TestHelp(c *gc.C) {
	for i, t := range helpTests {
		c.Logf("test %d", i)
		hctx := s.GetHookContext(c, t.relid, "")
		com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
		c.Assert(err, jc.ErrorIsNil)
		ctx := testing.Context(c)
		code := cmd.Main(com, ctx, []string{"--help"})
		c.Assert(code, gc.Equals, 0)
		c.Assert(bufferString(ctx.Stdout), gc.Equals, fmt.Sprintf(`
usage: relation-set [options] key=value [key=value ...]
purpose: set relation settings

options:
--file (= "")
    file containing key-value pairs
--format (= "")
    deprecated format flag
-r, --relation  (= %s)
    specify a relation by id

"relation-set" writes the local unit's settings for some relation.
If no relation is specified then the current relation is used. The
setting values are not inspected and are stored as strings. Setting
an empty string causes the setting to be removed. Duplicate settings
are not allowed.

The --file option should be used when one or more key-value pairs are
too long to fit within the command length limit of the shell or
operating system. The file should contain key-value pairs in the same
format as on the commandline. They may also span multiple lines. Blank
lines and lines starting with # are ignored. Settings in the file will
be overridden by any duplicate key-value arguments.
`[1:], t.expect))
		c.Assert(bufferString(ctx.Stderr), gc.Equals, "")
	}
}

type relationSetInitTest struct {
	summary  string
	ctxrelid int
	args     []string
	content  string
	err      string
	relid    int
	settings map[string]string
}

func (t relationSetInitTest) log(c *gc.C, i int) {
	var summary string
	if t.summary != "" {
		summary = " - " + t.summary
	}
	c.Logf("test %d%s", i, summary)
}

func (t relationSetInitTest) filename() (string, int) {
	for i, arg := range t.args {
		next := i + 1
		if arg == "--file" && next < len(t.args) {
			return t.args[next], next
		}
	}
	return "", -1
}

func (t relationSetInitTest) init(c *gc.C, s *RelationSetSuite) (cmd.Command, []string) {
	args := make([]string, len(t.args))
	copy(args, t.args)
	if filename, i := t.filename(); filename != "" {
		filename = filepath.Join(c.MkDir(), filename)
		args[i] = filename
		err := ioutil.WriteFile(filename, []byte(t.content), 0644)
		c.Assert(err, jc.ErrorIsNil)
	}

	hctx := s.GetHookContext(c, t.ctxrelid, "")
	com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
	c.Assert(err, jc.ErrorIsNil)

	return com, args
}

func (t relationSetInitTest) check(c *gc.C, com cmd.Command, err error) {
	if t.err == "" {
		if !c.Check(err, jc.ErrorIsNil) {
			return
		}

		rset := com.(*jujuc.RelationSetCommand)
		c.Check(rset.RelationId, gc.Equals, t.relid)

		settings := t.settings
		if settings == nil {
			settings = map[string]string{}
		}
		c.Check(rset.Settings, jc.DeepEquals, settings)
	} else {
		c.Check(err, gc.ErrorMatches, t.err)
	}
}

var relationSetInitTests = []relationSetInitTest{
	{
	// compatibility: 0 args is valid.
	}, {
		ctxrelid: -1,
		err:      `no relation id specified`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "one"},
		err:      `invalid value "one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "one"},
		err:      `invalid value "one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "ignored:one"},
		err:      `invalid value "ignored:one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:one"},
		err:      `invalid value "ignored:one" for flag -r: invalid relation id`,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "2"},
		err:      `invalid value "2" for flag -r: unknown relation id`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:2"},
		err:      `invalid value "ignored:2" for flag -r: unknown relation id`,
	}, {
		ctxrelid: -1,
		err:      `no relation id specified`,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "ignored:0"},
		relid:    0,
	}, {
		ctxrelid: 1,
		args:     []string{"-r", "0"},
		relid:    0,
	}, {
		ctxrelid: -1,
		args:     []string{"-r", "1"},
		relid:    1,
	}, {
		ctxrelid: 0,
		args:     []string{"-r", "1"},
		relid:    1,
	}, {
		ctxrelid: 1,
		args:     []string{"haha"},
		err:      `expected "key=value", got "haha"`,
	}, {
		ctxrelid: 1,
		args:     []string{"=haha"},
		err:      `expected "key=value", got "=haha"`,
	}, {
		ctxrelid: 1,
		args:     []string{"foo="},
		relid:    1,
		settings: map[string]string{"foo": ""},
	}, {
		ctxrelid: 1,
		args:     []string{"foo='"},
		relid:    1,
		settings: map[string]string{"foo": "'"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=bar"},
		relid:    1,
		settings: map[string]string{"foo": "bar"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=bar=baz=qux"},
		relid:    1,
		settings: map[string]string{"foo": "bar=baz=qux"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=foo: bar"},
		relid:    1,
		settings: map[string]string{"foo": "foo: bar"},
	}, {
		ctxrelid: 0,
		args:     []string{"-r", "1", "foo=bar"},
		relid:    1,
		settings: map[string]string{"foo": "bar"},
	}, {
		ctxrelid: 1,
		args:     []string{"foo=123", "bar=true", "baz=4.5", "qux="},
		relid:    1,
		settings: map[string]string{"foo": "123", "bar": "true", "baz": "4.5", "qux": ""},
	}, {
		summary:  "file with a valid setting",
		args:     []string{"--file", "spam"},
		content:  "foo=bar",
		settings: map[string]string{"foo": "bar"},
	}, {
		summary:  "an empty file",
		args:     []string{"--file", "spam"},
		content:  "",
		settings: map[string]string{},
	}, {
		summary: "an invalid file",
		args:    []string{"--file", "spam"},
		content: "haha",
		err:     `expected "key=value", got "haha"`,
	}, {
		summary: "an invalid file",
		args:    []string{"--file", "spam"},
		content: "=haha",
		err:     `expected "key=value", got "=haha"`,
	}, {
		summary:  "value with a space",
		args:     []string{"--file", "spam"},
		content:  "foo=foo: bar",
		settings: map[string]string{"foo": "foo: bar"},
	}, {
		summary:  "a messy file",
		args:     []string{"--file", "spam"},
		content:  "  \n # a comment \n\n  \nfoo=bar  \nham=eggs\n\n good=bad\n",
		settings: map[string]string{"foo": "bar", "ham": "eggs", "good": "bad"},
	}, {
		summary:  "file + settings",
		args:     []string{"--file", "spam", "foo=bar"},
		content:  "ham=eggs",
		settings: map[string]string{"ham": "eggs", "foo": "bar"},
	}, {
		summary:  "file overridden by settings",
		args:     []string{"--file", "spam", "foo=bar"},
		content:  "foo=baz",
		settings: map[string]string{"foo": "bar"},
	},
}

func (s *RelationSetSuite) TestInit(c *gc.C) {
	for i, t := range relationSetInitTests {
		t.log(c, i)
		com, args := t.init(c, s)

		err := testing.InitCommand(com, args)

		t.check(c, com, err)
	}
}

// Tests start with a relation with the settings {"base": "value"}
var relationSetRunTests = []struct {
	change map[string]string
	expect Settings
}{
	{
		map[string]string{"base": ""},
		Settings{},
	}, {
		map[string]string{"foo": "bar"},
		Settings{"base": "value", "foo": "bar"},
	}, {
		map[string]string{"base": "changed"},
		Settings{"base": "changed"},
	},
}

func (s *RelationSetSuite) TestRun(c *gc.C) {
	hctx := s.GetHookContext(c, 0, "")
	for i, t := range relationSetRunTests {
		c.Logf("test %d", i)

		pristine := Settings{"pristine": "untouched"}
		hctx.rels[0].units["u/0"] = pristine
		basic := Settings{"base": "value"}
		hctx.rels[1].units["u/0"] = basic

		// Run the command.
		com, err := jujuc.NewCommand(hctx, cmdString("relation-set"))
		c.Assert(err, jc.ErrorIsNil)
		rset := com.(*jujuc.RelationSetCommand)
		rset.RelationId = 1
		rset.Settings = t.change
		ctx := testing.Context(c)
		err = com.Run(ctx)
		c.Assert(err, jc.ErrorIsNil)

		// Check changes.
		c.Assert(hctx.rels[0].units["u/0"], gc.DeepEquals, pristine)
		c.Assert(hctx.rels[1].units["u/0"], gc.DeepEquals, t.expect)
	}
}

func (s *RelationSetSuite) TestRunDeprecationWarning(c *gc.C) {
	hctx := s.GetHookContext(c, 0, "")
	com, _ := jujuc.NewCommand(hctx, cmdString("relation-set"))

	// The rel= is needed to make this a valid command.
	ctx, err := testing.RunCommand(c, com, "--format", "foo", "rel=")

	c.Assert(err, jc.ErrorIsNil)
	c.Assert(testing.Stdout(ctx), gc.Equals, "")
	c.Assert(testing.Stderr(ctx), gc.Equals, "--format flag deprecated for command \"relation-set\"")
}
<|endoftext|>


<commit_before>package keeper

import (
	"context"
	"fmt"
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/zigbee-alliance/distributed-compliance-ledger/x/compliance/types"
	dclauthtypes "github.com/zigbee-alliance/distributed-compliance-ledger/x/dclauth/types"
	modeltypes "github.com/zigbee-alliance/distributed-compliance-ledger/x/model/types"
)

func (k msgServer) CertifyModel(goCtx context.Context, msg *types.MsgCertifyModel) (*types.MsgCertifyModelResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	signerAddr, err := sdk.AccAddressFromBech32(msg.Signer)
	if err != nil {
		return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid Address: (%s)", err)
	}

	// check if sender has enough rights to certify model
	// sender must have CertificationCenter role to certify/revoke model
	if !k.dclauthKeeper.HasRole(ctx, signerAddr, dclauthtypes.CertificationCenter) {
		return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized,
			"MsgAddTestingResult transaction should be signed by an account with the %s role",
			dclauthtypes.CertificationCenter,
		)
	}

	// The corresponding Model Version must be present on ledger.
	modelVersion, found := k.modelKeeper.GetModelVersion(ctx, msg.Vid, msg.Pid, msg.SoftwareVersion)
	if !found {
		return nil, modeltypes.NewErrModelVersionDoesNotExist(msg.Vid, msg.Pid, msg.SoftwareVersion)
	}

	// check if softwareVersionString matches with what is stored for the given version
	if modelVersion.SoftwareVersionString != msg.SoftwareVersionString {
		return nil, types.NewErrModelVersionStringDoesNotMatch(msg.Vid, msg.Pid, msg.SoftwareVersion, msg.SoftwareVersionString)
	}

	complianceInfo, found := k.GetComplianceInfo(ctx, msg.Vid, msg.Pid, msg.SoftwareVersion, msg.CertificationType)
	// nolint:nestif
	if found {
		// Compliance record already exist. Cases:
		// 1) We want to re-certify compliance which is already certified now -> Error.
		// 2) We want to certify provisioned compliance. So no revocations were done earlier and thus certification
		// will be tracked on ledger. So we have to ensure that there are tesing results at first.
		// 3) We want to certify revoked compliance. Either earlier certification was done before revocation
		// and thus the corresponding test results are already on the ledger, or only revocation is tracked on the ledger
		// and so the test results are not required to be present.

		// check if compliance is already in certified state
		if complianceInfo.SoftwareVersionCertificationStatus == types.CodeCertified {
			return nil, types.NewErrAlreadyCertified(msg.Vid, msg.Pid, msg.SoftwareVersion, msg.CertificationType)
		}

		// if state changes on `certified` check that certification date is after provisional/revocation date
		newDate, err := time.Parse(time.RFC3339, msg.CertificationDate)
		if err != nil {
			return nil, types.NewErrInvalidTestDateFormat(msg.CertificationDate)
		}
		oldDate, err := time.Parse(time.RFC3339, complianceInfo.Date)
		if err != nil {
			return nil, types.NewErrInvalidTestDateFormat(complianceInfo.Date)
		}
		if newDate.Before(oldDate) {
			return nil, types.NewErrInconsistentDates(
				fmt.Sprintf("The `certification_date`:%v must be after the current `date`:%v to "+
					"certify model", msg.CertificationDate, complianceInfo.Date),
			)
		}

		complianceInfo.SetCertifiedStatus(msg.CertificationDate, msg.Reason, msg.CDCertificationId)
	} else {
		// There is no compliance record yet. So certification will be tracked on ledger.

		complianceInfo = types.ComplianceInfo{
			Vid:                                msg.Vid,
			Pid:                                msg.Pid,
			SoftwareVersion:                    msg.SoftwareVersion,
			SoftwareVersionString:              msg.SoftwareVersionString,
			CertificationType:                  msg.CertificationType,
			Date:                               msg.CertificationDate,
			Reason:                             msg.Reason,
			Owner:                              msg.Signer,
			SoftwareVersionCertificationStatus: types.CodeCertified,
			History:                            []*types.ComplianceHistoryItem{},
			CDVersionNumber:                    msg.CDVersionNumber,
			CDCertificationId:                  msg.CDCertificationId,
		}
	}

	complianceInfo.SetOptionalFields(msg)

	// store compliance info
	k.SetComplianceInfo(ctx, complianceInfo)

	deviceSoftwareCompliance, _ := k.GetDeviceSoftwareCompliance(ctx, msg.CDCertificationId)
	deviceSoftwareCompliance.CDCertificateId = msg.CDCertificationId
	deviceSoftwareCompliance.ComplianceInfo = append(deviceSoftwareCompliance.ComplianceInfo, &complianceInfo)

	// store device compliance software
	k.SetDeviceSoftwareCompliance(ctx, deviceSoftwareCompliance)

	// update certified, revoked and provisional index
	certifiedModel := types.CertifiedModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             true,
	}
	k.SetCertifiedModel(ctx, certifiedModel)
	revokedModel := types.RevokedModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             false,
	}
	k.SetRevokedModel(ctx, revokedModel)
	provisionalModel := types.ProvisionalModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             false,
	}
	k.SetProvisionalModel(ctx, provisionalModel)

	return &types.MsgCertifyModelResponse{}, nil
}
<commit_msg>Add implementation for saving DeviceSoftwareCompliance in certify model txn<commit_after>package keeper

import (
	"context"
	"fmt"
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/zigbee-alliance/distributed-compliance-ledger/x/compliance/types"
	dclauthtypes "github.com/zigbee-alliance/distributed-compliance-ledger/x/dclauth/types"
	modeltypes "github.com/zigbee-alliance/distributed-compliance-ledger/x/model/types"
)

func (k msgServer) CertifyModel(goCtx context.Context, msg *types.MsgCertifyModel) (*types.MsgCertifyModelResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	signerAddr, err := sdk.AccAddressFromBech32(msg.Signer)
	if err != nil {
		return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid Address: (%s)", err)
	}

	// check if sender has enough rights to certify model
	// sender must have CertificationCenter role to certify/revoke model
	if !k.dclauthKeeper.HasRole(ctx, signerAddr, dclauthtypes.CertificationCenter) {
		return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized,
			"MsgAddTestingResult transaction should be signed by an account with the %s role",
			dclauthtypes.CertificationCenter,
		)
	}

	// The corresponding Model Version must be present on ledger.
	modelVersion, found := k.modelKeeper.GetModelVersion(ctx, msg.Vid, msg.Pid, msg.SoftwareVersion)
	if !found {
		return nil, modeltypes.NewErrModelVersionDoesNotExist(msg.Vid, msg.Pid, msg.SoftwareVersion)
	}

	// check if softwareVersionString matches with what is stored for the given version
	if modelVersion.SoftwareVersionString != msg.SoftwareVersionString {
		return nil, types.NewErrModelVersionStringDoesNotMatch(msg.Vid, msg.Pid, msg.SoftwareVersion, msg.SoftwareVersionString)
	}

	complianceInfo, found := k.GetComplianceInfo(ctx, msg.Vid, msg.Pid, msg.SoftwareVersion, msg.CertificationType)
	// nolint:nestif
	if found {
		// Compliance record already exist. Cases:
		// 1) We want to re-certify compliance which is already certified now -> Error.
		// 2) We want to certify provisioned compliance. So no revocations were done earlier and thus certification
		// will be tracked on ledger. So we have to ensure that there are tesing results at first.
		// 3) We want to certify revoked compliance. Either earlier certification was done before revocation
		// and thus the corresponding test results are already on the ledger, or only revocation is tracked on the ledger
		// and so the test results are not required to be present.

		// check if compliance is already in certified state
		if complianceInfo.SoftwareVersionCertificationStatus == types.CodeCertified {
			return nil, types.NewErrAlreadyCertified(msg.Vid, msg.Pid, msg.SoftwareVersion, msg.CertificationType)
		}

		// if state changes on `certified` check that certification date is after provisional/revocation date
		newDate, err := time.Parse(time.RFC3339, msg.CertificationDate)
		if err != nil {
			return nil, types.NewErrInvalidTestDateFormat(msg.CertificationDate)
		}
		oldDate, err := time.Parse(time.RFC3339, complianceInfo.Date)
		if err != nil {
			return nil, types.NewErrInvalidTestDateFormat(complianceInfo.Date)
		}
		if newDate.Before(oldDate) {
			return nil, types.NewErrInconsistentDates(
				fmt.Sprintf("The `certification_date`:%v must be after the current `date`:%v to "+
					"certify model", msg.CertificationDate, complianceInfo.Date),
			)
		}

		complianceInfo.SetCertifiedStatus(msg.CertificationDate, msg.Reason, msg.CDCertificationId)
	} else {
		// There is no compliance record yet. So certification will be tracked on ledger.

		complianceInfo = types.ComplianceInfo{
			Vid:                                msg.Vid,
			Pid:                                msg.Pid,
			SoftwareVersion:                    msg.SoftwareVersion,
			SoftwareVersionString:              msg.SoftwareVersionString,
			CertificationType:                  msg.CertificationType,
			Date:                               msg.CertificationDate,
			Reason:                             msg.Reason,
			Owner:                              msg.Signer,
			SoftwareVersionCertificationStatus: types.CodeCertified,
			History:                            []*types.ComplianceHistoryItem{},
			CDVersionNumber:                    msg.CDVersionNumber,
			CDCertificationId:                  msg.CDCertificationId,
		}
	}

	complianceInfo.SetOptionalFields(msg)

	// store compliance info
	k.SetComplianceInfo(ctx, complianceInfo)

	deviceSoftwareCompliance, found := k.GetDeviceSoftwareCompliance(ctx, msg.CDCertificationId)
	if !found {
		deviceSoftwareCompliance.CDCertificateId = msg.CDCertificationId
	}
	deviceSoftwareCompliance.ComplianceInfo = append(deviceSoftwareCompliance.ComplianceInfo, &complianceInfo)

	// store device compliance software
	k.SetDeviceSoftwareCompliance(ctx, deviceSoftwareCompliance)

	// update certified, revoked and provisional index
	certifiedModel := types.CertifiedModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             true,
	}
	k.SetCertifiedModel(ctx, certifiedModel)
	revokedModel := types.RevokedModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             false,
	}
	k.SetRevokedModel(ctx, revokedModel)
	provisionalModel := types.ProvisionalModel{
		Vid:               msg.Vid,
		Pid:               msg.Pid,
		SoftwareVersion:   msg.SoftwareVersion,
		CertificationType: msg.CertificationType,
		Value:             false,
	}
	k.SetProvisionalModel(ctx, provisionalModel)

	return &types.MsgCertifyModelResponse{}, nil
}
<|endoftext|>


<commit_before>package core

import (
	"context"
	"encoding/json"
	"expvar"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"chain/core/fetch"
	"chain/core/leader"
	"chain/core/mockhsm"
	"chain/core/txdb"
	"chain/crypto/ed25519"
	"chain/crypto/ed25519/hd25519"
	"chain/database/pg"
	"chain/database/sql"
	"chain/errors"
	"chain/net/http/httpjson"
	"chain/net/rpc"
	"chain/protocol"
	"chain/protocol/state"
)

var (
	errAlreadyConfigured = errors.New("core is already configured; must reset first")
	errUnconfigured      = errors.New("core is not configured")
	errBadGenerator      = errors.New("generator returned an unsuccessful response")
	errBadBlockXPub      = errors.New("supplied block xpub is invalid")
)

// reserved mockhsm key alias
const autoBlockKeyAlias = "_CHAIN_CORE_AUTO_BLOCK_KEY"

func isProduction() bool {
	return expvar.Get("buildtag").String() != `"dev"`
}

// errProdReset is returned when reset is called on a
// production system.
var errProdReset = errors.New("reset called on production system")

// Reset deletes all data, resulting in an unconfigured core.
// It must be called before any other functions in this package.
func Reset(ctx context.Context, db pg.DB) error {
	if isProduction() {
		return errors.Wrap(errProdReset)
	}

	const q = `
		TRUNCATE
			account_control_programs,
			account_utxos,
			accounts,
			annotated_accounts,
			annotated_assets,
			annotated_outputs,
			annotated_txs,
			asset_tags,
			assets,
			blocks,
			config,
			generator_pending_block,
			leader,
			mockhsm,
			pool_txs,
			query_blocks,
			reservations,
			signed_blocks,
			signers,
			snapshots
			RESTART IDENTITY;
	`

	_, err := db.Exec(ctx, q)
	return errors.Wrap(err)
}

func (a *api) reset(ctx context.Context) error {
	if isProduction() {
		return errors.Wrap(errProdReset)
	}

	w := httpjson.ResponseWriter(ctx)
	closeConnOK(w)
	execSelf("RESET=true")
	panic("unreached")
}

func (a *api) info(ctx context.Context) (map[string]interface{}, error) {
	if a.config == nil {
		// never configured
		return map[string]interface{}{
			"is_configured": false,
		}, nil
	}
	if leader.IsLeading() {
		return a.leaderInfo(ctx)
	} else {
		var resp map[string]interface{}
		err := callLeader(ctx, "/info", nil, &resp)
		return resp, err
	}
}

func (a *api) leaderInfo(ctx context.Context) (map[string]interface{}, error) {
	localHeight := a.c.Height()
	var (
		generatorHeight  uint64
		generatorFetched time.Time
	)
	if a.config.IsGenerator {
		generatorHeight = localHeight
		generatorFetched = time.Now()
	} else {
		generatorHeight, generatorFetched = fetch.GeneratorHeight()
	}

	buildCommit := json.RawMessage(expvar.Get("buildcommit").String())
	buildDate := json.RawMessage(expvar.Get("builddate").String())

	// Because everything is asynchronous, it's possible for the localHeight to
	// be higher than our cached generator height. In that case, display the
	// generatorHeight as our height.
	if localHeight > generatorHeight {
		generatorHeight = localHeight
	}

	return map[string]interface{}{
		"is_configured":                     true,
		"configured_at":                     a.config.ConfiguredAt,
		"is_signer":                         a.config.IsSigner,
		"is_generator":                      a.config.IsGenerator,
		"generator_url":                     a.config.GeneratorURL,
		"initial_block_hash":                a.config.InitialBlockHash,
		"block_height":                      localHeight,
		"generator_block_height":            generatorHeight,
		"generator_block_height_fetched_at": generatorFetched,
		"is_production":                     isProduction(),
		"build_commit":                      &buildCommit,
		"build_date":                        &buildDate,
	}, nil
}

// Configure configures the core by writing to the database.
// If running in a cored process,
// the caller must ensure that the new configuration is properly reloaded,
// for example by restarting the process.
//
// If c.IsSigner is true, Configure generates a new mockhsm keypair
// for signing blocks, and assigns it to c.BlockXPub.
//
// If c.IsGenerator is true, Configure creates an initial block,
// saves it, and assigns its hash to c.InitialBlockHash.
// Otherwise, c.IsGenerator is false, and Configure makes a test request
// to GeneratorURL to detect simple configuration mistakes.
func Configure(ctx context.Context, db pg.DB, c *Config) error {
	var err error
	if !c.IsGenerator {
		err = tryGenerator(ctx, c.GeneratorURL, c.InitialBlockHash.String())
		if err != nil {
			return err
		}
	}

	var signingKeys []ed25519.PublicKey
	if c.IsSigner {
		var blockXPub *hd25519.XPub
		if c.BlockXPub == "" {
			hsm := mockhsm.New(db)
			coreXPub, created, err := hsm.GetOrCreateKey(ctx, autoBlockKeyAlias)
			if err != nil {
				return err
			}
			blockXPub = coreXPub.XPub
			if created {
				log.Printf("Generated new block-signing key %s\n", blockXPub.String())
			} else {
				log.Printf("Using block-signing key %s\n", blockXPub.String())
			}
			c.BlockXPub = blockXPub.String()
		} else {
			blockXPub, err = hd25519.XPubFromString(c.BlockXPub)
			if err != nil {
				return errors.Wrap(errBadBlockXPub, err.Error())
			}
		}
		signingKeys = append(signingKeys, blockXPub.Key)
	}

	if c.IsGenerator {
		block, err := protocol.NewInitialBlock(signingKeys, 1, time.Now())
		if err != nil {
			return err
		}
		store, pool := txdb.New(db.(*sql.DB))
		chain, err := protocol.NewChain(ctx, store, pool, nil)
		if err != nil {
			return err
		}

		err = chain.CommitBlock(ctx, block, state.Empty())
		if err != nil {
			return err
		}

		c.InitialBlockHash = block.Hash()
	}

	const q = `
		INSERT INTO config (is_signer, block_xpub, is_generator, initial_block_hash, generator_url, configured_at)
		VALUES ($1, $2, $3, $4, $5, NOW())
	`
	_, err = db.Exec(ctx, q, c.IsSigner, c.BlockXPub, c.IsGenerator, c.InitialBlockHash, c.GeneratorURL)
	return err
}

func configure(ctx context.Context, x *Config) error {
	err := Configure(ctx, pg.FromContext(ctx), x)
	if err != nil {
		return err
	}

	w := httpjson.ResponseWriter(ctx)
	closeConnOK(w)
	execSelf()
	panic("unreached")
}

func tryGenerator(ctx context.Context, url, blockchainID string) error {
	client := &rpc.Client{
		BaseURL:      url,
		BlockchainID: blockchainID,
	}
	var x struct {
		BlockHeight uint64 `json:"block_height"`
	}
	err := client.Call(ctx, "/rpc/block-height", nil, &x)
	if err != nil {
		return errors.Wrap(errBadGenerator, err.Error())
	}

	if x.BlockHeight < 1 {
		return errBadGenerator
	}

	return nil
}

func closeConnOK(w http.ResponseWriter) {
	w.Header().Add("Connection", "close")
	w.WriteHeader(http.StatusNoContent)

	hijacker, ok := w.(http.Hijacker)
	if !ok {
		log.Printf("no hijacker")
		return
	}
	conn, buf, err := hijacker.Hijack()
	if err != nil {
		log.Printf("could not hijack connection: %s\n", err)
		return
	}
	err = buf.Flush()
	if err != nil {
		log.Printf("could not flush connection buffer: %s\n", err)
	}
	err = conn.Close()
	if err != nil {
		log.Printf("could not close connection: %s\n", err)
	}
}

// execSelf execs Args with environment values replaced
// by the ones in env.
func execSelf(env ...string) {
	binpath, err := exec.LookPath(os.Args[0])
	if err != nil {
		panic(err)
	}

	env = mergeEnvLists(env, os.Environ())
	err = syscall.Exec(binpath, os.Args, env)
	if err != nil {
		panic(err)
	}
}

// mergeEnvLists merges the two environment lists such that
// variables with the same name in "in" replace those in "out".
// This always returns a newly allocated slice.
func mergeEnvLists(in, out []string) []string {
	out = append([]string(nil), out...)
NextVar:
	for _, inkv := range in {
		k := strings.SplitAfterN(inkv, "=", 2)[0]
		for i, outkv := range out {
			if strings.HasPrefix(outkv, k) {
				out[i] = inkv
				continue NextVar
			}
		}
		out = append(out, inkv)
	}
	return out
}
<commit_msg>core: /info returns network RPC version<commit_after>package core

import (
	"context"
	"encoding/json"
	"expvar"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"chain/core/fetch"
	"chain/core/leader"
	"chain/core/mockhsm"
	"chain/core/txdb"
	"chain/crypto/ed25519"
	"chain/crypto/ed25519/hd25519"
	"chain/database/pg"
	"chain/database/sql"
	"chain/errors"
	"chain/net/http/httpjson"
	"chain/net/rpc"
	"chain/protocol"
	"chain/protocol/state"
)

var (
	errAlreadyConfigured = errors.New("core is already configured; must reset first")
	errUnconfigured      = errors.New("core is not configured")
	errBadGenerator      = errors.New("generator returned an unsuccessful response")
	errBadBlockXPub      = errors.New("supplied block xpub is invalid")
)

// reserved mockhsm key alias
const (
	networkRPCVersion = 1
	autoBlockKeyAlias = "_CHAIN_CORE_AUTO_BLOCK_KEY"
)

func isProduction() bool {
	return expvar.Get("buildtag").String() != `"dev"`
}

// errProdReset is returned when reset is called on a
// production system.
var errProdReset = errors.New("reset called on production system")

// Reset deletes all data, resulting in an unconfigured core.
// It must be called before any other functions in this package.
func Reset(ctx context.Context, db pg.DB) error {
	if isProduction() {
		return errors.Wrap(errProdReset)
	}

	const q = `
		TRUNCATE
			account_control_programs,
			account_utxos,
			accounts,
			annotated_accounts,
			annotated_assets,
			annotated_outputs,
			annotated_txs,
			asset_tags,
			assets,
			blocks,
			config,
			generator_pending_block,
			leader,
			mockhsm,
			pool_txs,
			query_blocks,
			reservations,
			signed_blocks,
			signers,
			snapshots
			RESTART IDENTITY;
	`

	_, err := db.Exec(ctx, q)
	return errors.Wrap(err)
}

func (a *api) reset(ctx context.Context) error {
	if isProduction() {
		return errors.Wrap(errProdReset)
	}

	w := httpjson.ResponseWriter(ctx)
	closeConnOK(w)
	execSelf("RESET=true")
	panic("unreached")
}

func (a *api) info(ctx context.Context) (map[string]interface{}, error) {
	if a.config == nil {
		// never configured
		return map[string]interface{}{
			"is_configured": false,
		}, nil
	}
	if leader.IsLeading() {
		return a.leaderInfo(ctx)
	} else {
		var resp map[string]interface{}
		err := callLeader(ctx, "/info", nil, &resp)
		return resp, err
	}
}

func (a *api) leaderInfo(ctx context.Context) (map[string]interface{}, error) {
	localHeight := a.c.Height()
	var (
		generatorHeight  uint64
		generatorFetched time.Time
	)
	if a.config.IsGenerator {
		generatorHeight = localHeight
		generatorFetched = time.Now()
	} else {
		generatorHeight, generatorFetched = fetch.GeneratorHeight()
	}

	buildCommit := json.RawMessage(expvar.Get("buildcommit").String())
	buildDate := json.RawMessage(expvar.Get("builddate").String())

	// Because everything is asynchronous, it's possible for the localHeight to
	// be higher than our cached generator height. In that case, display the
	// generatorHeight as our height.
	if localHeight > generatorHeight {
		generatorHeight = localHeight
	}

	return map[string]interface{}{
		"is_configured":                     true,
		"configured_at":                     a.config.ConfiguredAt,
		"is_signer":                         a.config.IsSigner,
		"is_generator":                      a.config.IsGenerator,
		"generator_url":                     a.config.GeneratorURL,
		"initial_block_hash":                a.config.InitialBlockHash,
		"block_height":                      localHeight,
		"generator_block_height":            generatorHeight,
		"generator_block_height_fetched_at": generatorFetched,
		"is_production":                     isProduction(),
		"network_rpc_version":               networkRPCVersion,
		"build_commit":                      &buildCommit,
		"build_date":                        &buildDate,
	}, nil
}

// Configure configures the core by writing to the database.
// If running in a cored process,
// the caller must ensure that the new configuration is properly reloaded,
// for example by restarting the process.
//
// If c.IsSigner is true, Configure generates a new mockhsm keypair
// for signing blocks, and assigns it to c.BlockXPub.
//
// If c.IsGenerator is true, Configure creates an initial block,
// saves it, and assigns its hash to c.InitialBlockHash.
// Otherwise, c.IsGenerator is false, and Configure makes a test request
// to GeneratorURL to detect simple configuration mistakes.
func Configure(ctx context.Context, db pg.DB, c *Config) error {
	var err error
	if !c.IsGenerator {
		err = tryGenerator(ctx, c.GeneratorURL, c.InitialBlockHash.String())
		if err != nil {
			return err
		}
	}

	var signingKeys []ed25519.PublicKey
	if c.IsSigner {
		var blockXPub *hd25519.XPub
		if c.BlockXPub == "" {
			hsm := mockhsm.New(db)
			coreXPub, created, err := hsm.GetOrCreateKey(ctx, autoBlockKeyAlias)
			if err != nil {
				return err
			}
			blockXPub = coreXPub.XPub
			if created {
				log.Printf("Generated new block-signing key %s\n", blockXPub.String())
			} else {
				log.Printf("Using block-signing key %s\n", blockXPub.String())
			}
			c.BlockXPub = blockXPub.String()
		} else {
			blockXPub, err = hd25519.XPubFromString(c.BlockXPub)
			if err != nil {
				return errors.Wrap(errBadBlockXPub, err.Error())
			}
		}
		signingKeys = append(signingKeys, blockXPub.Key)
	}

	if c.IsGenerator {
		block, err := protocol.NewInitialBlock(signingKeys, 1, time.Now())
		if err != nil {
			return err
		}
		store, pool := txdb.New(db.(*sql.DB))
		chain, err := protocol.NewChain(ctx, store, pool, nil)
		if err != nil {
			return err
		}

		err = chain.CommitBlock(ctx, block, state.Empty())
		if err != nil {
			return err
		}

		c.InitialBlockHash = block.Hash()
	}

	const q = `
		INSERT INTO config (is_signer, block_xpub, is_generator, initial_block_hash, generator_url, configured_at)
		VALUES ($1, $2, $3, $4, $5, NOW())
	`
	_, err = db.Exec(ctx, q, c.IsSigner, c.BlockXPub, c.IsGenerator, c.InitialBlockHash, c.GeneratorURL)
	return err
}

func configure(ctx context.Context, x *Config) error {
	err := Configure(ctx, pg.FromContext(ctx), x)
	if err != nil {
		return err
	}

	w := httpjson.ResponseWriter(ctx)
	closeConnOK(w)
	execSelf()
	panic("unreached")
}

func tryGenerator(ctx context.Context, url, blockchainID string) error {
	client := &rpc.Client{
		BaseURL:      url,
		BlockchainID: blockchainID,
	}
	var x struct {
		BlockHeight uint64 `json:"block_height"`
	}
	err := client.Call(ctx, "/rpc/block-height", nil, &x)
	if err != nil {
		return errors.Wrap(errBadGenerator, err.Error())
	}

	if x.BlockHeight < 1 {
		return errBadGenerator
	}

	return nil
}

func closeConnOK(w http.ResponseWriter) {
	w.Header().Add("Connection", "close")
	w.WriteHeader(http.StatusNoContent)

	hijacker, ok := w.(http.Hijacker)
	if !ok {
		log.Printf("no hijacker")
		return
	}
	conn, buf, err := hijacker.Hijack()
	if err != nil {
		log.Printf("could not hijack connection: %s\n", err)
		return
	}
	err = buf.Flush()
	if err != nil {
		log.Printf("could not flush connection buffer: %s\n", err)
	}
	err = conn.Close()
	if err != nil {
		log.Printf("could not close connection: %s\n", err)
	}
}

// execSelf execs Args with environment values replaced
// by the ones in env.
func execSelf(env ...string) {
	binpath, err := exec.LookPath(os.Args[0])
	if err != nil {
		panic(err)
	}

	env = mergeEnvLists(env, os.Environ())
	err = syscall.Exec(binpath, os.Args, env)
	if err != nil {
		panic(err)
	}
}

// mergeEnvLists merges the two environment lists such that
// variables with the same name in "in" replace those in "out".
// This always returns a newly allocated slice.
func mergeEnvLists(in, out []string) []string {
	out = append([]string(nil), out...)
NextVar:
	for _, inkv := range in {
		k := strings.SplitAfterN(inkv, "=", 2)[0]
		for i, outkv := range out {
			if strings.HasPrefix(outkv, k) {
				out[i] = inkv
				continue NextVar
			}
		}
		out = append(out, inkv)
	}
	return out
}
<|endoftext|>


<commit_before>package core

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"text/template"
)

var DefaultTmplFuncMap = template.FuncMap{
	"json": func(k string, v json.RawMessage) json.RawMessage {
		m := make(map[string]json.RawMessage)
		err := json.Unmarshal(v, &m)
		if err != nil {
			panic(err)
		}
		return jsonKey(k, m)
	},
	"str": func(v []byte) string {
		if bytes.IndexByte(v, '"') == 0 {
			v = bytes.Trim(v, "\"")
		}
		return fmt.Sprintf("%s\n", string(v))
	},
}

func readInput(path string) ([]byte, error) {
	var f *os.File
	if path == "" {
		f = os.Stdin
	} else if fp, err := os.Open(path); err != nil {
		return nil, err
	} else {
		f = fp
	}
	defer f.Close()
	return ioutil.ReadAll(f)
}

func readTemplate(path string) ([]byte, error) {
	data, err := ioutil.ReadFile(path)
	return data, err
}

func ToJSONMap(data []byte) (map[string]json.RawMessage, error) {
	contentMap := make(map[string]json.RawMessage)
	if err := json.Unmarshal(data, &contentMap); err != nil {
		return nil, err
	}
	return contentMap, nil
}

func traverseJSON(k string, jsonMap map[string]json.RawMessage) map[string]json.RawMessage {
	if msg, ok := jsonMap[k]; !ok {
		panic(fmt.Errorf("not valid key in json: %q", k))
	} else {
		m := make(map[string]json.RawMessage)
		err := json.Unmarshal(msg, &m)
		if err != nil {
			panic(err)
		}
		return m
	}
}

func jsonKey(key string, jsonMap map[string]json.RawMessage) json.RawMessage {
	cmp := strings.Split(key, ".")
	for i := 0; i < len(cmp)-1; i++ {
		jsonMap = traverseJSON(cmp[i], jsonMap)
	}
	return jsonMap[cmp[len(cmp)-1]]
}

func NewTemplate(data string) (*template.Template, error) {
	tmpl, err := template.New("json").Funcs(DefaultTmplFuncMap).Parse(data)
	return tmpl, err
}

func ApplyTemplate(tmpl *template.Template, ctx map[string]json.RawMessage) error {
	return tmpl.Execute(os.Stdout, ctx)
}
<commit_msg>remove newline when printing string value<commit_after>package core

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"text/template"
)

var DefaultTmplFuncMap = template.FuncMap{
	"json": func(k string, v json.RawMessage) json.RawMessage {
		m := make(map[string]json.RawMessage)
		err := json.Unmarshal(v, &m)
		if err != nil {
			panic(err)
		}
		return jsonKey(k, m)
	},
	"str": func(v []byte) string {
		if bytes.IndexByte(v, '"') == 0 {
			v = bytes.Trim(v, "\"")
		}
		return fmt.Sprintf("%s", string(v))
	},
}

func readInput(path string) ([]byte, error) {
	var f *os.File
	if path == "" {
		f = os.Stdin
	} else if fp, err := os.Open(path); err != nil {
		return nil, err
	} else {
		f = fp
	}
	defer f.Close()
	return ioutil.ReadAll(f)
}

func readTemplate(path string) ([]byte, error) {
	data, err := ioutil.ReadFile(path)
	return data, err
}

func ToJSONMap(data []byte) (map[string]json.RawMessage, error) {
	contentMap := make(map[string]json.RawMessage)
	if err := json.Unmarshal(data, &contentMap); err != nil {
		return nil, err
	}
	return contentMap, nil
}

func traverseJSON(k string, jsonMap map[string]json.RawMessage) map[string]json.RawMessage {
	if msg, ok := jsonMap[k]; !ok {
		panic(fmt.Errorf("not valid key in json: %q", k))
	} else {
		m := make(map[string]json.RawMessage)
		err := json.Unmarshal(msg, &m)
		if err != nil {
			panic(err)
		}
		return m
	}
}

func jsonKey(key string, jsonMap map[string]json.RawMessage) json.RawMessage {
	cmp := strings.Split(key, ".")
	for i := 0; i < len(cmp)-1; i++ {
		jsonMap = traverseJSON(cmp[i], jsonMap)
	}
	return jsonMap[cmp[len(cmp)-1]]
}

func NewTemplate(data string) (*template.Template, error) {
	tmpl, err := template.New("json").Funcs(DefaultTmplFuncMap).Parse(data)
	return tmpl, err
}

func ApplyTemplate(tmpl *template.Template, ctx map[string]json.RawMessage) error {
	return tmpl.Execute(os.Stdout, ctx)
}
<|endoftext|>


<commit_before>//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// 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.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//

package test

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"testing"

	driver "github.com/arangodb/go-driver"
)

type testEnv interface {
	Error(message ...interface{})
	Errorf(format string, args ...interface{})
	Fatal(message ...interface{})
	Fatalf(format string, args ...interface{})
	Log(message ...interface{})
	Logf(format string, args ...interface{})
}

// boolRef returns a reference to a given boolean
func boolRef(v bool) *bool {
	return &v
}

// assertOK fails the test if the given error is not nil.
func assertOK(err error, t *testing.T) {
	if err != nil {
		t.Fatalf("Assertion failed: %s", describe(err))
	}
}

// describe returns a string description of the given error.
func describe(err error) string {
	if err == nil {
		return "nil"
	}
	cause := driver.Cause(err)
	c, _ := json.Marshal(cause)
	if cause.Error() != err.Error() {
		return fmt.Sprintf("%v caused by %v", err, string(c))
	} else {
		return fmt.Sprintf("%v", string(c))
	}
}

func formatRawResponse(raw []byte) string {
	l := len(raw)
	if l < 2 {
		return hex.EncodeToString(raw)
	}
	if (raw[0] == '{' && raw[l-1] == '}') || (raw[0] == '[' && raw[l-1] == ']') {
		return string(raw)
	}
	return hex.EncodeToString(raw)
}
<commit_msg>Expand ResponseError more intelligent<commit_after>//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// 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.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//

package test

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"testing"

	driver "github.com/arangodb/go-driver"
)

type testEnv interface {
	Error(message ...interface{})
	Errorf(format string, args ...interface{})
	Fatal(message ...interface{})
	Fatalf(format string, args ...interface{})
	Log(message ...interface{})
	Logf(format string, args ...interface{})
}

// boolRef returns a reference to a given boolean
func boolRef(v bool) *bool {
	return &v
}

// assertOK fails the test if the given error is not nil.
func assertOK(err error, t *testing.T) {
	if err != nil {
		t.Fatalf("Assertion failed: %s", describe(err))
	}
}

// describe returns a string description of the given error.
func describe(err error) string {
	if err == nil {
		return "nil"
	}
	cause := driver.Cause(err)
	var msg string
	if re, ok := cause.(*driver.ResponseError); ok {
		msg = re.Error()
	} else {
		c, _ := json.Marshal(cause)
		msg = string(c)
	}
	if cause.Error() != err.Error() {
		return fmt.Sprintf("%v caused by %v", err, msg)
	} else {
		return fmt.Sprintf("%v", msg)
	}
}

func formatRawResponse(raw []byte) string {
	l := len(raw)
	if l < 2 {
		return hex.EncodeToString(raw)
	}
	if (raw[0] == '{' && raw[l-1] == '}') || (raw[0] == '[' && raw[l-1] == ']') {
		return string(raw)
	}
	return hex.EncodeToString(raw)
}
<|endoftext|>


<commit_before>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// 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 storage

import (
	"sync"

	"github.com/corestoreio/csfw/config/cfgpath"
	"github.com/corestoreio/csfw/util/errors"
)

// Storager is the underlying data storage for holding the keys and its values.
// Implementations can be spf13/viper or MySQL backed. Default Storager
// is a simple mutex protected map[string]interface{}.
// ProTip: If you use MySQL as Storager don't execute function
// ApplyCoreConfigData()
// The config.Writer calls the config.Storager functions and config.Storager
// must make sure of the correct type conversions to the supported type of the
// underlying storage engine.
type Storager interface {
	// Set sets a key with a value and returns on success nil or ErrKeyOverwritten,
	// on failure any other error
	Set(key cfgpath.Path, value interface{}) error
	// Get may return a NotFound error behaviour.
	Get(key cfgpath.Path) (interface{}, error)
	// AllKeys returns the fully qualified keys
	AllKeys() (cfgpath.PathSlice, error)
}

// NotFound error type which defines that a specific key cannot be found.
type NotFound struct{}

func (NotFound) Error() string  { return "[storage] Key not found" }
func (NotFound) NotFound() bool { return true } // @see errors.IsNotFound

type keyVal struct {
	k cfgpath.Path
	v interface{}
}

type kvmap struct {
	sync.Mutex
	kv map[uint32]keyVal // todo: create benchmark to check if worth switching to pointers
}

// NewKV creates a new simple key value storage using a map[string]interface{}
// without any persistence or sync to MySQL core_confing_data table
func NewKV() *kvmap {
	return &kvmap{
		kv: make(map[uint32]keyVal),
	}
}

// Set implements Storager interface
func (sp *kvmap) Set(key cfgpath.Path, value interface{}) error {
	sp.Lock()
	defer sp.Unlock()

	h32, err := key.Hash(-1)
	if err != nil {
		return errors.Wrap(err, "[storage] key.Hash")
	}
	sp.kv[h32] = keyVal{key, value}
	return nil
}

// Get implements Storager interface.
// Error behaviour: NotFound.
func (sp *kvmap) Get(key cfgpath.Path) (interface{}, error) {
	sp.Lock()
	defer sp.Unlock()

	h32, err := key.Hash(-1)
	if err != nil {
		return nil, errors.Wrap(err, "[storage] key.Hash")
	}
	if data, ok := sp.kv[h32]; ok {
		return data.v, nil
	}
	return nil, NotFound{}
}

// AllKeys implements Storager interface
func (sp *kvmap) AllKeys() (cfgpath.PathSlice, error) {
	sp.Lock()
	defer sp.Unlock()

	var ret = make(cfgpath.PathSlice, len(sp.kv), len(sp.kv))
	i := 0
	for _, kv := range sp.kv {
		ret[i] = kv.k
		i++
	}
	return ret, nil
}
<commit_msg>config/storage: Update documentation<commit_after>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// 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 storage

import (
	"sync"

	"github.com/corestoreio/csfw/config/cfgpath"
	"github.com/corestoreio/csfw/util/errors"
)

// Storager is the underlying data storage for holding the keys and its values.
// Implementations can be spf13/viper or MySQL backed. Default Storager is a
// simple mutex protected map[string]interface{}. ProTip: If you use MySQL as
// Storager don't execute function ApplyCoreConfigData() The config.Writer calls
// the config.Storager functions and config.Storager must make sure of the
// correct type conversions to the supported type of the underlying storage
// engine.
type Storager interface {
	// Set sets a key with a value and returns on success nil or ErrKeyOverwritten,
	// on failure any other error
	Set(key cfgpath.Path, value interface{}) error
	// Get returns the raw value on success or may return a NotFound error
	// behaviour if an entry cannot be found or does not exists. Any other error
	// can also occur.
	Get(key cfgpath.Path) (interface{}, error)
	// AllKeys returns the fully qualified keys
	AllKeys() (cfgpath.PathSlice, error)
}

// NotFound error type which defines that a specific key cannot be found.
type NotFound struct{}

func (NotFound) Error() string  { return "[storage] Key not found" }
func (NotFound) NotFound() bool { return true } // @see errors.IsNotFound

type keyVal struct {
	k cfgpath.Path
	v interface{}
}

type kvmap struct {
	sync.Mutex
	kv map[uint32]keyVal // todo: create benchmark to check if worth switching to pointers
}

// NewKV creates a new simple key value storage using a map[string]interface{}
// without any persistence or sync to MySQL core_confing_data table
func NewKV() *kvmap {
	return &kvmap{
		kv: make(map[uint32]keyVal),
	}
}

// Set implements Storager interface
func (sp *kvmap) Set(key cfgpath.Path, value interface{}) error {
	sp.Lock()
	defer sp.Unlock()

	h32, err := key.Hash(-1)
	if err != nil {
		return errors.Wrap(err, "[storage] key.Hash")
	}
	sp.kv[h32] = keyVal{key, value}
	return nil
}

// Get implements Storager interface.
// Error behaviour: NotFound.
func (sp *kvmap) Get(key cfgpath.Path) (interface{}, error) {
	sp.Lock()
	defer sp.Unlock()

	h32, err := key.Hash(-1)
	if err != nil {
		return nil, errors.Wrap(err, "[storage] key.Hash")
	}
	if data, ok := sp.kv[h32]; ok {
		return data.v, nil
	}
	return nil, NotFound{}
}

// AllKeys implements Storager interface
func (sp *kvmap) AllKeys() (cfgpath.PathSlice, error) {
	sp.Lock()
	defer sp.Unlock()

	var ret = make(cfgpath.PathSlice, len(sp.kv), len(sp.kv))
	i := 0
	for _, kv := range sp.kv {
		ret[i] = kv.k
		i++
	}
	return ret, nil
}
<|endoftext|>


<commit_before>package subscriber

import (
	"context"
	"sort"
	"sync"
	"time"

	"github.com/mailgun/kafka-pixy/actor"
	"github.com/mailgun/kafka-pixy/config"
	"github.com/mailgun/kafka-pixy/none"
	"github.com/mailgun/kazoo-go"
	"github.com/pkg/errors"
	"github.com/samuel/go-zookeeper/zk"
)

// It is ok for an attempt to claim a partition to fail, for it might take
// some time for the current partition owner to release it. So we won't report
// first several failures to claim a partition as an error.
const safeClaimRetriesCount = 10

// T is a subscriber implementation based on ZooKeeper. It maintains consumer
// group membership and topic subscriptions, watches for other members to join,
// leave and update their subscriptions, and generates notifications of such
// changes. It also provides an API to for a partition consumer to claim and
// release a group-topic-partition.
//
// FIXME: It is assumed that all members of the group are registered with the
// FIXME: `static` pattern. If a member that pattern is either `white_list` or
// FIXME: `black_list` joins the group the result will be unpredictable.
type T struct {
	actDesc          *actor.Descriptor
	cfg              *config.Proxy
	group            string
	groupZNode       *kazoo.Consumergroup
	groupMemberZNode *kazoo.ConsumergroupInstance
	registered       bool
	topicsCh         chan []string
	subscriptionsCh  chan map[string][]string
	stopCh           chan none.T
	claimErrorsCh    chan none.T
	wg               sync.WaitGroup
}

// Spawn creates a subscriber instance and starts its goroutine.
func Spawn(parentActDesc *actor.Descriptor, group string, cfg *config.Proxy, kazooClt *kazoo.Kazoo) *T {
	groupZNode := kazooClt.Consumergroup(group)
	groupMemberZNode := groupZNode.Instance(cfg.ClientID)
	actDesc := parentActDesc.NewChild("member")
	actDesc.AddLogField("kafka.group", group)
	ss := &T{
		actDesc:          actDesc,
		cfg:              cfg,
		group:            group,
		groupZNode:       groupZNode,
		groupMemberZNode: groupMemberZNode,
		topicsCh:         make(chan []string),
		subscriptionsCh:  make(chan map[string][]string),
		stopCh:           make(chan none.T),
		claimErrorsCh:    make(chan none.T, 1),
	}
	actor.Spawn(ss.actDesc, &ss.wg, ss.run)
	return ss
}

// Topics returns a channel to receive a list of topics the member should
// subscribe to. To make the member unsubscribe from all topics either nil or
// an empty topic list can be sent.
func (ss *T) Topics() chan<- []string {
	return ss.topicsCh
}

// Subscriptions returns a channel that subscriptions will be sent whenever a
// member joins or leaves the group or when an existing member updates its
// subscription.
func (ss *T) Subscriptions() <-chan map[string][]string {
	return ss.subscriptionsCh
}

// ClaimPartition claims a topic/partition to be consumed by this member of the
// consumer group. It blocks until either succeeds or canceled by the caller. It
// returns a function that should be called to release the claim.
func (ss *T) ClaimPartition(claimerActDesc *actor.Descriptor, topic string, partition int32, cancelCh <-chan none.T) func() {
	beginAt := time.Now()
	retries := 0
	err := ss.groupMemberZNode.ClaimPartition(topic, partition)
	for err != nil {
		logEntry := claimerActDesc.Log().WithError(err)
		logFailureFn := logEntry.Infof
		if retries++; retries > safeClaimRetriesCount {
			logFailureFn = logEntry.Errorf
		}
		logFailureFn("Failed to claim partition: via=%s, retries=%d, took=%s",
			ss.actDesc, retries, millisSince(beginAt))

		// Let the subscriber actor know that a claim attempt failed.
		select {
		case ss.claimErrorsCh <- none.V:
		default:
		}
		// Wait until either the retry timeout expires or the claim is canceled.
		select {
		case <-time.After(ss.cfg.Consumer.RetryBackoff):
		case <-cancelCh:
			return func() {}
		}
		err = ss.groupMemberZNode.ClaimPartition(topic, partition)
	}
	claimerActDesc.Log().Infof("Partition claimed: via=%s, retries=%d, took=%s",
		ss.actDesc, retries, millisSince(beginAt))
	return func() {
		beginAt := time.Now()
		retries := 0
		err := ss.groupMemberZNode.ReleasePartition(topic, partition)
		for err != nil && err != kazoo.ErrPartitionNotClaimed {
			logEntry := claimerActDesc.Log().WithError(err)
			logFailureFn := logEntry.Infof
			if retries++; retries > safeClaimRetriesCount {
				logFailureFn = logEntry.Errorf
			}
			logFailureFn("Failed to release partition: via=%s, retries=%d, took=%s",
				ss.actDesc, retries, millisSince(beginAt))
			<-time.After(ss.cfg.Consumer.RetryBackoff)
			err = ss.groupMemberZNode.ReleasePartition(topic, partition)
		}
		claimerActDesc.Log().Infof("Partition released: via=%s, retries=%d, took=%s",
			ss.actDesc, retries, millisSince(beginAt))
	}
}

// Stop signals the consumer group member to stop and blocks until its
// goroutines are over.
func (ss *T) Stop() {
	close(ss.stopCh)
	ss.wg.Wait()
}

func (ss *T) run() {
	defer close(ss.subscriptionsCh)

	// Ensure a group ZNode exists.
	err := ss.groupZNode.Create()
	for err != nil {
		ss.actDesc.Log().WithError(err).Error("Failed to create a group znode")
		select {
		case <-time.After(ss.cfg.Consumer.RetryBackoff):
		case <-ss.stopCh:
			return
		}
		err = ss.groupZNode.Create()
	}

	// Ensure that the member leaves the group in ZooKeeper on stop. We retry
	// indefinitely here until ZooKeeper confirms that there is no registration.
	defer func() {
		err := ss.groupMemberZNode.Deregister()
		for err != nil && err != kazoo.ErrInstanceNotRegistered {
			ss.actDesc.Log().WithError(err).Error("Failed to deregister")
			<-time.After(ss.cfg.Consumer.RetryBackoff)
			err = ss.groupMemberZNode.Deregister()
		}
	}()

	var (
		nilOrSubscriptionsCh     chan<- map[string][]string
		nilOrWatchCh             <-chan none.T
		nilOrTimeoutCh           <-chan time.Time
		cancelWatch              context.CancelFunc
		shouldSubmitTopics       = false
		shouldFetchSubscriptions = false
		topics                   []string
		subscriptions            map[string][]string
		submittedAt              time.Time
	)
	for {
		select {
		case topics = <-ss.topicsCh:
			sort.Strings(topics)
			shouldSubmitTopics = true

		case nilOrSubscriptionsCh <- subscriptions:
			nilOrSubscriptionsCh = nil

		case <-nilOrWatchCh:
			nilOrWatchCh = nil
			cancelWatch()
			shouldFetchSubscriptions = true

		case <-ss.claimErrorsCh:
			sinceLastSubmit := time.Now().Sub(submittedAt)
			if sinceLastSubmit > ss.cfg.Consumer.RetryBackoff {
				ss.actDesc.Log().Infof("Resubmit triggered by claim failure: since=%v", sinceLastSubmit)
				shouldSubmitTopics = true
			}
		case <-nilOrTimeoutCh:
			nilOrTimeoutCh = nil

		case <-ss.stopCh:
			if cancelWatch != nil {
				cancelWatch()
			}
			return
		}

		if shouldSubmitTopics {
			submittedAt = time.Now()
			if err = ss.submitTopics(topics); err != nil {
				ss.actDesc.Log().WithError(err).Error("Failed to submit topics")
				nilOrTimeoutCh = time.After(ss.cfg.Consumer.RetryBackoff)
				continue
			}
			ss.actDesc.Log().Infof("Submitted: topics=%v", topics)
			shouldSubmitTopics = false
			if cancelWatch != nil {
				cancelWatch()
			}
			shouldFetchSubscriptions = true
		}

		if shouldFetchSubscriptions {
			subscriptions, nilOrWatchCh, cancelWatch, err = ss.fetchSubscriptions()
			if err != nil {
				ss.actDesc.Log().WithError(err).Error("Failed to fetch subscriptions")
				nilOrTimeoutCh = time.After(ss.cfg.Consumer.RetryBackoff)
				continue
			}
			shouldFetchSubscriptions = false
			ss.actDesc.Log().Infof("Fetched subscriptions: %v", subscriptions)
			nilOrSubscriptionsCh = ss.subscriptionsCh
		}
	}
}

// fetchSubscriptions retrieves subscription topics for all group members and
// returns a channel that will be closed
func (ss *T) fetchSubscriptions() (map[string][]string, <-chan none.T, context.CancelFunc, error) {
	members, groupUpdateWatchCh, err := ss.groupZNode.WatchInstances()
	if err != nil {
		return nil, nil, nil, errors.Wrapf(err, "failed to watch members")
	}

	memberUpdateWatchChs := make(map[string]<-chan zk.Event, len(members))
	subscriptions := make(map[string][]string, len(members))
	for _, member := range members {
		var registration *kazoo.Registration
		registration, memberUpdateWatchCh, err := member.WatchRegistration()
		for err != nil {
			return nil, nil, nil, errors.Wrapf(err, "failed to watch registration, member=%s", member.ID)
		}
		memberUpdateWatchChs[member.ID] = memberUpdateWatchCh

		topics := make([]string, 0, len(registration.Subscription))
		for topic := range registration.Subscription {
			topics = append(topics, topic)
		}
		// Sort topics to ensure deterministic output.
		sort.Strings(topics)
		subscriptions[member.ID] = topics
	}
	aggregateWatchCh := make(chan none.T)
	ctx, cancel := context.WithCancel(context.Background())

	go ss.forwardWatch(ctx, "members", groupUpdateWatchCh, aggregateWatchCh)
	for memberID, memberUpdateWatchCh := range memberUpdateWatchChs {
		go ss.forwardWatch(ctx, memberID, memberUpdateWatchCh, aggregateWatchCh)
	}
	return subscriptions, aggregateWatchCh, cancel, nil
}

func (ss *T) submitTopics(topics []string) error {
	if len(topics) == 0 {
		err := ss.groupMemberZNode.Deregister()
		if err != nil && err != kazoo.ErrInstanceNotRegistered {
			return errors.Wrap(err, "failed to deregister")
		}
		ss.registered = false
		return nil
	}
	var err error
	if ss.registered {
		err = ss.groupMemberZNode.UpdateRegistration(topics)
	} else {
		err = ss.groupMemberZNode.Register(topics)
	}
	for err != nil {
		return errors.Wrap(err, "failed to register")
	}
	ss.registered = true
	return nil
}

func (ss *T) forwardWatch(ctx context.Context, alias string, fromCh <-chan zk.Event, toCh chan<- none.T) {
	select {
	case <-fromCh:
		ss.actDesc.Log().Infof("Watch triggered: alias=%s", alias)
		select {
		case toCh <- none.V:
		case <-ctx.Done():
		}
	case <-ctx.Done():
	}
}

func millisSince(t time.Time) time.Duration {
	return time.Now().Sub(t) / time.Millisecond * time.Millisecond
}
<commit_msg>Time topic submit at request end<commit_after>package subscriber

import (
	"context"
	"sort"
	"sync"
	"time"

	"github.com/mailgun/kafka-pixy/actor"
	"github.com/mailgun/kafka-pixy/config"
	"github.com/mailgun/kafka-pixy/none"
	"github.com/mailgun/kazoo-go"
	"github.com/pkg/errors"
	"github.com/samuel/go-zookeeper/zk"
)

// It is ok for an attempt to claim a partition to fail, for it might take
// some time for the current partition owner to release it. So we won't report
// first several failures to claim a partition as an error.
const safeClaimRetriesCount = 10

// T is a subscriber implementation based on ZooKeeper. It maintains consumer
// group membership and topic subscriptions, watches for other members to join,
// leave and update their subscriptions, and generates notifications of such
// changes. It also provides an API to for a partition consumer to claim and
// release a group-topic-partition.
//
// FIXME: It is assumed that all members of the group are registered with the
// FIXME: `static` pattern. If a member that pattern is either `white_list` or
// FIXME: `black_list` joins the group the result will be unpredictable.
type T struct {
	actDesc          *actor.Descriptor
	cfg              *config.Proxy
	group            string
	groupZNode       *kazoo.Consumergroup
	groupMemberZNode *kazoo.ConsumergroupInstance
	registered       bool
	topicsCh         chan []string
	subscriptionsCh  chan map[string][]string
	stopCh           chan none.T
	claimErrorsCh    chan none.T
	wg               sync.WaitGroup
}

// Spawn creates a subscriber instance and starts its goroutine.
func Spawn(parentActDesc *actor.Descriptor, group string, cfg *config.Proxy, kazooClt *kazoo.Kazoo) *T {
	groupZNode := kazooClt.Consumergroup(group)
	groupMemberZNode := groupZNode.Instance(cfg.ClientID)
	actDesc := parentActDesc.NewChild("member")
	actDesc.AddLogField("kafka.group", group)
	ss := &T{
		actDesc:          actDesc,
		cfg:              cfg,
		group:            group,
		groupZNode:       groupZNode,
		groupMemberZNode: groupMemberZNode,
		topicsCh:         make(chan []string),
		subscriptionsCh:  make(chan map[string][]string),
		stopCh:           make(chan none.T),
		claimErrorsCh:    make(chan none.T, 1),
	}
	actor.Spawn(ss.actDesc, &ss.wg, ss.run)
	return ss
}

// Topics returns a channel to receive a list of topics the member should
// subscribe to. To make the member unsubscribe from all topics either nil or
// an empty topic list can be sent.
func (ss *T) Topics() chan<- []string {
	return ss.topicsCh
}

// Subscriptions returns a channel that subscriptions will be sent whenever a
// member joins or leaves the group or when an existing member updates its
// subscription.
func (ss *T) Subscriptions() <-chan map[string][]string {
	return ss.subscriptionsCh
}

// ClaimPartition claims a topic/partition to be consumed by this member of the
// consumer group. It blocks until either succeeds or canceled by the caller. It
// returns a function that should be called to release the claim.
func (ss *T) ClaimPartition(claimerActDesc *actor.Descriptor, topic string, partition int32, cancelCh <-chan none.T) func() {
	beginAt := time.Now()
	retries := 0
	err := ss.groupMemberZNode.ClaimPartition(topic, partition)
	for err != nil {
		logEntry := claimerActDesc.Log().WithError(err)
		logFailureFn := logEntry.Infof
		if retries++; retries > safeClaimRetriesCount {
			logFailureFn = logEntry.Errorf
		}
		logFailureFn("Failed to claim partition: via=%s, retries=%d, took=%s",
			ss.actDesc, retries, millisSince(beginAt))

		// Let the subscriber actor know that a claim attempt failed.
		select {
		case ss.claimErrorsCh <- none.V:
		default:
		}
		// Wait until either the retry timeout expires or the claim is canceled.
		select {
		case <-time.After(ss.cfg.Consumer.RetryBackoff):
		case <-cancelCh:
			return func() {}
		}
		err = ss.groupMemberZNode.ClaimPartition(topic, partition)
	}
	claimerActDesc.Log().Infof("Partition claimed: via=%s, retries=%d, took=%s",
		ss.actDesc, retries, millisSince(beginAt))
	return func() {
		beginAt := time.Now()
		retries := 0
		err := ss.groupMemberZNode.ReleasePartition(topic, partition)
		for err != nil && err != kazoo.ErrPartitionNotClaimed {
			logEntry := claimerActDesc.Log().WithError(err)
			logFailureFn := logEntry.Infof
			if retries++; retries > safeClaimRetriesCount {
				logFailureFn = logEntry.Errorf
			}
			logFailureFn("Failed to release partition: via=%s, retries=%d, took=%s",
				ss.actDesc, retries, millisSince(beginAt))
			<-time.After(ss.cfg.Consumer.RetryBackoff)
			err = ss.groupMemberZNode.ReleasePartition(topic, partition)
		}
		claimerActDesc.Log().Infof("Partition released: via=%s, retries=%d, took=%s",
			ss.actDesc, retries, millisSince(beginAt))
	}
}

// Stop signals the consumer group member to stop and blocks until its
// goroutines are over.
func (ss *T) Stop() {
	close(ss.stopCh)
	ss.wg.Wait()
}

func (ss *T) run() {
	defer close(ss.subscriptionsCh)

	// Ensure a group ZNode exists.
	err := ss.groupZNode.Create()
	for err != nil {
		ss.actDesc.Log().WithError(err).Error("Failed to create a group znode")
		select {
		case <-time.After(ss.cfg.Consumer.RetryBackoff):
		case <-ss.stopCh:
			return
		}
		err = ss.groupZNode.Create()
	}

	// Ensure that the member leaves the group in ZooKeeper on stop. We retry
	// indefinitely here until ZooKeeper confirms that there is no registration.
	defer func() {
		err := ss.groupMemberZNode.Deregister()
		for err != nil && err != kazoo.ErrInstanceNotRegistered {
			ss.actDesc.Log().WithError(err).Error("Failed to deregister")
			<-time.After(ss.cfg.Consumer.RetryBackoff)
			err = ss.groupMemberZNode.Deregister()
		}
	}()

	var (
		nilOrSubscriptionsCh     chan<- map[string][]string
		nilOrWatchCh             <-chan none.T
		nilOrTimeoutCh           <-chan time.Time
		cancelWatch              context.CancelFunc
		shouldSubmitTopics       = false
		shouldFetchSubscriptions = false
		topics                   []string
		subscriptions            map[string][]string
		submittedAt              = time.Now()
	)
	for {
		select {
		case topics = <-ss.topicsCh:
			sort.Strings(topics)
			shouldSubmitTopics = true

		case nilOrSubscriptionsCh <- subscriptions:
			nilOrSubscriptionsCh = nil

		case <-nilOrWatchCh:
			nilOrWatchCh = nil
			cancelWatch()
			shouldFetchSubscriptions = true

		case <-ss.claimErrorsCh:
			sinceLastSubmit := time.Now().Sub(submittedAt)
			if sinceLastSubmit > ss.cfg.Consumer.RetryBackoff {
				ss.actDesc.Log().Infof("Resubmit triggered by claim failure: since=%v", sinceLastSubmit)
				shouldSubmitTopics = true
			}
		case <-nilOrTimeoutCh:
			nilOrTimeoutCh = nil

		case <-ss.stopCh:
			if cancelWatch != nil {
				cancelWatch()
			}
			return
		}

		if shouldSubmitTopics {
			if err = ss.submitTopics(topics); err != nil {
				ss.actDesc.Log().WithError(err).Error("Failed to submit topics")
				nilOrTimeoutCh = time.After(ss.cfg.Consumer.RetryBackoff)
				continue
			}
			submittedAt = time.Now()
			ss.actDesc.Log().Infof("Submitted: topics=%v", topics)
			shouldSubmitTopics = false
			if cancelWatch != nil {
				cancelWatch()
			}
			shouldFetchSubscriptions = true
		}

		if shouldFetchSubscriptions {
			subscriptions, nilOrWatchCh, cancelWatch, err = ss.fetchSubscriptions()
			if err != nil {
				ss.actDesc.Log().WithError(err).Error("Failed to fetch subscriptions")
				nilOrTimeoutCh = time.After(ss.cfg.Consumer.RetryBackoff)
				continue
			}
			shouldFetchSubscriptions = false
			ss.actDesc.Log().Infof("Fetched subscriptions: %v", subscriptions)
			nilOrSubscriptionsCh = ss.subscriptionsCh
		}
	}
}

// fetchSubscriptions retrieves subscription topics for all group members and
// returns a channel that will be closed
func (ss *T) fetchSubscriptions() (map[string][]string, <-chan none.T, context.CancelFunc, error) {
	members, groupUpdateWatchCh, err := ss.groupZNode.WatchInstances()
	if err != nil {
		return nil, nil, nil, errors.Wrapf(err, "failed to watch members")
	}

	memberUpdateWatchChs := make(map[string]<-chan zk.Event, len(members))
	subscriptions := make(map[string][]string, len(members))
	for _, member := range members {
		var registration *kazoo.Registration
		registration, memberUpdateWatchCh, err := member.WatchRegistration()
		for err != nil {
			return nil, nil, nil, errors.Wrapf(err, "failed to watch registration, member=%s", member.ID)
		}
		memberUpdateWatchChs[member.ID] = memberUpdateWatchCh

		topics := make([]string, 0, len(registration.Subscription))
		for topic := range registration.Subscription {
			topics = append(topics, topic)
		}
		// Sort topics to ensure deterministic output.
		sort.Strings(topics)
		subscriptions[member.ID] = topics
	}
	aggregateWatchCh := make(chan none.T)
	ctx, cancel := context.WithCancel(context.Background())

	go ss.forwardWatch(ctx, "members", groupUpdateWatchCh, aggregateWatchCh)
	for memberID, memberUpdateWatchCh := range memberUpdateWatchChs {
		go ss.forwardWatch(ctx, memberID, memberUpdateWatchCh, aggregateWatchCh)
	}
	return subscriptions, aggregateWatchCh, cancel, nil
}

func (ss *T) submitTopics(topics []string) error {
	if len(topics) == 0 {
		err := ss.groupMemberZNode.Deregister()
		if err != nil && err != kazoo.ErrInstanceNotRegistered {
			return errors.Wrap(err, "failed to deregister")
		}
		ss.registered = false
		return nil
	}
	var err error
	if ss.registered {
		err = ss.groupMemberZNode.UpdateRegistration(topics)
	} else {
		err = ss.groupMemberZNode.Register(topics)
	}
	for err != nil {
		return errors.Wrap(err, "failed to register")
	}
	ss.registered = true
	return nil
}

func (ss *T) forwardWatch(ctx context.Context, alias string, fromCh <-chan zk.Event, toCh chan<- none.T) {
	select {
	case <-fromCh:
		ss.actDesc.Log().Infof("Watch triggered: alias=%s", alias)
		select {
		case toCh <- none.V:
		case <-ctx.Done():
		}
	case <-ctx.Done():
	}
}

func millisSince(t time.Time) time.Duration {
	return time.Now().Sub(t) / time.Millisecond * time.Millisecond
}
<|endoftext|>