<commit_before>/*
 * Minimalist Object Storage, (C) 2015 Minio, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     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 donut

import (
	"errors"
	"io"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/minio-io/iodine"
	"github.com/minio-io/minio/pkg/drivers"
	"github.com/minio-io/minio/pkg/storage/donut"
	"log"
)

// donutDriver - creates a new single disk drivers driver using donut
type donutDriver struct {
	donut donut.Donut
}

const (
	blockSize = 10 * 1024 * 1024
)

// Start a single disk subsystem
func Start(path string) (chan<- string, <-chan error, drivers.Driver) {
	ctrlChannel := make(chan string)
	errorChannel := make(chan error)
	s := new(donutDriver)

	// TODO donut driver should be passed in as Start param and driven by config
	var err *iodine.Error
	s.donut, err = donut.NewDonut(path)
	if err != nil {
		err = err.Annotate(map[string]string{"path": path})
		if err != nil {
			log.Println(err.EmitHumanReadable())
		}
	}

	go start(ctrlChannel, errorChannel, s)
	return ctrlChannel, errorChannel, s
}

func start(ctrlChannel <-chan string, errorChannel chan<- error, s *donutDriver) {
	close(errorChannel)
}

// ListBuckets returns a list of buckets
func (d donutDriver) ListBuckets() (results []drivers.BucketMetadata, err error) {
	buckets, err := d.donut.ListBuckets()
	if err != nil {
		return nil, err
	}
	for _, bucket := range buckets {
		if err != nil {
			return nil, err
		}
		result := drivers.BucketMetadata{
			Name: bucket,
			// TODO Add real created date
			Created: time.Now(),
		}
		results = append(results, result)
	}
	return results, nil
}

// CreateBucket creates a new bucket
func (d donutDriver) CreateBucket(bucket string) error {
	return d.donut.CreateBucket(bucket)
}

// GetBucketMetadata retrieves an bucket's metadata
func (d donutDriver) GetBucketMetadata(bucket string) (drivers.BucketMetadata, error) {
	return drivers.BucketMetadata{}, errors.New("Not Implemented")
}

// CreateBucketPolicy sets a bucket's access policy
func (d donutDriver) CreateBucketPolicy(bucket string, p drivers.BucketPolicy) error {
	return errors.New("Not Implemented")
}

// GetBucketPolicy returns a bucket's access policy
func (d donutDriver) GetBucketPolicy(bucket string) (drivers.BucketPolicy, error) {
	return drivers.BucketPolicy{}, errors.New("Not Implemented")
}

// GetObject retrieves an object and writes it to a writer
func (d donutDriver) GetObject(target io.Writer, bucket, key string) (int64, error) {
	reader, err := d.donut.GetObjectReader(bucket, key)
	if err != nil {
		return 0, drivers.ObjectNotFound{
			Bucket: bucket,
			Object: key,
		}
	}
	return io.Copy(target, reader)
}

// GetPartialObject retrieves an object range and writes it to a writer
func (d donutDriver) GetPartialObject(w io.Writer, bucket, object string, start, length int64) (int64, error) {
	return 0, errors.New("Not Implemented")
}

// GetObjectMetadata retrieves an object's metadata
func (d donutDriver) GetObjectMetadata(bucket, key string, prefix string) (drivers.ObjectMetadata, error) {
	metadata, err := d.donut.GetObjectMetadata(bucket, key)
	created, err := time.Parse(time.RFC3339Nano, metadata["sys.created"])
	if err != nil {
		return drivers.ObjectMetadata{}, err
	}
	size, err := strconv.ParseInt(metadata["sys.size"], 10, 64)
	if err != nil {
		return drivers.ObjectMetadata{}, err
	}
	objectMetadata := drivers.ObjectMetadata{
		Bucket: bucket,
		Key:    key,

		ContentType: metadata["contentType"],
		Created:     created,
		Md5:         metadata["sys.md5"],
		Size:        size,
	}
	return objectMetadata, nil
}

// ListObjects - returns list of objects
func (d donutDriver) ListObjects(bucket string, resources drivers.BucketResourcesMetadata) ([]drivers.ObjectMetadata, drivers.BucketResourcesMetadata, error) {
	// TODO Fix IsPrefixSet && IsDelimiterSet and use them
	objects, err := d.donut.ListObjects(bucket)
	if err != nil {
		return nil, drivers.BucketResourcesMetadata{}, err
	}
	sort.Strings(objects)
	if resources.Prefix != "" {
		objects = filterPrefix(objects, resources.Prefix)
		objects = removePrefix(objects, resources.Prefix)
	}
	if resources.Maxkeys <= 0 || resources.Maxkeys > 1000 {
		resources.Maxkeys = 1000
	}

	var actualObjects []string
	var commonPrefixes []string
	if strings.TrimSpace(resources.Delimiter) != "" {
		actualObjects = filterDelimited(objects, resources.Delimiter)
		commonPrefixes = filterNotDelimited(objects, resources.Delimiter)
		commonPrefixes = extractDir(commonPrefixes, resources.Delimiter)
		commonPrefixes = uniqueObjects(commonPrefixes)
		resources.CommonPrefixes = commonPrefixes
	} else {
		actualObjects = objects
	}

	var results []drivers.ObjectMetadata
	for _, object := range actualObjects {
		if len(results) >= resources.Maxkeys {
			resources.IsTruncated = true
			break
		}
		metadata, err := d.GetObjectMetadata(bucket, resources.Prefix+object, "")
		if err != nil {
			return nil, drivers.BucketResourcesMetadata{}, err
		}
		results = append(results, metadata)
	}
	return results, resources, nil
}

func filterPrefix(objects []string, prefix string) []string {
	var results []string
	for _, object := range objects {
		if strings.HasPrefix(object, prefix) {
			results = append(results, object)
		}
	}
	return results
}

func removePrefix(objects []string, prefix string) []string {
	var results []string
	for _, object := range objects {
		results = append(results, strings.TrimPrefix(object, prefix))
	}
	return results
}

func filterDelimited(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		if !strings.Contains(object, delim) {
			results = append(results, object)
		}
	}
	return results
}
func filterNotDelimited(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		if strings.Contains(object, delim) {
			results = append(results, object)
		}
	}
	return results
}

func extractDir(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		parts := strings.Split(object, delim)
		results = append(results, parts[0]+"/")
	}
	return results
}

func uniqueObjects(objects []string) []string {
	objectMap := make(map[string]string)
	for _, v := range objects {
		objectMap[v] = v
	}
	var results []string
	for k := range objectMap {
		results = append(results, k)
	}
	sort.Strings(results)
	return results
}

// CreateObject creates a new object
func (d donutDriver) CreateObject(bucketKey, objectKey, contentType, expectedMd5sum string, reader io.Reader) error {
	writer, err := d.donut.GetObjectWriter(bucketKey, objectKey)
	if err != nil {
		return err
	}
	if _, err := io.Copy(writer, reader); err != nil {
		return err
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	contentType = strings.TrimSpace(contentType)
	metadata := make(map[string]string)
	metadata["bucket"] = bucketKey
	metadata["object"] = objectKey
	metadata["contentType"] = contentType
	if err = writer.SetMetadata(metadata); err != nil {
		return err
	}
	return writer.Close()
}
<commit_msg>Remove unnecessary if <block><commit_after>/*
 * Minimalist Object Storage, (C) 2015 Minio, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     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 donut

import (
	"errors"
	"io"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/minio-io/iodine"
	"github.com/minio-io/minio/pkg/drivers"
	"github.com/minio-io/minio/pkg/storage/donut"
	"log"
)

// donutDriver - creates a new single disk drivers driver using donut
type donutDriver struct {
	donut donut.Donut
}

const (
	blockSize = 10 * 1024 * 1024
)

// Start a single disk subsystem
func Start(path string) (chan<- string, <-chan error, drivers.Driver) {
	ctrlChannel := make(chan string)
	errorChannel := make(chan error)
	s := new(donutDriver)

	// TODO donut driver should be passed in as Start param and driven by config
	var err *iodine.Error
	s.donut, err = donut.NewDonut(path)
	if err != nil {
		err = err.Annotate(map[string]string{"path": path})
		if err != nil {
			log.Println(err.EmitHumanReadable())
		}
	}

	go start(ctrlChannel, errorChannel, s)
	return ctrlChannel, errorChannel, s
}

func start(ctrlChannel <-chan string, errorChannel chan<- error, s *donutDriver) {
	close(errorChannel)
}

// ListBuckets returns a list of buckets
func (d donutDriver) ListBuckets() (results []drivers.BucketMetadata, err error) {
	buckets, err := d.donut.ListBuckets()
	if err != nil {
		return nil, err
	}
	for _, bucket := range buckets {
		result := drivers.BucketMetadata{
			Name: bucket,
			// TODO Add real created date
			Created: time.Now(),
		}
		results = append(results, result)
	}
	return results, nil
}

// CreateBucket creates a new bucket
func (d donutDriver) CreateBucket(bucket string) error {
	return d.donut.CreateBucket(bucket)
}

// GetBucketMetadata retrieves an bucket's metadata
func (d donutDriver) GetBucketMetadata(bucket string) (drivers.BucketMetadata, error) {
	return drivers.BucketMetadata{}, errors.New("Not Implemented")
}

// CreateBucketPolicy sets a bucket's access policy
func (d donutDriver) CreateBucketPolicy(bucket string, p drivers.BucketPolicy) error {
	return errors.New("Not Implemented")
}

// GetBucketPolicy returns a bucket's access policy
func (d donutDriver) GetBucketPolicy(bucket string) (drivers.BucketPolicy, error) {
	return drivers.BucketPolicy{}, errors.New("Not Implemented")
}

// GetObject retrieves an object and writes it to a writer
func (d donutDriver) GetObject(target io.Writer, bucket, key string) (int64, error) {
	reader, err := d.donut.GetObjectReader(bucket, key)
	if err != nil {
		return 0, drivers.ObjectNotFound{
			Bucket: bucket,
			Object: key,
		}
	}
	return io.Copy(target, reader)
}

// GetPartialObject retrieves an object range and writes it to a writer
func (d donutDriver) GetPartialObject(w io.Writer, bucket, object string, start, length int64) (int64, error) {
	return 0, errors.New("Not Implemented")
}

// GetObjectMetadata retrieves an object's metadata
func (d donutDriver) GetObjectMetadata(bucket, key string, prefix string) (drivers.ObjectMetadata, error) {
	metadata, err := d.donut.GetObjectMetadata(bucket, key)
	created, err := time.Parse(time.RFC3339Nano, metadata["sys.created"])
	if err != nil {
		return drivers.ObjectMetadata{}, err
	}
	size, err := strconv.ParseInt(metadata["sys.size"], 10, 64)
	if err != nil {
		return drivers.ObjectMetadata{}, err
	}
	objectMetadata := drivers.ObjectMetadata{
		Bucket: bucket,
		Key:    key,

		ContentType: metadata["contentType"],
		Created:     created,
		Md5:         metadata["sys.md5"],
		Size:        size,
	}
	return objectMetadata, nil
}

// ListObjects - returns list of objects
func (d donutDriver) ListObjects(bucket string, resources drivers.BucketResourcesMetadata) ([]drivers.ObjectMetadata, drivers.BucketResourcesMetadata, error) {
	// TODO Fix IsPrefixSet && IsDelimiterSet and use them
	objects, err := d.donut.ListObjects(bucket)
	if err != nil {
		return nil, drivers.BucketResourcesMetadata{}, err
	}
	sort.Strings(objects)
	if resources.Prefix != "" {
		objects = filterPrefix(objects, resources.Prefix)
		objects = removePrefix(objects, resources.Prefix)
	}
	if resources.Maxkeys <= 0 || resources.Maxkeys > 1000 {
		resources.Maxkeys = 1000
	}

	var actualObjects []string
	var commonPrefixes []string
	if strings.TrimSpace(resources.Delimiter) != "" {
		actualObjects = filterDelimited(objects, resources.Delimiter)
		commonPrefixes = filterNotDelimited(objects, resources.Delimiter)
		commonPrefixes = extractDir(commonPrefixes, resources.Delimiter)
		commonPrefixes = uniqueObjects(commonPrefixes)
		resources.CommonPrefixes = commonPrefixes
	} else {
		actualObjects = objects
	}

	var results []drivers.ObjectMetadata
	for _, object := range actualObjects {
		if len(results) >= resources.Maxkeys {
			resources.IsTruncated = true
			break
		}
		metadata, err := d.GetObjectMetadata(bucket, resources.Prefix+object, "")
		if err != nil {
			return nil, drivers.BucketResourcesMetadata{}, err
		}
		results = append(results, metadata)
	}
	return results, resources, nil
}

func filterPrefix(objects []string, prefix string) []string {
	var results []string
	for _, object := range objects {
		if strings.HasPrefix(object, prefix) {
			results = append(results, object)
		}
	}
	return results
}

func removePrefix(objects []string, prefix string) []string {
	var results []string
	for _, object := range objects {
		results = append(results, strings.TrimPrefix(object, prefix))
	}
	return results
}

func filterDelimited(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		if !strings.Contains(object, delim) {
			results = append(results, object)
		}
	}
	return results
}
func filterNotDelimited(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		if strings.Contains(object, delim) {
			results = append(results, object)
		}
	}
	return results
}

func extractDir(objects []string, delim string) []string {
	var results []string
	for _, object := range objects {
		parts := strings.Split(object, delim)
		results = append(results, parts[0]+"/")
	}
	return results
}

func uniqueObjects(objects []string) []string {
	objectMap := make(map[string]string)
	for _, v := range objects {
		objectMap[v] = v
	}
	var results []string
	for k := range objectMap {
		results = append(results, k)
	}
	sort.Strings(results)
	return results
}

// CreateObject creates a new object
func (d donutDriver) CreateObject(bucketKey, objectKey, contentType, expectedMd5sum string, reader io.Reader) error {
	writer, err := d.donut.GetObjectWriter(bucketKey, objectKey)
	if err != nil {
		return err
	}
	if _, err := io.Copy(writer, reader); err != nil {
		return err
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	contentType = strings.TrimSpace(contentType)
	metadata := make(map[string]string)
	metadata["bucket"] = bucketKey
	metadata["object"] = objectKey
	metadata["contentType"] = contentType
	if err = writer.SetMetadata(metadata); err != nil {
		return err
	}
	return writer.Close()
}
<|endoftext|>


<commit_before>// Package path provides functions for manipulating filesystem path names.
package path

import (
	"os"
	"path/filepath"

	"github.com/elves/elvish/pkg/eval"
)

// Ns is the namespace for the re: module.
var Ns = eval.NsBuilder{}.AddGoFns("path:", fns).Ns()

var fns = map[string]interface{}{
	"abs":           filepath.Abs,
	"base":          filepath.Base,
	"clean":         filepath.Clean,
	"dir":           filepath.Dir,
	"ext":           filepath.Ext,
	"eval-symlinks": filepath.EvalSymlinks,
	"is-abs":        filepath.IsAbs,
	"is-dir":        isDir,
	"is-regular":    isRegular,
}

//elvdoc:fn abs
//
// ```elvish
// path:abs $path
// ```
//
// Outputs `$path` converted to an absolute path.
//
// ```elvish-transcript
// ~> cd ~
// ~> path:abs bin
// ▶ /home/user/bin
// ```

//elvdoc:fn base
//
// ```elvish
// path:base $path
// ```
//
// Outputs the last element of `$path`. This is analogous to the POSIX `basename` command. See the
// [Go documentation](https://pkg.go.dev/path/filepath#Base) for more details.
//
// ```elvish-transcript
// ~> path:base ~/bin
// ▶ bin
// ```

//elvdoc:fn clean
//
// ```elvish
// path:clean $path
// ```
//
// Outputs the shortest version of `$path` equivalent to `$path` by purely lexical processing. This
// is most useful for eliminating unnecessary relative path elements such as `.` and `..` without
// asking the OS to evaluate the path name. See the [Go
// documentation](https://pkg.go.dev/path/filepath#Clean) for more details.
//
// ```elvish-transcript
// ~> path:clean ./../bin
// ▶ ../bin
// ```

//elvdoc:fn dir
//
// ```elvish
// path:dir $path
// ```
//
// Outputs all but the last element of `$path`, typically the path's enclosing directory. See the
// [Go documentation](https://pkg.go.dev/path/filepath#Dir) for more details. This is analogous to
// the POSIX `dirname` command.
//
// ```elvish-transcript
// ~> path:dir /a/b/c/something
// ▶ /a/b/c
// ```

//elvdoc:fn ext
//
// ```elvish
// ext $path
// ```
//
// Outputs the file name extension used by `$path` (including the separating period). If there is no
// extension the empty string is output. See the [Go
// documentation](https://pkg.go.dev/path/filepath#Ext) for more details.
//
// ```elvish-transcript
// ~> path:ext hello.elv
// ▶ .elv
// ```

//elvdoc:fn is-abs
//
// ```elvish
// is-abs $path
// ```
//
// Outputs `$true` if the path is an abolute path. Note that platforms like Windows have different
// rules than UNIX like platforms for what constitutes an absolute path. See the [Go
// documentation](https://pkg.go.dev/path/filepath#IsAbs) for more details.
//
// ```elvish-transcript
// ~> path:is-abs hello.elv
// ▶ false
// ~> path:is-abs /hello.elv
// ▶ true
// ```

//elvdoc:fn eval-symlinks
//
// ```elvish-transcript
// ~> mkdir bin
// ~> ln -s bin sbin
// ~> path:eval-symlinks ./sbin/a_command
// ▶ bin/a_command
// ```
//
// Outputs `$path` after resolving any symbolic links. If `$path` is relative the result will be
// relative to the current directory, unless one of the components is an absolute symbolic link.
// This function calls `path:clean` on the result before outputing it. This is analogous to the
// external `realpath` or `readlink` command found on many systems. See the [Go
// documentation](https://pkg.go.dev/path/filepath#EvalSymlinks) for more details.

//elvdoc:fn is-dir
//
// ```elvish
// is-dir $path
// ```
//
// Outputs `$true` if the path resolves to a directory.
//
// ```elvish-transcript
// ~> touch not-a-dir
// ~> path:is-dir not-a-dir
// ▶ false
// ~> path:is-dir /tmp
// ▶ true
// ```

func isDir(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.Mode().IsDir()
}

//elvdoc:fn is-regular
//
// ```elvish
// is-regular $path
// ```
//
// Outputs `$true` if the path resolves to a directory.
//
// ```elvish-transcript
// ~> touch not-a-dir
// ~> path:is-regular not-a-dir
// ▶ true
// ~> path:is-dir /tmp
// ▶ false
// ```

func isRegular(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.Mode().IsRegular()
}
<commit_msg>Fix typo in description of path:is-regular<commit_after>// Package path provides functions for manipulating filesystem path names.
package path

import (
	"os"
	"path/filepath"

	"github.com/elves/elvish/pkg/eval"
)

// Ns is the namespace for the re: module.
var Ns = eval.NsBuilder{}.AddGoFns("path:", fns).Ns()

var fns = map[string]interface{}{
	"abs":           filepath.Abs,
	"base":          filepath.Base,
	"clean":         filepath.Clean,
	"dir":           filepath.Dir,
	"ext":           filepath.Ext,
	"eval-symlinks": filepath.EvalSymlinks,
	"is-abs":        filepath.IsAbs,
	"is-dir":        isDir,
	"is-regular":    isRegular,
}

//elvdoc:fn abs
//
// ```elvish
// path:abs $path
// ```
//
// Outputs `$path` converted to an absolute path.
//
// ```elvish-transcript
// ~> cd ~
// ~> path:abs bin
// ▶ /home/user/bin
// ```

//elvdoc:fn base
//
// ```elvish
// path:base $path
// ```
//
// Outputs the last element of `$path`. This is analogous to the POSIX `basename` command. See the
// [Go documentation](https://pkg.go.dev/path/filepath#Base) for more details.
//
// ```elvish-transcript
// ~> path:base ~/bin
// ▶ bin
// ```

//elvdoc:fn clean
//
// ```elvish
// path:clean $path
// ```
//
// Outputs the shortest version of `$path` equivalent to `$path` by purely lexical processing. This
// is most useful for eliminating unnecessary relative path elements such as `.` and `..` without
// asking the OS to evaluate the path name. See the [Go
// documentation](https://pkg.go.dev/path/filepath#Clean) for more details.
//
// ```elvish-transcript
// ~> path:clean ./../bin
// ▶ ../bin
// ```

//elvdoc:fn dir
//
// ```elvish
// path:dir $path
// ```
//
// Outputs all but the last element of `$path`, typically the path's enclosing directory. See the
// [Go documentation](https://pkg.go.dev/path/filepath#Dir) for more details. This is analogous to
// the POSIX `dirname` command.
//
// ```elvish-transcript
// ~> path:dir /a/b/c/something
// ▶ /a/b/c
// ```

//elvdoc:fn ext
//
// ```elvish
// ext $path
// ```
//
// Outputs the file name extension used by `$path` (including the separating period). If there is no
// extension the empty string is output. See the [Go
// documentation](https://pkg.go.dev/path/filepath#Ext) for more details.
//
// ```elvish-transcript
// ~> path:ext hello.elv
// ▶ .elv
// ```

//elvdoc:fn is-abs
//
// ```elvish
// is-abs $path
// ```
//
// Outputs `$true` if the path is an abolute path. Note that platforms like Windows have different
// rules than UNIX like platforms for what constitutes an absolute path. See the [Go
// documentation](https://pkg.go.dev/path/filepath#IsAbs) for more details.
//
// ```elvish-transcript
// ~> path:is-abs hello.elv
// ▶ false
// ~> path:is-abs /hello.elv
// ▶ true
// ```

//elvdoc:fn eval-symlinks
//
// ```elvish-transcript
// ~> mkdir bin
// ~> ln -s bin sbin
// ~> path:eval-symlinks ./sbin/a_command
// ▶ bin/a_command
// ```
//
// Outputs `$path` after resolving any symbolic links. If `$path` is relative the result will be
// relative to the current directory, unless one of the components is an absolute symbolic link.
// This function calls `path:clean` on the result before outputing it. This is analogous to the
// external `realpath` or `readlink` command found on many systems. See the [Go
// documentation](https://pkg.go.dev/path/filepath#EvalSymlinks) for more details.

//elvdoc:fn is-dir
//
// ```elvish
// is-dir $path
// ```
//
// Outputs `$true` if the path resolves to a directory. If the final element of the path is a
// symlink, even if it points to a directory, it still outputs `$false` since a symlink is not a
// directory. Use [`eval-symlinks`](#patheval-symlinks) on the path first if you don't care if the
// final element is a symlink.
//
// ```elvish-transcript
// ~> touch not-a-dir
// ~> path:is-dir not-a-dir
// ▶ false
// ~> path:is-dir /tmp
// ▶ true
// ```

func isDir(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.Mode().IsDir()
}

//elvdoc:fn is-regular
//
// ```elvish
// is-regular $path
// ```
//
// Outputs `$true` if the path resolves to a regular file. If the final element of the path is a
// symlink, even if it points to a regular file, it still outputs `$false` since a symlink is not a
// regular file. Use [`eval-symlinks`](#patheval-symlinks) on the path first if you don't care if
// the final element is a symlink.
//
// ```elvish-transcript
// ~> touch not-a-dir
// ~> path:is-regular not-a-dir
// ▶ true
// ~> path:is-dir /tmp
// ▶ false
// ```

func isRegular(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.Mode().IsRegular()
}
<|endoftext|>


<commit_before>package landscaper

import (
	"encoding/json"
	"errors"
	"reflect"

	"github.com/pmezard/go-difflib/difflib"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"k8s.io/helm/pkg/helm"
)

// Executor is responsible for applying a desired landscape to the actual landscape
type Executor interface {
	Apply(Components, Components) error

	CreateComponent(*Component) error
	UpdateComponent(*Component) error
	DeleteComponent(*Component) error
}

type executor struct {
	helmClient     helm.Interface
	chartLoader    ChartLoader
	kubeSecrets    SecretsWriteDeleter
	dryRun         bool
	wait           bool
	waitTimeout    int64
	disabledStages []string
}

// NewExecutor is a factory method to create a new Executor
func NewExecutor(helmClient helm.Interface, chartLoader ChartLoader, kubeSecrets SecretsWriteDeleter, dryRun bool, wait bool, waitTimeout int64, disabledStages []string) Executor {
	return &executor{
		helmClient:     helmClient,
		chartLoader:    chartLoader,
		kubeSecrets:    kubeSecrets,
		dryRun:         dryRun,
		wait:           wait,
		waitTimeout:    waitTimeout,
		disabledStages: disabledStages,
	}
}

// gatherForcedUpdates returns a map that for each to-be-updated component indicates if it needs a forced update.
// there may be several reasons to do so: releases that differ only in secret values are forced so that pods will restart with the new values; releases that differ in namespace cannot be updated
func (e *executor) gatherForcedUpdates(current, update Components) (map[string]bool, error) {
	needForcedUpdate := map[string]bool{}

	for _, cmp := range update {
		// releases that differ only in secret values are forced so that pods will restart with the new values
		for _, curCmp := range current {
			if curCmp.Name == cmp.Name && isOnlySecretValueDiff(*curCmp, *cmp) {
				logrus.Infof("%s differs in secrets values only; don't update but delete + create instead", cmp.Name)
				needForcedUpdate[cmp.Name] = true
			}
		}
		if curCmp := current[cmp.Name]; curCmp != nil {
			if curCmp.Namespace != cmp.Namespace {
				logrus.Infof("%s differs in namespace; don't update but delete + create instead", cmp.Name)
				needForcedUpdate[cmp.Name] = true
			}
		}
	}

	return needForcedUpdate, nil
}

// Apply transforms the current state into the desired state
func (e *executor) Apply(desired, current Components) error {
	create, update, delete := diff(desired, current)

	// some to-be-updated components need a delete + create instead
	needForcedUpdate, err := e.gatherForcedUpdates(current, update)
	if err != nil {
		return err
	}

	// delete+create pairs will never work in dry run since the dry-run "deleted" component will exist in create
	if !e.dryRun {
		create, update, delete = integrateForcedUpdates(current, create, update, delete, needForcedUpdate)
	}

	logrus.WithFields(logrus.Fields{"create": len(create), "update": len(update), "delete": len(delete)}).Info("Apply desired state")

	if err := logDifferences(current, create, update, delete, logrus.Infof); err != nil {
		return err
	}

	if e.stageEnabled("delete") {
		for _, cmp := range delete {
			if err := e.DeleteComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("DeleteComponent failed")
				return err
			}
		}
	}

	if e.stageEnabled("update") {
		for _, cmp := range update {
			if err := e.UpdateComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("UpdateComponent failed")
				return err
			}
		}
	}

	if e.stageEnabled("create") {
		for _, cmp := range create {
			if err := e.CreateComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("CreateComponent failed")
				return err
			}
		}
	}

	logrus.WithFields(logrus.Fields{"created": len(create), "updated": len(update), "deleted": len(delete)}).Info("Applied desired state successfully")
	return nil
}

func (e *executor) stageEnabled(stage string) bool {
	for _, stageDisabled := range e.disabledStages {
		if stageDisabled == stage {
			return false
		}
	}
	return true
}

// CreateComponent creates the given Component
func (e *executor) CreateComponent(cmp *Component) error {
	// We need to ensure the chart is available on the local system. LoadChart will ensure
	// this is the case by downloading the chart if it is not there yet
	chartRef, err := cmp.FullChartRef()
	if err != nil {
		return err
	}
	_, chartPath, err := e.chartLoader.Load(chartRef)
	if err != nil {
		return err
	}

	rawValues, err := cmp.Configuration.YAML()
	if err != nil {
		return err
	}

	logrus.WithFields(logrus.Fields{
		"release":   cmp.Name,
		"chart":     cmp.Release.Chart,
		"chartPath": chartPath,
		"rawValues": rawValues,
		"values":    cmp.Configuration,
		"dryrun":    e.dryRun,
	}).Debug("Create component")

	if len(cmp.SecretValues) > 0 && !e.dryRun {
		err = e.kubeSecrets.Write(cmp.Name, cmp.Namespace, cmp.SecretValues)
		if err != nil {
			return err
		}
	}

	_, err = e.helmClient.InstallRelease(
		chartPath,
		cmp.Namespace,
		helm.ValueOverrides([]byte(rawValues)),
		helm.ReleaseName(cmp.Name),
		helm.InstallDryRun(e.dryRun),
		helm.InstallReuseName(true),
		helm.InstallWait(e.wait),
		helm.InstallTimeout(e.waitTimeout),
	)
	if err != nil {
		return errors.New(grpc.ErrorDesc(err))
	}

	return nil
}

// UpdateComponent updates the given Component
func (e *executor) UpdateComponent(cmp *Component) error {
	// We need to ensure the chart is available on the local system. LoadChart will ensure
	// this is the case by downloading the chart if it is not there yet
	chartRef, err := cmp.FullChartRef()
	if err != nil {
		return err
	}
	_, chartPath, err := e.chartLoader.Load(chartRef)
	if err != nil {
		return err
	}

	rawValues, err := cmp.Configuration.YAML()
	if err != nil {
		return err
	}

	if !e.dryRun {
		if e.stageEnabled("deleteSecrets") || len(cmp.SecretValues) > 0 {
			err = e.kubeSecrets.Delete(cmp.Name, cmp.Namespace)
		}

		if len(cmp.SecretValues) > 0 {
			err = e.kubeSecrets.Write(cmp.Name, cmp.Namespace, cmp.SecretValues)
			if err != nil {
				return err
			}
		}
	}

	logrus.WithFields(logrus.Fields{
		"release":   cmp.Name,
		"chart":     cmp.Release.Chart,
		"chartPath": chartPath,
		"values":    cmp.Configuration,
		"dryrun":    e.dryRun,
	}).Debug("Update component")

	_, err = e.helmClient.UpdateRelease(
		cmp.Name,
		chartPath,
		helm.UpdateValueOverrides([]byte(rawValues)),
		helm.UpgradeDryRun(e.dryRun),
		helm.UpgradeWait(e.wait),
		helm.UpgradeTimeout(e.waitTimeout),
	)
	if err != nil {
		return errors.New(grpc.ErrorDesc(err))
	}

	return nil
}

// DeleteComponent removes the given Component
func (e *executor) DeleteComponent(cmp *Component) error {
	logrus.WithFields(logrus.Fields{
		"release": cmp.Name,
		"values":  cmp.Configuration,
		"dryrun":  e.dryRun,
	}).Debug("Delete component")

	if len(cmp.SecretValues) > 0 && !e.dryRun {
		err := e.kubeSecrets.Delete(cmp.Name, cmp.Namespace)
		if err != nil {
			return err
		}
	}

	if !e.dryRun {
		_, err := e.helmClient.DeleteRelease(
			cmp.Name,
			helm.DeletePurge(true),
			helm.DeleteDryRun(e.dryRun),
		)
		if err != nil {
			return errors.New(grpc.ErrorDesc(err))
		}
	}

	return nil
}

// diff takes desired and current components, and returns the components to create, update and delete to get from current to desired
func diff(desired, current Components) (Components, Components, Components) {
	create := Components{}
	update := Components{}
	delete := Components{}

	for name, desiredCmp := range desired {
		if currentCmp, ok := current[name]; ok {
			if !desiredCmp.Equals(currentCmp) {
				update[name] = desiredCmp
			}
		} else {
			create[name] = desiredCmp
		}
	}

	for name, currentCmp := range current {
		if _, ok := desired[name]; !ok {
			delete[name] = currentCmp
		}
	}

	return create, update, delete
}

// componentDiffText returns a diff as text. current and desired can be nil and indicate non-existence (e.g. current nil and desired non-nil means: create)
func componentDiffText(current, desired *Component) (string, error) {
	cText, dText := []string{}, []string{}
	cName, dName := "<none>", "<none>"
	if current != nil {
		cs, err := json.MarshalIndent(current, "", "  ")
		if err != nil {
			return "", err
		}
		cText = difflib.SplitLines(string(cs))
		cName = current.Name
	}
	if desired != nil {
		ds, err := json.MarshalIndent(desired, "", "  ")
		if err != nil {
			return "", err
		}
		dText = difflib.SplitLines(string(ds))
		dName = desired.Name
	}

	return difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
		A:        cText,
		FromFile: "Current " + cName,
		B:        dText,
		ToFile:   "Desired " + dName,
		Context:  3,
	})
}

// logDifferences logs the Create, Update and Delete w.r.t. current to logf
func logDifferences(current, creates, updates, deletes Components, logf func(format string, args ...interface{})) error {
	log := func(action string, current, desired *Component) error {
		diff, err := componentDiffText(current, desired)
		if err != nil {
			return err
		}
		logf("%s", action)
		if diff != "" {
			logf("Diff:\n%s", diff)
		}
		if current != nil && desired != nil && !reflect.DeepEqual(current.SecretValues, desired.SecretValues) {
			logrus.Info("Diff: secrets have changed, not shown here")
		}
		return nil
	}

	for _, d := range deletes {
		logf("Delete: %s", d.Name)
	}

	for _, d := range creates {
		if err := log("Create: "+d.Name, nil, d); err != nil {
			return err
		}
	}

	for _, d := range updates {
		c := current[d.Name]
		if err := log("Update: "+d.Name, c, d); err != nil {
			return err
		}
	}

	return nil
}

// integrateForcedUpdates removes forceUpdate from update and inserts it into delete + create
func integrateForcedUpdates(current, create, update, delete Components, forceUpdate map[string]bool) (Components, Components, Components) {
	fixUpdate := Components{}
	for _, cmp := range update {
		if forceUpdate[cmp.Name] {
			if currentCmp, ok := current[cmp.Name]; ok {
				delete[currentCmp.Name] = currentCmp // delete the current component
			}
			create[cmp.Name] = cmp // create cmp, by definition a desired component
		} else {
			fixUpdate[cmp.Name] = cmp
		}
	}
	return create, fixUpdate, delete
}

// isOnlySecretValueDiff tells whether the given Components differ in their .SecretValues fields and are identical otherwise
func isOnlySecretValueDiff(a, b Component) bool {
	secValsEqual := reflect.DeepEqual(a.SecretValues, b.SecretValues)
	a.SecretValues = SecretValues{}
	b.SecretValues = SecretValues{}
	return !secValsEqual && reflect.DeepEqual(a, b)
}
<commit_msg>feat(log) Only log relevant statements<commit_after>package landscaper

import (
	"encoding/json"
	"errors"
	"reflect"

	"github.com/pmezard/go-difflib/difflib"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"k8s.io/helm/pkg/helm"
)

// Executor is responsible for applying a desired landscape to the actual landscape
type Executor interface {
	Apply(Components, Components) error

	CreateComponent(*Component) error
	UpdateComponent(*Component) error
	DeleteComponent(*Component) error
}

type executor struct {
	helmClient     helm.Interface
	chartLoader    ChartLoader
	kubeSecrets    SecretsWriteDeleter
	dryRun         bool
	wait           bool
	waitTimeout    int64
	disabledStages []string
}

// NewExecutor is a factory method to create a new Executor
func NewExecutor(helmClient helm.Interface, chartLoader ChartLoader, kubeSecrets SecretsWriteDeleter, dryRun bool, wait bool, waitTimeout int64, disabledStages []string) Executor {
	return &executor{
		helmClient:     helmClient,
		chartLoader:    chartLoader,
		kubeSecrets:    kubeSecrets,
		dryRun:         dryRun,
		wait:           wait,
		waitTimeout:    waitTimeout,
		disabledStages: disabledStages,
	}
}

// gatherForcedUpdates returns a map that for each to-be-updated component indicates if it needs a forced update.
// there may be several reasons to do so: releases that differ only in secret values are forced so that pods will restart with the new values; releases that differ in namespace cannot be updated
func (e *executor) gatherForcedUpdates(current, update Components) (map[string]bool, error) {
	needForcedUpdate := map[string]bool{}

	for _, cmp := range update {
		// releases that differ only in secret values are forced so that pods will restart with the new values
		for _, curCmp := range current {
			if curCmp.Name == cmp.Name && isOnlySecretValueDiff(*curCmp, *cmp) {
				logrus.Infof("%s differs in secrets values only; don't update but delete + create instead", cmp.Name)
				needForcedUpdate[cmp.Name] = true
			}
		}
		if curCmp := current[cmp.Name]; curCmp != nil {
			if curCmp.Namespace != cmp.Namespace {
				logrus.Infof("%s differs in namespace; don't update but delete + create instead", cmp.Name)
				needForcedUpdate[cmp.Name] = true
			}
		}
	}

	return needForcedUpdate, nil
}

// Apply transforms the current state into the desired state
func (e *executor) Apply(desired, current Components) error {
	create, update, delete := diff(desired, current)

	// some to-be-updated components need a delete + create instead
	needForcedUpdate, err := e.gatherForcedUpdates(current, update)
	if err != nil {
		return err
	}

	// delete+create pairs will never work in dry run since the dry-run "deleted" component will exist in create
	if !e.dryRun {
		create, update, delete = integrateForcedUpdates(current, create, update, delete, needForcedUpdate)
	}

	logrus.WithFields(logrus.Fields{"create": len(create), "update": len(update), "delete": len(delete)}).Info("Apply desired state")

	if err := logDifferences(current, create, update, delete, e.stageEnabled("update"), e.stageEnabled("delete"), logrus.Infof); err != nil {
		return err
	}

	if e.stageEnabled("delete") {
		for _, cmp := range delete {
			if err := e.DeleteComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("DeleteComponent failed")
				return err
			}
		}
	}

	if e.stageEnabled("update") {
		for _, cmp := range update {
			if err := e.UpdateComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("UpdateComponent failed")
				return err
			}
		}
	}

	if e.stageEnabled("create") {
		for _, cmp := range create {
			if err := e.CreateComponent(cmp); err != nil {
				logrus.WithFields(logrus.Fields{"error": err}).Error("CreateComponent failed")
				return err
			}
		}
	}

	logrus.WithFields(logrus.Fields{"created": len(create), "updated": len(update), "deleted": len(delete)}).Info("Applied desired state successfully")
	return nil
}

func (e *executor) stageEnabled(stage string) bool {
	for _, stageDisabled := range e.disabledStages {
		if stageDisabled == stage {
			return false
		}
	}
	return true
}

// CreateComponent creates the given Component
func (e *executor) CreateComponent(cmp *Component) error {
	// We need to ensure the chart is available on the local system. LoadChart will ensure
	// this is the case by downloading the chart if it is not there yet
	chartRef, err := cmp.FullChartRef()
	if err != nil {
		return err
	}
	_, chartPath, err := e.chartLoader.Load(chartRef)
	if err != nil {
		return err
	}

	rawValues, err := cmp.Configuration.YAML()
	if err != nil {
		return err
	}

	logrus.WithFields(logrus.Fields{
		"release":   cmp.Name,
		"chart":     cmp.Release.Chart,
		"chartPath": chartPath,
		"rawValues": rawValues,
		"values":    cmp.Configuration,
		"dryrun":    e.dryRun,
	}).Debug("Create component")

	if len(cmp.SecretValues) > 0 && !e.dryRun {
		err = e.kubeSecrets.Write(cmp.Name, cmp.Namespace, cmp.SecretValues)
		if err != nil {
			return err
		}
	}

	_, err = e.helmClient.InstallRelease(
		chartPath,
		cmp.Namespace,
		helm.ValueOverrides([]byte(rawValues)),
		helm.ReleaseName(cmp.Name),
		helm.InstallDryRun(e.dryRun),
		helm.InstallReuseName(true),
		helm.InstallWait(e.wait),
		helm.InstallTimeout(e.waitTimeout),
	)
	if err != nil {
		return errors.New(grpc.ErrorDesc(err))
	}

	return nil
}

// UpdateComponent updates the given Component
func (e *executor) UpdateComponent(cmp *Component) error {
	// We need to ensure the chart is available on the local system. LoadChart will ensure
	// this is the case by downloading the chart if it is not there yet
	chartRef, err := cmp.FullChartRef()
	if err != nil {
		return err
	}
	_, chartPath, err := e.chartLoader.Load(chartRef)
	if err != nil {
		return err
	}

	rawValues, err := cmp.Configuration.YAML()
	if err != nil {
		return err
	}

	if !e.dryRun {
		if e.stageEnabled("deleteSecrets") || len(cmp.SecretValues) > 0 {
			err = e.kubeSecrets.Delete(cmp.Name, cmp.Namespace)
		}

		if len(cmp.SecretValues) > 0 {
			err = e.kubeSecrets.Write(cmp.Name, cmp.Namespace, cmp.SecretValues)
			if err != nil {
				return err
			}
		}
	}

	logrus.WithFields(logrus.Fields{
		"release":   cmp.Name,
		"chart":     cmp.Release.Chart,
		"chartPath": chartPath,
		"values":    cmp.Configuration,
		"dryrun":    e.dryRun,
	}).Debug("Update component")

	_, err = e.helmClient.UpdateRelease(
		cmp.Name,
		chartPath,
		helm.UpdateValueOverrides([]byte(rawValues)),
		helm.UpgradeDryRun(e.dryRun),
		helm.UpgradeWait(e.wait),
		helm.UpgradeTimeout(e.waitTimeout),
	)
	if err != nil {
		return errors.New(grpc.ErrorDesc(err))
	}

	return nil
}

// DeleteComponent removes the given Component
func (e *executor) DeleteComponent(cmp *Component) error {
	logrus.WithFields(logrus.Fields{
		"release": cmp.Name,
		"values":  cmp.Configuration,
		"dryrun":  e.dryRun,
	}).Debug("Delete component")

	if len(cmp.SecretValues) > 0 && !e.dryRun {
		err := e.kubeSecrets.Delete(cmp.Name, cmp.Namespace)
		if err != nil {
			return err
		}
	}

	if !e.dryRun {
		_, err := e.helmClient.DeleteRelease(
			cmp.Name,
			helm.DeletePurge(true),
			helm.DeleteDryRun(e.dryRun),
		)
		if err != nil {
			return errors.New(grpc.ErrorDesc(err))
		}
	}

	return nil
}

// diff takes desired and current components, and returns the components to create, update and delete to get from current to desired
func diff(desired, current Components) (Components, Components, Components) {
	create := Components{}
	update := Components{}
	delete := Components{}

	for name, desiredCmp := range desired {
		if currentCmp, ok := current[name]; ok {
			if !desiredCmp.Equals(currentCmp) {
				update[name] = desiredCmp
			}
		} else {
			create[name] = desiredCmp
		}
	}

	for name, currentCmp := range current {
		if _, ok := desired[name]; !ok {
			delete[name] = currentCmp
		}
	}

	return create, update, delete
}

// componentDiffText returns a diff as text. current and desired can be nil and indicate non-existence (e.g. current nil and desired non-nil means: create)
func componentDiffText(current, desired *Component) (string, error) {
	cText, dText := []string{}, []string{}
	cName, dName := "<none>", "<none>"
	if current != nil {
		cs, err := json.MarshalIndent(current, "", "  ")
		if err != nil {
			return "", err
		}
		cText = difflib.SplitLines(string(cs))
		cName = current.Name
	}
	if desired != nil {
		ds, err := json.MarshalIndent(desired, "", "  ")
		if err != nil {
			return "", err
		}
		dText = difflib.SplitLines(string(ds))
		dName = desired.Name
	}

	return difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
		A:        cText,
		FromFile: "Current " + cName,
		B:        dText,
		ToFile:   "Desired " + dName,
		Context:  3,
	})
}

// logDifferences logs the Create, Update and Delete w.r.t. current to logf
func logDifferences(current, creates, updates, deletes Components, updateStageEnabled bool, deleteStageEnabled bool, logf func(format string, args ...interface{})) error {
	log := func(action string, current, desired *Component) error {
		diff, err := componentDiffText(current, desired)
		if err != nil {
			return err
		}
		logf("%s", action)
		if diff != "" {
			logf("Diff:\n%s", diff)
		}
		if current != nil && desired != nil && !reflect.DeepEqual(current.SecretValues, desired.SecretValues) {
			logrus.Info("Diff: secrets have changed, not shown here")
		}
		return nil
	}

	// Log diffs only if delete is applied
	if deleteStageEnabled {
		for _, d := range deletes {
			logf("Delete: %s", d.Name)
		}
	}

	for _, d := range creates {
		if err := log("Create: "+d.Name, nil, d); err != nil {
			return err
		}
	}

	// Log diffs only if upadete is applied
	if updateStageEnabled {
		for _, d := range updates {
			c := current[d.Name]
			if err := log("Update: "+d.Name, c, d); err != nil {
				return err
			}
		}
	}

	return nil
}

// integrateForcedUpdates removes forceUpdate from update and inserts it into delete + create
func integrateForcedUpdates(current, create, update, delete Components, forceUpdate map[string]bool) (Components, Components, Components) {
	fixUpdate := Components{}
	for _, cmp := range update {
		if forceUpdate[cmp.Name] {
			if currentCmp, ok := current[cmp.Name]; ok {
				delete[currentCmp.Name] = currentCmp // delete the current component
			}
			create[cmp.Name] = cmp // create cmp, by definition a desired component
		} else {
			fixUpdate[cmp.Name] = cmp
		}
	}
	return create, fixUpdate, delete
}

// isOnlySecretValueDiff tells whether the given Components differ in their .SecretValues fields and are identical otherwise
func isOnlySecretValueDiff(a, b Component) bool {
	secValsEqual := reflect.DeepEqual(a.SecretValues, b.SecretValues)
	a.SecretValues = SecretValues{}
	b.SecretValues = SecretValues{}
	return !secValsEqual && reflect.DeepEqual(a, b)
}
<|endoftext|>


<commit_before>/*
 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
 * Copyright 2020 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     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 lifecycle contains all the lifecycle related data types and marshallers.
package lifecycle

import (
	"encoding/xml"
	"time"
)

// AbortIncompleteMultipartUpload structure, not supported yet on MinIO
type AbortIncompleteMultipartUpload struct {
	XMLName             xml.Name       `xml:"AbortIncompleteMultipartUpload,omitempty"  json:"-"`
	DaysAfterInitiation ExpirationDays `xml:"DaysAfterInitiation,omitempty" json:"DaysAfterInitiation,omitempty"`
}

// IsDaysNull returns true if days field is null
func (n AbortIncompleteMultipartUpload) IsDaysNull() bool {
	return n.DaysAfterInitiation == ExpirationDays(0)
}

// MarshalXML if days after initiation is set to non-zero value
func (n AbortIncompleteMultipartUpload) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type abortIncompleteMultipartUploadWrapper AbortIncompleteMultipartUpload
	return e.EncodeElement(abortIncompleteMultipartUploadWrapper(n), start)
}

// NoncurrentVersionExpiration - Specifies when noncurrent object versions expire.
// Upon expiration, server permanently deletes the noncurrent object versions.
// Set this lifecycle configuration action on a bucket that has versioning enabled
// (or suspended) to request server delete noncurrent object versions at a
// specific period in the object's lifetime.
type NoncurrentVersionExpiration struct {
	XMLName        xml.Name       `xml:"NoncurrentVersionExpiration" json:"-"`
	NoncurrentDays ExpirationDays `xml:"NoncurrentDays,omitempty"`
}

// MarshalXML if non-current days not set to non zero value
func (n NoncurrentVersionExpiration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type noncurrentVersionExpirationWrapper NoncurrentVersionExpiration
	return e.EncodeElement(noncurrentVersionExpirationWrapper(n), start)
}

// IsDaysNull returns true if days field is null
func (n NoncurrentVersionExpiration) IsDaysNull() bool {
	return n.NoncurrentDays == ExpirationDays(0)
}

// NoncurrentVersionTransition structure, set this action to request server to
// transition noncurrent object versions to different set storage classes
// at a specific period in the object's lifetime.
type NoncurrentVersionTransition struct {
	XMLName        xml.Name       `xml:"NoncurrentVersionTransition,omitempty"  json:"-"`
	StorageClass   string         `xml:"StorageClass,omitempty" json:"StorageClass,omitempty"`
	NoncurrentDays ExpirationDays `xml:"NoncurrentDays,omitempty" json:"NoncurrentDays,omitempty"`
}

// IsDaysNull returns true if days field is null
func (n NoncurrentVersionTransition) IsDaysNull() bool {
	return n.NoncurrentDays == ExpirationDays(0)
}

// MarshalXML is extended to leave out
// <NoncurrentVersionTransition></NoncurrentVersionTransition> tags
func (n NoncurrentVersionTransition) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type noncurrentVersionTransitionWrapper NoncurrentVersionTransition
	return e.EncodeElement(noncurrentVersionTransitionWrapper(n), start)
}

// Tag structure key/value pair representing an object tag to apply lifecycle configuration
type Tag struct {
	XMLName xml.Name `xml:"Tag,omitempty" json:"-"`
	Key     string   `xml:"Key,omitempty" json:"Key,omitempty"`
	Value   string   `xml:"Value,omitempty" json:"Value,omitempty"`
}

// IsEmpty returns whether this tag is empty or not.
func (tag Tag) IsEmpty() bool {
	return tag.Key == ""
}

// Transition structure - transition details of lifecycle configuration
type Transition struct {
	XMLName      xml.Name       `xml:"Transition" json:"-"`
	Date         ExpirationDate `xml:"Date,omitempty" json:"Date,omitempty"`
	StorageClass string         `xml:"StorageClass,omitempty" json:"StorageClass,omitempty"`
	Days         ExpirationDays `xml:"Days,omitempty" json:"Days,omitempty"`
}

// IsDaysNull returns true if days field is null
func (t Transition) IsDaysNull() bool {
	return t.Days == ExpirationDays(0)
}

// IsDateNull returns true if date field is null
func (t Transition) IsDateNull() bool {
	return t.Date.Time.IsZero()
}

// IsNull returns true if both date and days fields are null
func (t Transition) IsNull() bool {
	return t.IsDaysNull() && t.IsDateNull()
}

// MarshalXML is transition is non null
func (t Transition) MarshalXML(en *xml.Encoder, startElement xml.StartElement) error {
	if t.IsNull() {
		return nil
	}
	type transitionWrapper Transition
	return en.EncodeElement(transitionWrapper(t), startElement)
}

// And And Rule for LifecycleTag, to be used in LifecycleRuleFilter
type And struct {
	XMLName xml.Name `xml:"And,omitempty" json:"-"`
	Prefix  string   `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Tags    []Tag    `xml:"Tag,omitempty" json:"Tags,omitempty"`
}

// IsEmpty returns true if Tags field is null
func (a And) IsEmpty() bool {
	return len(a.Tags) == 0 && a.Prefix == ""
}

// Filter will be used in selecting rule(s) for lifecycle configuration
type Filter struct {
	XMLName xml.Name `xml:"Filter" json:"-"`
	And     And      `xml:"And,omitempty" json:"And,omitempty"`
	Prefix  string   `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Tag     Tag      `xml:"Tag,omitempty" json:"Tag,omitempty"`
}

// MarshalXML - produces the xml representation of the Filter struct
// only one of Prefix, And and Tag should be present in the output.
func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if err := e.EncodeToken(start); err != nil {
		return err
	}

	switch {
	case !f.And.IsEmpty():
		if err := e.EncodeElement(f.And, xml.StartElement{Name: xml.Name{Local: "And"}}); err != nil {
			return err
		}
	case !f.Tag.IsEmpty():
		if err := e.EncodeElement(f.Tag, xml.StartElement{Name: xml.Name{Local: "Tag"}}); err != nil {
			return err
		}
	default:
		// Always print Prefix field when both And & Tag are empty
		if err := e.EncodeElement(f.Prefix, xml.StartElement{Name: xml.Name{Local: "Prefix"}}); err != nil {
			return err
		}
	}

	return e.EncodeToken(xml.EndElement{Name: start.Name})
}

// ExpirationDays is a type alias to unmarshal Days in Expiration
type ExpirationDays int

// MarshalXML encodes number of days to expire if it is non-zero and
// encodes empty string otherwise
func (eDays ExpirationDays) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if eDays == 0 {
		return nil
	}
	return e.EncodeElement(int(eDays), startElement)
}

// ExpirationDate is a embedded type containing time.Time to unmarshal
// Date in Expiration
type ExpirationDate struct {
	time.Time
}

// MarshalXML encodes expiration date if it is non-zero and encodes
// empty string otherwise
func (eDate ExpirationDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if eDate.Time.IsZero() {
		return nil
	}
	return e.EncodeElement(eDate.Format(time.RFC3339), startElement)
}

// ExpireDeleteMarker represents value of ExpiredObjectDeleteMarker field in Expiration XML element.
type ExpireDeleteMarker bool

// MarshalXML encodes delete marker boolean into an XML form.
func (b ExpireDeleteMarker) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if !b {
		return nil
	}
	type expireDeleteMarkerWrapper ExpireDeleteMarker
	return e.EncodeElement(expireDeleteMarkerWrapper(b), startElement)
}

// Expiration structure - expiration details of lifecycle configuration
type Expiration struct {
	XMLName      xml.Name           `xml:"Expiration,omitempty" json:"-"`
	Date         ExpirationDate     `xml:"Date,omitempty" json:"Date,omitempty"`
	Days         ExpirationDays     `xml:"Days,omitempty" json:"Days,omitempty"`
	DeleteMarker ExpireDeleteMarker `xml:"ExpiredObjectDeleteMarker,omitempty"`
}

// IsDaysNull returns true if days field is null
func (e Expiration) IsDaysNull() bool {
	return e.Days == ExpirationDays(0)
}

// IsDateNull returns true if date field is null
func (e Expiration) IsDateNull() bool {
	return e.Date.Time.IsZero()
}

// IsNull returns true if both date and days fields are null
func (e Expiration) IsNull() bool {
	return e.IsDaysNull() && e.IsDateNull()
}

// MarshalXML is expiration is non null
func (e Expiration) MarshalXML(en *xml.Encoder, startElement xml.StartElement) error {
	if e.IsNull() {
		return nil
	}
	type expirationWrapper Expiration
	return en.EncodeElement(expirationWrapper(e), startElement)
}

// Rule represents a single rule in lifecycle configuration
type Rule struct {
	XMLName                        xml.Name                       `xml:"Rule,omitempty" json:"-"`
	AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty" json:"AbortIncompleteMultipartUpload,omitempty"`
	Expiration                     Expiration                     `xml:"Expiration,omitempty" json:"Expiration,omitempty"`
	ID                             string                         `xml:"ID" json:"ID"`
	RuleFilter                     Filter                         `xml:"Filter,omitempty" json:"Filter,omitempty"`
	NoncurrentVersionExpiration    NoncurrentVersionExpiration    `xml:"NoncurrentVersionExpiration,omitempty"  json:"NoncurrentVersionExpiration,omitempty"`
	NoncurrentVersionTransition    NoncurrentVersionTransition    `xml:"NoncurrentVersionTransition,omitempty" json:"NoncurrentVersionTransition,omitempty"`
	Prefix                         string                         `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Status                         string                         `xml:"Status" json:"Status"`
	Transition                     Transition                     `xml:"Transition,omitempty" json:"Transition,omitempty"`
}

// Configuration is a collection of Rule objects.
type Configuration struct {
	XMLName xml.Name `xml:"LifecycleConfiguration,omitempty" json:"-"`
	Rules   []Rule   `xml:"Rule"`
}

// Empty check if lifecycle configuration is empty
func (c *Configuration) Empty() bool {
	if c == nil {
		return true
	}
	return len(c.Rules) == 0
}

// NewConfiguration initializes a fresh lifecycle configuration
// for manipulation, such as setting and removing lifecycle rules
// and filters.
func NewConfiguration() *Configuration {
	return &Configuration{}
}
<commit_msg>lifecycle: Add IsEnabled() to ExpireDeleteMarker (#1441)<commit_after>/*
 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
 * Copyright 2020 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     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 lifecycle contains all the lifecycle related data types and marshallers.
package lifecycle

import (
	"encoding/xml"
	"time"
)

// AbortIncompleteMultipartUpload structure, not supported yet on MinIO
type AbortIncompleteMultipartUpload struct {
	XMLName             xml.Name       `xml:"AbortIncompleteMultipartUpload,omitempty"  json:"-"`
	DaysAfterInitiation ExpirationDays `xml:"DaysAfterInitiation,omitempty" json:"DaysAfterInitiation,omitempty"`
}

// IsDaysNull returns true if days field is null
func (n AbortIncompleteMultipartUpload) IsDaysNull() bool {
	return n.DaysAfterInitiation == ExpirationDays(0)
}

// MarshalXML if days after initiation is set to non-zero value
func (n AbortIncompleteMultipartUpload) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type abortIncompleteMultipartUploadWrapper AbortIncompleteMultipartUpload
	return e.EncodeElement(abortIncompleteMultipartUploadWrapper(n), start)
}

// NoncurrentVersionExpiration - Specifies when noncurrent object versions expire.
// Upon expiration, server permanently deletes the noncurrent object versions.
// Set this lifecycle configuration action on a bucket that has versioning enabled
// (or suspended) to request server delete noncurrent object versions at a
// specific period in the object's lifetime.
type NoncurrentVersionExpiration struct {
	XMLName        xml.Name       `xml:"NoncurrentVersionExpiration" json:"-"`
	NoncurrentDays ExpirationDays `xml:"NoncurrentDays,omitempty"`
}

// MarshalXML if non-current days not set to non zero value
func (n NoncurrentVersionExpiration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type noncurrentVersionExpirationWrapper NoncurrentVersionExpiration
	return e.EncodeElement(noncurrentVersionExpirationWrapper(n), start)
}

// IsDaysNull returns true if days field is null
func (n NoncurrentVersionExpiration) IsDaysNull() bool {
	return n.NoncurrentDays == ExpirationDays(0)
}

// NoncurrentVersionTransition structure, set this action to request server to
// transition noncurrent object versions to different set storage classes
// at a specific period in the object's lifetime.
type NoncurrentVersionTransition struct {
	XMLName        xml.Name       `xml:"NoncurrentVersionTransition,omitempty"  json:"-"`
	StorageClass   string         `xml:"StorageClass,omitempty" json:"StorageClass,omitempty"`
	NoncurrentDays ExpirationDays `xml:"NoncurrentDays,omitempty" json:"NoncurrentDays,omitempty"`
}

// IsDaysNull returns true if days field is null
func (n NoncurrentVersionTransition) IsDaysNull() bool {
	return n.NoncurrentDays == ExpirationDays(0)
}

// MarshalXML is extended to leave out
// <NoncurrentVersionTransition></NoncurrentVersionTransition> tags
func (n NoncurrentVersionTransition) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if n.IsDaysNull() {
		return nil
	}
	type noncurrentVersionTransitionWrapper NoncurrentVersionTransition
	return e.EncodeElement(noncurrentVersionTransitionWrapper(n), start)
}

// Tag structure key/value pair representing an object tag to apply lifecycle configuration
type Tag struct {
	XMLName xml.Name `xml:"Tag,omitempty" json:"-"`
	Key     string   `xml:"Key,omitempty" json:"Key,omitempty"`
	Value   string   `xml:"Value,omitempty" json:"Value,omitempty"`
}

// IsEmpty returns whether this tag is empty or not.
func (tag Tag) IsEmpty() bool {
	return tag.Key == ""
}

// Transition structure - transition details of lifecycle configuration
type Transition struct {
	XMLName      xml.Name       `xml:"Transition" json:"-"`
	Date         ExpirationDate `xml:"Date,omitempty" json:"Date,omitempty"`
	StorageClass string         `xml:"StorageClass,omitempty" json:"StorageClass,omitempty"`
	Days         ExpirationDays `xml:"Days,omitempty" json:"Days,omitempty"`
}

// IsDaysNull returns true if days field is null
func (t Transition) IsDaysNull() bool {
	return t.Days == ExpirationDays(0)
}

// IsDateNull returns true if date field is null
func (t Transition) IsDateNull() bool {
	return t.Date.Time.IsZero()
}

// IsNull returns true if both date and days fields are null
func (t Transition) IsNull() bool {
	return t.IsDaysNull() && t.IsDateNull()
}

// MarshalXML is transition is non null
func (t Transition) MarshalXML(en *xml.Encoder, startElement xml.StartElement) error {
	if t.IsNull() {
		return nil
	}
	type transitionWrapper Transition
	return en.EncodeElement(transitionWrapper(t), startElement)
}

// And And Rule for LifecycleTag, to be used in LifecycleRuleFilter
type And struct {
	XMLName xml.Name `xml:"And,omitempty" json:"-"`
	Prefix  string   `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Tags    []Tag    `xml:"Tag,omitempty" json:"Tags,omitempty"`
}

// IsEmpty returns true if Tags field is null
func (a And) IsEmpty() bool {
	return len(a.Tags) == 0 && a.Prefix == ""
}

// Filter will be used in selecting rule(s) for lifecycle configuration
type Filter struct {
	XMLName xml.Name `xml:"Filter" json:"-"`
	And     And      `xml:"And,omitempty" json:"And,omitempty"`
	Prefix  string   `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Tag     Tag      `xml:"Tag,omitempty" json:"Tag,omitempty"`
}

// MarshalXML - produces the xml representation of the Filter struct
// only one of Prefix, And and Tag should be present in the output.
func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if err := e.EncodeToken(start); err != nil {
		return err
	}

	switch {
	case !f.And.IsEmpty():
		if err := e.EncodeElement(f.And, xml.StartElement{Name: xml.Name{Local: "And"}}); err != nil {
			return err
		}
	case !f.Tag.IsEmpty():
		if err := e.EncodeElement(f.Tag, xml.StartElement{Name: xml.Name{Local: "Tag"}}); err != nil {
			return err
		}
	default:
		// Always print Prefix field when both And & Tag are empty
		if err := e.EncodeElement(f.Prefix, xml.StartElement{Name: xml.Name{Local: "Prefix"}}); err != nil {
			return err
		}
	}

	return e.EncodeToken(xml.EndElement{Name: start.Name})
}

// ExpirationDays is a type alias to unmarshal Days in Expiration
type ExpirationDays int

// MarshalXML encodes number of days to expire if it is non-zero and
// encodes empty string otherwise
func (eDays ExpirationDays) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if eDays == 0 {
		return nil
	}
	return e.EncodeElement(int(eDays), startElement)
}

// ExpirationDate is a embedded type containing time.Time to unmarshal
// Date in Expiration
type ExpirationDate struct {
	time.Time
}

// MarshalXML encodes expiration date if it is non-zero and encodes
// empty string otherwise
func (eDate ExpirationDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if eDate.Time.IsZero() {
		return nil
	}
	return e.EncodeElement(eDate.Format(time.RFC3339), startElement)
}

// ExpireDeleteMarker represents value of ExpiredObjectDeleteMarker field in Expiration XML element.
type ExpireDeleteMarker bool

// MarshalXML encodes delete marker boolean into an XML form.
func (b ExpireDeleteMarker) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
	if !b {
		return nil
	}
	type expireDeleteMarkerWrapper ExpireDeleteMarker
	return e.EncodeElement(expireDeleteMarkerWrapper(b), startElement)
}

// IsEnabled returns true if the auto delete-marker expiration is enabled
func (b ExpireDeleteMarker) IsEnabled() bool {
	return bool(b)
}

// Expiration structure - expiration details of lifecycle configuration
type Expiration struct {
	XMLName      xml.Name           `xml:"Expiration,omitempty" json:"-"`
	Date         ExpirationDate     `xml:"Date,omitempty" json:"Date,omitempty"`
	Days         ExpirationDays     `xml:"Days,omitempty" json:"Days,omitempty"`
	DeleteMarker ExpireDeleteMarker `xml:"ExpiredObjectDeleteMarker,omitempty"`
}

// IsDaysNull returns true if days field is null
func (e Expiration) IsDaysNull() bool {
	return e.Days == ExpirationDays(0)
}

// IsDateNull returns true if date field is null
func (e Expiration) IsDateNull() bool {
	return e.Date.Time.IsZero()
}

// IsNull returns true if both date and days fields are null
func (e Expiration) IsNull() bool {
	return e.IsDaysNull() && e.IsDateNull()
}

// MarshalXML is expiration is non null
func (e Expiration) MarshalXML(en *xml.Encoder, startElement xml.StartElement) error {
	if e.IsNull() {
		return nil
	}
	type expirationWrapper Expiration
	return en.EncodeElement(expirationWrapper(e), startElement)
}

// Rule represents a single rule in lifecycle configuration
type Rule struct {
	XMLName                        xml.Name                       `xml:"Rule,omitempty" json:"-"`
	AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty" json:"AbortIncompleteMultipartUpload,omitempty"`
	Expiration                     Expiration                     `xml:"Expiration,omitempty" json:"Expiration,omitempty"`
	ID                             string                         `xml:"ID" json:"ID"`
	RuleFilter                     Filter                         `xml:"Filter,omitempty" json:"Filter,omitempty"`
	NoncurrentVersionExpiration    NoncurrentVersionExpiration    `xml:"NoncurrentVersionExpiration,omitempty"  json:"NoncurrentVersionExpiration,omitempty"`
	NoncurrentVersionTransition    NoncurrentVersionTransition    `xml:"NoncurrentVersionTransition,omitempty" json:"NoncurrentVersionTransition,omitempty"`
	Prefix                         string                         `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
	Status                         string                         `xml:"Status" json:"Status"`
	Transition                     Transition                     `xml:"Transition,omitempty" json:"Transition,omitempty"`
}

// Configuration is a collection of Rule objects.
type Configuration struct {
	XMLName xml.Name `xml:"LifecycleConfiguration,omitempty" json:"-"`
	Rules   []Rule   `xml:"Rule"`
}

// Empty check if lifecycle configuration is empty
func (c *Configuration) Empty() bool {
	if c == nil {
		return true
	}
	return len(c.Rules) == 0
}

// NewConfiguration initializes a fresh lifecycle configuration
// for manipulation, such as setting and removing lifecycle rules
// and filters.
func NewConfiguration() *Configuration {
	return &Configuration{}
}
<|endoftext|>


<commit_before>// +build integration

package proxy

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

	"github.com/hellofresh/janus/pkg/router"
	"github.com/hellofresh/janus/pkg/test"
	"github.com/hellofresh/stats-go"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
)

var tests = []struct {
	description         string
	method              string
	url                 string
	expectedContentType string
	expectedCode        int
}{
	{
		description:         "Get example route",
		method:              "GET",
		url:                 "/example",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	}, {
		description:         "Get invalid route",
		method:              "GET",
		url:                 "/invalid-route",
		expectedContentType: "text/plain; charset=utf-8",
		expectedCode:        http.StatusNotFound,
	},
	{
		description:         "Get one posts - strip path",
		method:              "GET",
		url:                 "/posts/1",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "Get one posts - append path",
		method:              "GET",
		url:                 "/append",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "Get one recipe - parameter interpolation",
		url:                 "/api/recipes/5252b1b5301bbf46038b473f",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "No parameter to interpolate",
		url:                 "/api/recipes/search",
		expectedContentType: "application/json",
		expectedCode:        http.StatusNotFound,
	},
}

func TestSuccessfulProxy(t *testing.T) {
	t.Parallel()

	log.SetOutput(ioutil.Discard)

	ts := test.NewServer(createRegisterAndRouter())
	defer ts.Close()

	for _, tc := range tests {
		t.Run(tc.description, func(t *testing.T) {
			res, err := ts.Do(tc.method, tc.url, make(map[string]string))
			assert.NoError(t, err)
			if res != nil {
				defer res.Body.Close()
			}

			assert.Equal(t, tc.expectedContentType, res.Header.Get("Content-Type"), tc.description)
			assert.Equal(t, tc.expectedCode, res.StatusCode, tc.description)
		})
	}
}

func createProxyDefinitions() []*Definition {
	return []*Definition{
		{
			ListenPath: "/example/*",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/hello-world"}},
			},
			Methods: []string{"ALL"},
		},
		{
			ListenPath: "/posts/*",
			StripPath:  true,
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/posts"}},
			},
			Methods: []string{"ALL"},
		},
		{
			ListenPath: "/append/*",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/hello-world"}},
			},
			AppendPath: true,
			Upstreams:  &Upstreams{},
			Methods:    []string{"GET"},
		},
		{
			ListenPath: "/api/recipes/{id:[\\da-f]{24}}",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/recipes/{id}"}},
			},
			Methods: []string{"GET"},
		},
		{
			ListenPath: "/api/recipes/search",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/recipes/{id}"}},
			},
			Methods: []string{"GET"},
		},
	}
}

func createRegisterAndRouter() router.Router {
	r := router.NewChiRouter()
	createRegister(r)
	return r
}

func createRegister(r router.Router) *Register {
	var routes []*Route

	definitions := createProxyDefinitions()
	for _, def := range definitions {
		routes = append(routes, NewRoute(def))
	}

	statsClient, _ := stats.NewClient("noop://")
	register := NewRegister(r, Params{StatsClient: statsClient})
	register.AddMany(routes)

	return register
}
<commit_msg>Removed duplicated field<commit_after>// +build integration

package proxy

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

	"github.com/hellofresh/janus/pkg/router"
	"github.com/hellofresh/janus/pkg/test"
	"github.com/hellofresh/stats-go"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
)

var tests = []struct {
	description         string
	method              string
	url                 string
	expectedContentType string
	expectedCode        int
}{
	{
		description:         "Get example route",
		method:              "GET",
		url:                 "/example",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	}, {
		description:         "Get invalid route",
		method:              "GET",
		url:                 "/invalid-route",
		expectedContentType: "text/plain; charset=utf-8",
		expectedCode:        http.StatusNotFound,
	},
	{
		description:         "Get one posts - strip path",
		method:              "GET",
		url:                 "/posts/1",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "Get one posts - append path",
		method:              "GET",
		url:                 "/append",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "Get one recipe - parameter interpolation",
		url:                 "/api/recipes/5252b1b5301bbf46038b473f",
		expectedContentType: "application/json; charset=utf-8",
		expectedCode:        http.StatusOK,
	},
	{
		description:         "No parameter to interpolate",
		url:                 "/api/recipes/search",
		expectedContentType: "application/json",
		expectedCode:        http.StatusNotFound,
	},
}

func TestSuccessfulProxy(t *testing.T) {
	t.Parallel()

	log.SetOutput(ioutil.Discard)

	ts := test.NewServer(createRegisterAndRouter())
	defer ts.Close()

	for _, tc := range tests {
		t.Run(tc.description, func(t *testing.T) {
			res, err := ts.Do(tc.method, tc.url, make(map[string]string))
			assert.NoError(t, err)
			if res != nil {
				defer res.Body.Close()
			}

			assert.Equal(t, tc.expectedContentType, res.Header.Get("Content-Type"), tc.description)
			assert.Equal(t, tc.expectedCode, res.StatusCode, tc.description)
		})
	}
}

func createProxyDefinitions() []*Definition {
	return []*Definition{
		{
			ListenPath: "/example/*",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/hello-world"}},
			},
			Methods: []string{"ALL"},
		},
		{
			ListenPath: "/posts/*",
			StripPath:  true,
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/posts"}},
			},
			Methods: []string{"ALL"},
		},
		{
			ListenPath: "/append/*",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/hello-world"}},
			},
			AppendPath: true,
			Methods:    []string{"GET"},
		},
		{
			ListenPath: "/api/recipes/{id:[\\da-f]{24}}",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/recipes/{id}"}},
			},
			Methods: []string{"GET"},
		},
		{
			ListenPath: "/api/recipes/search",
			Upstreams: &Upstreams{
				Balancing: "roundrobin",
				Targets:   []*Target{{Target: "http://localhost:9089/recipes/{id}"}},
			},
			Methods: []string{"GET"},
		},
	}
}

func createRegisterAndRouter() router.Router {
	r := router.NewChiRouter()
	createRegister(r)
	return r
}

func createRegister(r router.Router) *Register {
	var routes []*Route

	definitions := createProxyDefinitions()
	for _, def := range definitions {
		routes = append(routes, NewRoute(def))
	}

	statsClient, _ := stats.NewClient("noop://")
	register := NewRegister(r, Params{StatsClient: statsClient})
	register.AddMany(routes)

	return register
}
<|endoftext|>


<commit_before>package sharings

import (
	"net/http"
	"strings"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/web/jsonapi"
)

// Recipient is a struct describing a sharing recipient
type Recipient struct {
	RID    string `json:"_id,omitempty"`
	RRev   string `json:"_rev,omitempty"`
	Email  string `json:"email"`
	URL    string `json:"url"`
	Client *auth.Client
}

// ID returns the recipient qualified identifier
func (r *Recipient) ID() string { return r.RID }

// Rev returns the recipient revision
func (r *Recipient) Rev() string { return r.RRev }

// DocType returns the recipient document type
func (r *Recipient) DocType() string { return consts.Recipients }

// SetID changes the recipient qualified identifier
func (r *Recipient) SetID(id string) { r.RID = id }

// SetRev changes the recipient revision
func (r *Recipient) SetRev(rev string) { r.RRev = rev }

// Relationships implements jsonapi.Doc
func (r *Recipient) Relationships() jsonapi.RelationshipMap { return nil }

// Included implements jsonapi.Doc
func (r *Recipient) Included() []jsonapi.Object { return nil }

// Links implements jsonapi.Doc
func (r *Recipient) Links() *jsonapi.LinksList {
	return &jsonapi.LinksList{Self: "/recipients/" + r.RID}
}

// ExtractDomain returns the recipient's domain without the scheme
func (r *Recipient) ExtractDomain() (string, error) {
	if r.URL == "" {
		return "", ErrRecipientHasNoURL
	}
	if tokens := strings.Split(r.URL, "://"); len(tokens) > 1 {
		return tokens[1], nil
	}
	return r.URL, nil
}

// GetAccessToken sends an access_token requests to the recipient, using the
// given authorization code
func (r *Recipient) GetAccessToken(code string) (*auth.AccessToken, error) {
	client := new(http.Client)
	rURL, err := r.ExtractDomain()
	if err != nil {
		return nil, err
	}
	req := &auth.Request{
		Domain:     rURL,
		HTTPClient: client,
	}
	return req.GetAccessToken(r.Client, code)
}

// Register creates a OAuth request and register to the Recipient
func (r *Recipient) Register(instance *instance.Instance) error {
	rURL, err := r.ExtractDomain()
	if err != nil {
		return err
	}
	client := new(http.Client)
	req := &auth.Request{
		Domain:     rURL,
		HTTPClient: client,
	}
	clientURI := instance.Scheme() + "://" + instance.Domain
	redirectURI := clientURI + "/sharings/answer"

	// Get the Cozy's public name
	doc := &couchdb.JSONDoc{}
	err = couchdb.GetDoc(instance, consts.Settings, consts.InstanceSettingsID, doc)
	if err != nil {
		return err
	}
	sharerPublicName, _ := doc.M["public_name"].(string)
	if sharerPublicName == "" {
		return ErrPublicNameNotDefined
	}

	authClient := &auth.Client{
		RedirectURIs: []string{redirectURI},
		ClientName:   sharerPublicName,
		ClientKind:   "sharing",
		SoftwareID:   "github.com/cozy/cozy-stack",
		ClientURI:    clientURI,
	}

	resClient, err := req.RegisterClient(authClient)
	if err != nil {
		return err
	}

	r.Client = resClient
	return couchdb.UpdateDoc(instance, r)
}

// CreateRecipient inserts a Recipient document in database
func CreateRecipient(db couchdb.Database, doc *Recipient) error {
	err := couchdb.CreateDoc(db, doc)
	return err
}

var (
	_ couchdb.Doc    = &Recipient{}
	_ jsonapi.Object = &Recipient{}
)
<commit_msg>Use PageURL helper<commit_after>package sharings

import (
	"net/http"
	"strings"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/web/jsonapi"
)

// Recipient is a struct describing a sharing recipient
type Recipient struct {
	RID    string `json:"_id,omitempty"`
	RRev   string `json:"_rev,omitempty"`
	Email  string `json:"email"`
	URL    string `json:"url"`
	Client *auth.Client
}

// ID returns the recipient qualified identifier
func (r *Recipient) ID() string { return r.RID }

// Rev returns the recipient revision
func (r *Recipient) Rev() string { return r.RRev }

// DocType returns the recipient document type
func (r *Recipient) DocType() string { return consts.Recipients }

// SetID changes the recipient qualified identifier
func (r *Recipient) SetID(id string) { r.RID = id }

// SetRev changes the recipient revision
func (r *Recipient) SetRev(rev string) { r.RRev = rev }

// Relationships implements jsonapi.Doc
func (r *Recipient) Relationships() jsonapi.RelationshipMap { return nil }

// Included implements jsonapi.Doc
func (r *Recipient) Included() []jsonapi.Object { return nil }

// Links implements jsonapi.Doc
func (r *Recipient) Links() *jsonapi.LinksList {
	return &jsonapi.LinksList{Self: "/recipients/" + r.RID}
}

// ExtractDomain returns the recipient's domain without the scheme
func (r *Recipient) ExtractDomain() (string, error) {
	if r.URL == "" {
		return "", ErrRecipientHasNoURL
	}
	if tokens := strings.Split(r.URL, "://"); len(tokens) > 1 {
		return tokens[1], nil
	}
	return r.URL, nil
}

// GetAccessToken sends an access_token requests to the recipient, using the
// given authorization code
func (r *Recipient) GetAccessToken(code string) (*auth.AccessToken, error) {
	client := new(http.Client)
	rURL, err := r.ExtractDomain()
	if err != nil {
		return nil, err
	}
	req := &auth.Request{
		Domain:     rURL,
		HTTPClient: client,
	}
	return req.GetAccessToken(r.Client, code)
}

// Register creates a OAuth request and register to the Recipient
func (r *Recipient) Register(instance *instance.Instance) error {
	rURL, err := r.ExtractDomain()
	if err != nil {
		return err
	}
	client := new(http.Client)
	req := &auth.Request{
		Domain:     rURL,
		HTTPClient: client,
	}
	clientURI := instance.PageURL("", nil)
	redirectURI := instance.PageURL("/sharings/answer", nil)

	// Get the Cozy's public name
	doc := &couchdb.JSONDoc{}
	err = couchdb.GetDoc(instance, consts.Settings, consts.InstanceSettingsID, doc)
	if err != nil {
		return err
	}
	sharerPublicName, _ := doc.M["public_name"].(string)
	if sharerPublicName == "" {
		return ErrPublicNameNotDefined
	}

	authClient := &auth.Client{
		RedirectURIs: []string{redirectURI},
		ClientName:   sharerPublicName,
		ClientKind:   "sharing",
		SoftwareID:   "github.com/cozy/cozy-stack",
		ClientURI:    clientURI,
	}

	resClient, err := req.RegisterClient(authClient)
	if err != nil {
		return err
	}

	r.Client = resClient
	return couchdb.UpdateDoc(instance, r)
}

// CreateRecipient inserts a Recipient document in database
func CreateRecipient(db couchdb.Database, doc *Recipient) error {
	err := couchdb.CreateDoc(db, doc)
	return err
}

var (
	_ couchdb.Doc    = &Recipient{}
	_ jsonapi.Object = &Recipient{}
)
<|endoftext|>


<commit_before>/*
Copyright 2011 Google Inc.

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

     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 webserver

import (
	"bufio"
	"crypto/rand"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"strings"
	"time"
)

type HandlerPicker func(req *http.Request) (http.HandlerFunc, bool)

type Server struct {
	premux   []HandlerPicker
	mux      *http.ServeMux
	listener net.Listener

	enableTLS               bool
	tlsCertFile, tlsKeyFile string
}

func New() *Server {
	return &Server{
		premux: make([]HandlerPicker, 0),
		mux:    http.NewServeMux(),
	}
}

func (s *Server) SetTLS(certFile, keyFile string) {
	s.enableTLS = true
	s.tlsCertFile = certFile
	s.tlsKeyFile = keyFile
}

func (s *Server) ListenURL() string {
	scheme := "http"
	if s.enableTLS {
		scheme = "https"
	}
	if s.listener != nil {
		addr := strings.Replace(s.listener.Addr().String(), "[::]", "localhost", 1)
		return scheme + "://" + addr
	}
	return ""
}

// Register conditional handler-picker functions which get run before
// HandleFunc or Handle.  The HandlerPicker should return false if
// it's not interested in a request.
func (s *Server) RegisterPreMux(hp HandlerPicker) {
	s.premux = append(s.premux, hp)
}

func (s *Server) HandleFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
	s.mux.HandleFunc(pattern, fn)
}

func (s *Server) Handle(pattern string, handler http.Handler) {
	s.mux.Handle(pattern, handler)
}

func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	for _, hp := range s.premux {
		handler, ok := hp(req)
		if ok {
			handler(rw, req)
			return
		}
	}
	s.mux.ServeHTTP(rw, req)
}

// Listen starts listening on the given host:port addr.
func (s *Server) Listen(addr string) error {
	if s.listener != nil {
		return nil
	}

	doLog := os.Getenv("TESTING_PORT_WRITE_FD") == "" // Don't make noise during unit tests
	if addr == "" {
		return fmt.Errorf("<host>:<port> needs to be provided to start listening")
	}

	var err error
	s.listener, err = net.Listen("tcp", addr)
	if err != nil {
		return fmt.Errorf("Failed to listen on %s: %v", addr, err)
	}
	base := s.ListenURL()
	if doLog {
		log.Printf("Starting to listen on %s\n", base)
	}

	if s.enableTLS {
		config := &tls.Config{
			Rand:       rand.Reader,
			Time:       time.Now,
			NextProtos: []string{"http/1.1"},
		}
		config.Certificates = make([]tls.Certificate, 1)
		config.Certificates[0], err = tls.LoadX509KeyPair(s.tlsCertFile, s.tlsKeyFile)
		if err != nil {
			return fmt.Errorf("Failed to load TLS cert: %v", err)
		}
		s.listener = tls.NewListener(s.listener, config)
	}

	if doLog && strings.HasSuffix(base, ":0") {
		log.Printf("Now listening on %s\n", s.ListenURL())
	}

	return nil
}

func (s *Server) Serve() {
	if err := s.Listen(""); err != nil {
		log.Fatalf("Listen error: %v", err)
	}
	go runTestHarnessIntegration(s.listener)
	err := http.Serve(s.listener, s)
	if err != nil {
		log.Printf("Error in http server: %v\n", err)
		os.Exit(1)
	}
}

// Signals the test harness that we've started listening.
// TODO: write back the port number that we randomly selected?
// For now just writes back a single byte.
func runTestHarnessIntegration(listener net.Listener) {
	writePipe, err := pipeFromEnvFd("TESTING_PORT_WRITE_FD")
	if err != nil {
		return
	}
	readPipe, _ := pipeFromEnvFd("TESTING_CONTROL_READ_FD")

	if writePipe != nil {
		writePipe.Write([]byte(listener.Addr().String() + "\n"))
	}

	if readPipe != nil {
		bufr := bufio.NewReader(readPipe)
		for {
			line, err := bufr.ReadString('\n')
			if err == io.EOF || line == "EXIT\n" {
				os.Exit(0)
			}
			return
		}
	}
}
<commit_msg>webserver: use runsit listener instead of net.Listen<commit_after>/*
Copyright 2011 Google Inc.

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

     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 webserver

import (
	"bufio"
	"crypto/rand"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"camlistore.org/third_party/github.com/bradfitz/runsit/listen"
)

type HandlerPicker func(req *http.Request) (http.HandlerFunc, bool)

type Server struct {
	premux   []HandlerPicker
	mux      *http.ServeMux
	listener net.Listener

	enableTLS               bool
	tlsCertFile, tlsKeyFile string
}

func New() *Server {
	return &Server{
		premux: make([]HandlerPicker, 0),
		mux:    http.NewServeMux(),
	}
}

func (s *Server) SetTLS(certFile, keyFile string) {
	s.enableTLS = true
	s.tlsCertFile = certFile
	s.tlsKeyFile = keyFile
}

func (s *Server) ListenURL() string {
	scheme := "http"
	if s.enableTLS {
		scheme = "https"
	}
	if s.listener != nil {
		addr := strings.Replace(s.listener.Addr().String(), "[::]", "localhost", 1)
		return scheme + "://" + addr
	}
	return ""
}

// Register conditional handler-picker functions which get run before
// HandleFunc or Handle.  The HandlerPicker should return false if
// it's not interested in a request.
func (s *Server) RegisterPreMux(hp HandlerPicker) {
	s.premux = append(s.premux, hp)
}

func (s *Server) HandleFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
	s.mux.HandleFunc(pattern, fn)
}

func (s *Server) Handle(pattern string, handler http.Handler) {
	s.mux.Handle(pattern, handler)
}

func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	for _, hp := range s.premux {
		handler, ok := hp(req)
		if ok {
			handler(rw, req)
			return
		}
	}
	s.mux.ServeHTTP(rw, req)
}

// Listen starts listening on the given host:port addr.
func (s *Server) Listen(addr string) error {
	if s.listener != nil {
		return nil
	}

	doLog := os.Getenv("TESTING_PORT_WRITE_FD") == "" // Don't make noise during unit tests
	if addr == "" {
		return fmt.Errorf("<host>:<port> needs to be provided to start listening")
	}

	var err error
	s.listener, err = listen.Listen(addr)
	if err != nil {
		return fmt.Errorf("Failed to listen on %s: %v", addr, err)
	}
	base := s.ListenURL()
	if doLog {
		log.Printf("Starting to listen on %s\n", base)
	}

	if s.enableTLS {
		config := &tls.Config{
			Rand:       rand.Reader,
			Time:       time.Now,
			NextProtos: []string{"http/1.1"},
		}
		config.Certificates = make([]tls.Certificate, 1)
		config.Certificates[0], err = tls.LoadX509KeyPair(s.tlsCertFile, s.tlsKeyFile)
		if err != nil {
			return fmt.Errorf("Failed to load TLS cert: %v", err)
		}
		s.listener = tls.NewListener(s.listener, config)
	}

	if doLog && strings.HasSuffix(base, ":0") {
		log.Printf("Now listening on %s\n", s.ListenURL())
	}

	return nil
}

func (s *Server) Serve() {
	if err := s.Listen(""); err != nil {
		log.Fatalf("Listen error: %v", err)
	}
	go runTestHarnessIntegration(s.listener)
	err := http.Serve(s.listener, s)
	if err != nil {
		log.Printf("Error in http server: %v\n", err)
		os.Exit(1)
	}
}

// Signals the test harness that we've started listening.
// TODO: write back the port number that we randomly selected?
// For now just writes back a single byte.
func runTestHarnessIntegration(listener net.Listener) {
	writePipe, err := pipeFromEnvFd("TESTING_PORT_WRITE_FD")
	if err != nil {
		return
	}
	readPipe, _ := pipeFromEnvFd("TESTING_CONTROL_READ_FD")

	if writePipe != nil {
		writePipe.Write([]byte(listener.Addr().String() + "\n"))
	}

	if readPipe != nil {
		bufr := bufio.NewReader(readPipe)
		for {
			line, err := bufr.ReadString('\n')
			if err == io.EOF || line == "EXIT\n" {
				os.Exit(0)
			}
			return
		}
	}
}
<|endoftext|>