<commit_before>// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains the code dealing with package directory trees.

package main

import (
	"bytes"
	"go/doc"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"strings"
	"unicode"
)

type Directory struct {
	Depth int
	Path  string // includes Name
	Name  string
	Text  string       // package documentation, if any
	Dirs  []*Directory // subdirectories
}

func isGoFile(fi os.FileInfo) bool {
	name := fi.Name()
	return !fi.IsDir() &&
		len(name) > 0 && name[0] != '.' && // ignore .files
		filepath.Ext(name) == ".go"
}

func isPkgFile(fi os.FileInfo) bool {
	return isGoFile(fi) &&
		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
}

func isPkgDir(fi os.FileInfo) bool {
	name := fi.Name()
	return fi.IsDir() && len(name) > 0 &&
		name[0] != '_' && name[0] != '.' // ignore _files and .files
}

func firstSentence(s string) string {
	i := -1 // index+1 of first terminator (punctuation ending a sentence)
	j := -1 // index+1 of first terminator followed by white space
	prev := 'A'
	for k, ch := range s {
		k1 := k + 1
		if ch == '.' || ch == '!' || ch == '?' {
			if i < 0 {
				i = k1 // first terminator
			}
			if k1 < len(s) && s[k1] <= ' ' {
				if j < 0 {
					j = k1 // first terminator followed by white space
				}
				if !unicode.IsUpper(prev) {
					j = k1
					break
				}
			}
		}
		prev = ch
	}

	if j < 0 {
		// use the next best terminator
		j = i
		if j < 0 {
			// no terminator at all, use the entire string
			j = len(s)
		}
	}

	return s[0:j]
}

type treeBuilder struct {
	pathFilter func(string) bool
	maxDepth   int
}

func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
	if b.pathFilter != nil && !b.pathFilter(path) {
		return nil
	}

	if depth >= b.maxDepth {
		// return a dummy directory so that the parent directory
		// doesn't get discarded just because we reached the max
		// directory depth
		return &Directory{depth, path, name, "", nil}
	}

	list, err := fs.ReadDir(path)
	if err != nil {
		// newDirTree is called with a path that should be a package
		// directory; errors here should not happen, but if they do,
		// we want to know about them
		log.Printf("ReadDir(%s): %s", path, err)
	}

	// determine number of subdirectories and if there are package files
	ndirs := 0
	hasPkgFiles := false
	var synopses [4]string // prioritized package documentation (0 == highest priority)
	for _, d := range list {
		switch {
		case isPkgDir(d):
			ndirs++
		case isPkgFile(d):
			// looks like a package file, but may just be a file ending in ".go";
			// don't just count it yet (otherwise we may end up with hasPkgFiles even
			// though the directory doesn't contain any real package files - was bug)
			if synopses[0] == "" {
				// no "optimal" package synopsis yet; continue to collect synopses
				file, err := parseFile(fset, filepath.Join(path, d.Name()),
					parser.ParseComments|parser.PackageClauseOnly)
				if err == nil {
					hasPkgFiles = true
					if file.Doc != nil {
						// prioritize documentation
						i := -1
						switch file.Name.Name {
						case name:
							i = 0 // normal case: directory name matches package name
						case fakePkgName:
							i = 1 // synopses for commands
						case "main":
							i = 2 // directory contains a main package
						default:
							i = 3 // none of the above
						}
						if 0 <= i && i < len(synopses) && synopses[i] == "" {
							synopses[i] = doc.Synopsis(file.Doc.Text())
						}
					}
				}
			}
		}
	}

	// create subdirectory tree
	var dirs []*Directory
	if ndirs > 0 {
		dirs = make([]*Directory, ndirs)
		i := 0
		for _, d := range list {
			if isPkgDir(d) {
				name := d.Name()
				dd := b.newDirTree(fset, filepath.Join(path, name), name, depth+1)
				if dd != nil {
					dirs[i] = dd
					i++
				}
			}
		}
		dirs = dirs[0:i]
	}

	// if there are no package files and no subdirectories
	// containing package files, ignore the directory
	if !hasPkgFiles && len(dirs) == 0 {
		return nil
	}

	// select the highest-priority synopsis for the directory entry, if any
	synopsis := ""
	for _, synopsis = range synopses {
		if synopsis != "" {
			break
		}
	}

	return &Directory{depth, path, name, synopsis, dirs}
}

// newDirectory creates a new package directory tree with at most maxDepth
// levels, anchored at root. The result tree is pruned such that it only
// contains directories that contain package files or that contain
// subdirectories containing package files (transitively). If a non-nil
// pathFilter is provided, directory paths additionally must be accepted
// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
// provided for maxDepth, nodes at larger depths are pruned as well; they
// are assumed to contain package files even if their contents are not known
// (i.e., in this case the tree may contain directories w/o any package files).
//
func newDirectory(root string, pathFilter func(string) bool, maxDepth int) *Directory {
	// The root could be a symbolic link so use Stat not Lstat.
	d, err := fs.Stat(root)
	// If we fail here, report detailed error messages; otherwise
	// is is hard to see why a directory tree was not built.
	switch {
	case err != nil:
		log.Printf("newDirectory(%s): %s", root, err)
		return nil
	case !isPkgDir(d):
		log.Printf("newDirectory(%s): not a package directory", root)
		return nil
	}
	if maxDepth < 0 {
		maxDepth = 1e6 // "infinity"
	}
	b := treeBuilder{pathFilter, maxDepth}
	// the file set provided is only for local parsing, no position
	// information escapes and thus we don't need to save the set
	return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
}

func (dir *Directory) writeLeafs(buf *bytes.Buffer) {
	if dir != nil {
		if len(dir.Dirs) == 0 {
			buf.WriteString(dir.Path)
			buf.WriteByte('\n')
			return
		}

		for _, d := range dir.Dirs {
			d.writeLeafs(buf)
		}
	}
}

func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
	if dir != nil {
		if !skipRoot {
			c <- dir
		}
		for _, d := range dir.Dirs {
			d.walk(c, false)
		}
	}
}

func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
	c := make(chan *Directory)
	go func() {
		dir.walk(c, skipRoot)
		close(c)
	}()
	return c
}

func (dir *Directory) lookupLocal(name string) *Directory {
	for _, d := range dir.Dirs {
		if d.Name == name {
			return d
		}
	}
	return nil
}

// lookup looks for the *Directory for a given path, relative to dir.
func (dir *Directory) lookup(path string) *Directory {
	d := strings.Split(dir.Path, string(filepath.Separator))
	p := strings.Split(path, string(filepath.Separator))
	i := 0
	for i < len(d) {
		if i >= len(p) || d[i] != p[i] {
			return nil
		}
		i++
	}
	for dir != nil && i < len(p) {
		dir = dir.lookupLocal(p[i])
		i++
	}
	return dir
}

// DirEntry describes a directory entry. The Depth and Height values
// are useful for presenting an entry in an indented fashion.
//
type DirEntry struct {
	Depth    int    // >= 0
	Height   int    // = DirList.MaxHeight - Depth, > 0
	Path     string // includes Name, relative to DirList root
	Name     string
	Synopsis string
}

type DirList struct {
	MaxHeight int // directory tree height, > 0
	List      []DirEntry
}

// listing creates a (linear) directory listing from a directory tree.
// If skipRoot is set, the root directory itself is excluded from the list.
//
func (root *Directory) listing(skipRoot bool) *DirList {
	if root == nil {
		return nil
	}

	// determine number of entries n and maximum height
	n := 0
	minDepth := 1 << 30 // infinity
	maxDepth := 0
	for d := range root.iter(skipRoot) {
		n++
		if minDepth > d.Depth {
			minDepth = d.Depth
		}
		if maxDepth < d.Depth {
			maxDepth = d.Depth
		}
	}
	maxHeight := maxDepth - minDepth + 1

	if n == 0 {
		return nil
	}

	// create list
	list := make([]DirEntry, n)
	i := 0
	for d := range root.iter(skipRoot) {
		p := &list[i]
		p.Depth = d.Depth - minDepth
		p.Height = maxHeight - p.Depth
		// the path is relative to root.Path - remove the root.Path
		// prefix (the prefix should always be present but avoid
		// crashes and check)
		path := d.Path
		if strings.HasPrefix(d.Path, root.Path) {
			path = d.Path[len(root.Path):]
		}
		// remove trailing separator if any - path must be relative
		if len(path) > 0 && path[0] == filepath.Separator {
			path = path[1:]
		}
		p.Path = path
		p.Name = d.Name
		p.Synopsis = d.Text
		i++
	}

	return &DirList{maxHeight, list}
}
<commit_msg>godoc: remove dead code<commit_after>// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains the code dealing with package directory trees.

package main

import (
	"bytes"
	"go/doc"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"strings"
)

type Directory struct {
	Depth int
	Path  string // includes Name
	Name  string
	Text  string       // package documentation, if any
	Dirs  []*Directory // subdirectories
}

func isGoFile(fi os.FileInfo) bool {
	name := fi.Name()
	return !fi.IsDir() &&
		len(name) > 0 && name[0] != '.' && // ignore .files
		filepath.Ext(name) == ".go"
}

func isPkgFile(fi os.FileInfo) bool {
	return isGoFile(fi) &&
		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
}

func isPkgDir(fi os.FileInfo) bool {
	name := fi.Name()
	return fi.IsDir() && len(name) > 0 &&
		name[0] != '_' && name[0] != '.' // ignore _files and .files
}

type treeBuilder struct {
	pathFilter func(string) bool
	maxDepth   int
}

func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
	if b.pathFilter != nil && !b.pathFilter(path) {
		return nil
	}

	if depth >= b.maxDepth {
		// return a dummy directory so that the parent directory
		// doesn't get discarded just because we reached the max
		// directory depth
		return &Directory{depth, path, name, "", nil}
	}

	list, err := fs.ReadDir(path)
	if err != nil {
		// newDirTree is called with a path that should be a package
		// directory; errors here should not happen, but if they do,
		// we want to know about them
		log.Printf("ReadDir(%s): %s", path, err)
	}

	// determine number of subdirectories and if there are package files
	ndirs := 0
	hasPkgFiles := false
	var synopses [4]string // prioritized package documentation (0 == highest priority)
	for _, d := range list {
		switch {
		case isPkgDir(d):
			ndirs++
		case isPkgFile(d):
			// looks like a package file, but may just be a file ending in ".go";
			// don't just count it yet (otherwise we may end up with hasPkgFiles even
			// though the directory doesn't contain any real package files - was bug)
			if synopses[0] == "" {
				// no "optimal" package synopsis yet; continue to collect synopses
				file, err := parseFile(fset, filepath.Join(path, d.Name()),
					parser.ParseComments|parser.PackageClauseOnly)
				if err == nil {
					hasPkgFiles = true
					if file.Doc != nil {
						// prioritize documentation
						i := -1
						switch file.Name.Name {
						case name:
							i = 0 // normal case: directory name matches package name
						case fakePkgName:
							i = 1 // synopses for commands
						case "main":
							i = 2 // directory contains a main package
						default:
							i = 3 // none of the above
						}
						if 0 <= i && i < len(synopses) && synopses[i] == "" {
							synopses[i] = doc.Synopsis(file.Doc.Text())
						}
					}
				}
			}
		}
	}

	// create subdirectory tree
	var dirs []*Directory
	if ndirs > 0 {
		dirs = make([]*Directory, ndirs)
		i := 0
		for _, d := range list {
			if isPkgDir(d) {
				name := d.Name()
				dd := b.newDirTree(fset, filepath.Join(path, name), name, depth+1)
				if dd != nil {
					dirs[i] = dd
					i++
				}
			}
		}
		dirs = dirs[0:i]
	}

	// if there are no package files and no subdirectories
	// containing package files, ignore the directory
	if !hasPkgFiles && len(dirs) == 0 {
		return nil
	}

	// select the highest-priority synopsis for the directory entry, if any
	synopsis := ""
	for _, synopsis = range synopses {
		if synopsis != "" {
			break
		}
	}

	return &Directory{depth, path, name, synopsis, dirs}
}

// newDirectory creates a new package directory tree with at most maxDepth
// levels, anchored at root. The result tree is pruned such that it only
// contains directories that contain package files or that contain
// subdirectories containing package files (transitively). If a non-nil
// pathFilter is provided, directory paths additionally must be accepted
// by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
// provided for maxDepth, nodes at larger depths are pruned as well; they
// are assumed to contain package files even if their contents are not known
// (i.e., in this case the tree may contain directories w/o any package files).
//
func newDirectory(root string, pathFilter func(string) bool, maxDepth int) *Directory {
	// The root could be a symbolic link so use Stat not Lstat.
	d, err := fs.Stat(root)
	// If we fail here, report detailed error messages; otherwise
	// is is hard to see why a directory tree was not built.
	switch {
	case err != nil:
		log.Printf("newDirectory(%s): %s", root, err)
		return nil
	case !isPkgDir(d):
		log.Printf("newDirectory(%s): not a package directory", root)
		return nil
	}
	if maxDepth < 0 {
		maxDepth = 1e6 // "infinity"
	}
	b := treeBuilder{pathFilter, maxDepth}
	// the file set provided is only for local parsing, no position
	// information escapes and thus we don't need to save the set
	return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
}

func (dir *Directory) writeLeafs(buf *bytes.Buffer) {
	if dir != nil {
		if len(dir.Dirs) == 0 {
			buf.WriteString(dir.Path)
			buf.WriteByte('\n')
			return
		}

		for _, d := range dir.Dirs {
			d.writeLeafs(buf)
		}
	}
}

func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
	if dir != nil {
		if !skipRoot {
			c <- dir
		}
		for _, d := range dir.Dirs {
			d.walk(c, false)
		}
	}
}

func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
	c := make(chan *Directory)
	go func() {
		dir.walk(c, skipRoot)
		close(c)
	}()
	return c
}

func (dir *Directory) lookupLocal(name string) *Directory {
	for _, d := range dir.Dirs {
		if d.Name == name {
			return d
		}
	}
	return nil
}

// lookup looks for the *Directory for a given path, relative to dir.
func (dir *Directory) lookup(path string) *Directory {
	d := strings.Split(dir.Path, string(filepath.Separator))
	p := strings.Split(path, string(filepath.Separator))
	i := 0
	for i < len(d) {
		if i >= len(p) || d[i] != p[i] {
			return nil
		}
		i++
	}
	for dir != nil && i < len(p) {
		dir = dir.lookupLocal(p[i])
		i++
	}
	return dir
}

// DirEntry describes a directory entry. The Depth and Height values
// are useful for presenting an entry in an indented fashion.
//
type DirEntry struct {
	Depth    int    // >= 0
	Height   int    // = DirList.MaxHeight - Depth, > 0
	Path     string // includes Name, relative to DirList root
	Name     string
	Synopsis string
}

type DirList struct {
	MaxHeight int // directory tree height, > 0
	List      []DirEntry
}

// listing creates a (linear) directory listing from a directory tree.
// If skipRoot is set, the root directory itself is excluded from the list.
//
func (root *Directory) listing(skipRoot bool) *DirList {
	if root == nil {
		return nil
	}

	// determine number of entries n and maximum height
	n := 0
	minDepth := 1 << 30 // infinity
	maxDepth := 0
	for d := range root.iter(skipRoot) {
		n++
		if minDepth > d.Depth {
			minDepth = d.Depth
		}
		if maxDepth < d.Depth {
			maxDepth = d.Depth
		}
	}
	maxHeight := maxDepth - minDepth + 1

	if n == 0 {
		return nil
	}

	// create list
	list := make([]DirEntry, n)
	i := 0
	for d := range root.iter(skipRoot) {
		p := &list[i]
		p.Depth = d.Depth - minDepth
		p.Height = maxHeight - p.Depth
		// the path is relative to root.Path - remove the root.Path
		// prefix (the prefix should always be present but avoid
		// crashes and check)
		path := d.Path
		if strings.HasPrefix(d.Path, root.Path) {
			path = d.Path[len(root.Path):]
		}
		// remove trailing separator if any - path must be relative
		if len(path) > 0 && path[0] == filepath.Separator {
			path = path[1:]
		}
		p.Path = path
		p.Name = d.Name
		p.Synopsis = d.Text
		i++
	}

	return &DirList{maxHeight, list}
}
<|endoftext|>


<commit_before>package close

import (
	"net/http"

	"golang.org/x/net/context"

	"github.com/AlexanderChen1989/xrest"
)

// Closer client connection close plug
type Closer struct {
	next xrest.Handler
}

// New create closer plug
func New() *Closer {
	return &Closer{}
}

// Plug implements xrest.Plugger interface
func (c *Closer) Plug(h xrest.Handler) xrest.Handler {
	c.next = h
	return c
}

// ServeHTTP implements xrest.Handler interface
func (c *Closer) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	// Cancel the context if the client closes the connection
	if cn, ok := w.(http.CloseNotifier); ok {
		var cancel context.CancelFunc
		ctx, cancel = context.WithCancel(ctx)
		defer cancel()
		go func() {
			<-cn.CloseNotify()
			cancel()
		}()
	}

	c.next.ServeHTTP(ctx, w, r)
}
<commit_msg>unexpose closer<commit_after>package close

import (
	"net/http"

	"golang.org/x/net/context"

	"github.com/AlexanderChen1989/xrest"
)

// Closer client connection close plug
type closer struct {
	next xrest.Handler
}

// New create closer plug
func New() xrest.Plugger {
	return &closer{}
}

// Plug implements xrest.Plugger interface
func (c *closer) Plug(h xrest.Handler) xrest.Handler {
	c.next = h
	return c
}

// ServeHTTP implements xrest.Handler interface
func (c *closer) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	// Cancel the context if the client closes the connection
	if cn, ok := w.(http.CloseNotifier); ok {
		var cancel context.CancelFunc
		ctx, cancel = context.WithCancel(ctx)
		defer cancel()
		go func() {
			<-cn.CloseNotify()
			cancel()
		}()
	}

	c.next.ServeHTTP(ctx, w, r)
}
<|endoftext|>


<commit_before>package recorder

import (
	"os"
	"log"

	"github.com/dnaeon/vcr/cassette"
)

// Recorder states
const (
	ModeRecording = iota
	ModeReplaying
)

type Recorder struct {
	// Operating mode of the recorder
	mode int

	// HTTP server used to mock requests
	server *httptest.Server

	// Proxy function that can be used by client transports
	proxyFunc    func(*http.Request) (*url.URL, error)

	// Default transport that can be used by clients to inject
	transport *http.Transport

	// Cassette used by the recorder
	cassette cassette.Cassette
}

// Creates a new recorder
func NewRecorder(cassetteName string) *Recorder {
	c := cassette.NewCassette(cassetteName)
	if os.Stat(cassetteName); os.IsNotExist(err) {
		mode := ModeRecording
	} else {
		mode := ModeReplaying
	}

	// Handler for client requests
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Pass cassette to handler for recording and replaying interactions
		interaction, err := proxyRequestHandler(r, c)
		if err != nil {
			log.Fatal(err)
		}

		w.WriteHeader(interaction.Response.Code)
		fmt.Fprintln(w, interaction.Response.Body)
	})

	// HTTP server used to mock requests
	server := httptest.NewUnstartedServer(handler)

	// A proxy function which routes all requests through our HTTP server
	// Can be used by clients to inject into their own transports
	proxyFunc := func(*http.Request) (*url.URL, error) {
		return url.Parse(server.URL)
	}

	// A transport which can be used by clients to inject
	transport := &http.Transport{
		Proxy: proxyFunc,
	}

	r := &Recorder{
		mode:      mode,
		server:    server,
		proxyFunc: proxyFunc,
		transport: transport,
		cassette:  c,
	}

	return r
}
<commit_msg>Implement Start() and Stop() methods for Recorder type<commit_after>package recorder

import (
	"os"
	"log"

	"github.com/dnaeon/vcr/cassette"
)

// Recorder states
const (
	ModeRecording = iota
	ModeReplaying
)

type Recorder struct {
	// Operating mode of the recorder
	mode int

	// HTTP server used to mock requests
	server *httptest.Server

	// Proxy function that can be used by client transports
	proxyFunc    func(*http.Request) (*url.URL, error)

	// Default transport that can be used by clients to inject
	transport *http.Transport

	// Cassette used by the recorder
	cassette cassette.Cassette
}

// Creates a new recorder
func NewRecorder(cassetteName string) *Recorder {
	c := cassette.NewCassette(cassetteName)
	if os.Stat(cassetteName); os.IsNotExist(err) {
		mode := ModeRecording
	} else {
		mode := ModeReplaying
	}

	// Handler for client requests
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Pass cassette to handler for recording and replaying interactions
		interaction, err := proxyRequestHandler(r, c)
		if err != nil {
			log.Fatal(err)
		}

		w.WriteHeader(interaction.Response.Code)
		fmt.Fprintln(w, interaction.Response.Body)
	})

	// HTTP server used to mock requests
	server := httptest.NewUnstartedServer(handler)

	// A proxy function which routes all requests through our HTTP server
	// Can be used by clients to inject into their own transports
	proxyFunc := func(*http.Request) (*url.URL, error) {
		return url.Parse(server.URL)
	}

	// A transport which can be used by clients to inject
	transport := &http.Transport{
		Proxy: proxyFunc,
	}

	r := &Recorder{
		mode:      mode,
		server:    server,
		proxyFunc: proxyFunc,
		transport: transport,
		cassette:  c,
	}

	return r
}

func (r *Recorder) Start() error {
	// Load cassette data if in replay mode
	if r.mode == ModeReplaying {
		if err := r.cassette.Load(); err != nil {
			return err
		}
	}

	// Start HTTP server to mock request
	r.server.Start()

	return nil
}

func (r *Recorder) Stop() error {
	r.server.Close()

	// Save cassette if in recording mode
	if r.mode == ModeRecording {
		if err := r.cassette.Save(); err != nil {
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>/*
Copyright 2017 caicloud authors. All rights reserved.

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 api

import "time"

// Project represents a group to manage a set of related applications. It maybe a real project, which contains several or many applications.
type Project struct {
	ID          string    `bson:"_id,omitempty" json:"id,omitempty" description:"id of the project"`
	Name        string    `bson:"name,omitempty" json:"name,omitempty" description:"name of the project, should be unique"`
	Description string    `bson:"description,omitempty" json:"description,omitempty" description:"description of the project"`
	Owner       string    `bson:"owner,omitempty" json:"owner,omitempty" description:"owner of the project"`
	CreatedTime time.Time `bson:"createdTime,omitempty" json:"createdTime,omitempty" description:"created time of the project"`
	UpdatedTime time.Time `bson:"updatedTime,omitempty" json:"updatedTime,omitempty" description:"updated time of the project"`
}

// Pipeline represents a set of configs to describe the workflow of CI/CD.
type Pipeline struct {
	ID          string `bson:"_id,omitempty" json:"id,omitempty" description:"id of the pipeline"`
	Name        string `bson:"name,omitempty" json:"name,omitempty" description:"name of the pipeline，unique in one project"`
	Description string `bson:"description,omitempty" json:"description,omitempty" description:"description of the pipeline"`
	Owner       string `bson:"owner,omitempty" json:"owner,omitempty" description:"owner of the pipeline"`
	ProjectID   string `bson:"projectId,omitempty" json:"projectId,omitempty" description:"id of the project which the pipeline belongs to"`
	// TODO （robin）Remove the association between the pipeline and the service after pipeline replaces service.
	ServiceID   string       `bson:"serviceId,omitempty" json:"serviceId,omitempty" description:"id of the service which the pipeline is related to"`
	Build       *Build       `bson:"build,omitempty" json:"build,omitempty" description:"build spec of the pipeline"`
	AutoTrigger *AutoTrigger `bson:"autoTrigger,omitempty" json:"autoTrigger,omitempty" description:"auto trigger strategy of the pipeline"`
	CreatedTime time.Time    `bson:"createdTime,omitempty" json:"createdTime,omitempty" description:"created time of the pipeline"`
	UpdatedTime time.Time    `bson:"updatedTime,omitempty" json:"updatedTime,omitempty" description:"updated time of the pipeline"`
}

// Build represents the build config and stages of CI.
type Build struct {
	BuilderImage *BuilderImage `bson:"builderImage,omitempty" json:"builderImage,omitempty" description:"image information of the builder"`
	Stages       *BuildStages  `bson:"stages,omitempty" json:"stages,omitempty" description:"stages of CI"`
}

// EnvVar represents the environment variables with name and value.
type EnvVar struct {
	Name  string `bson:"name,omitempty" json:"name,omitempty" description:"name of the environment variable"`
	Value string `bson:"value,omitempty" json:"value,omitempty" description:"value of the environment variable"`
}

// BuilderImage represents the image information of the builder.
type BuilderImage struct {
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"image name of the builder"`
	EnvVars []EnvVar `bson:"envVars,omitempty" json:"envVars,omitempty" description:"environment variables of the builder"`
}

// BuildStages represents the build stages of CI.
type BuildStages struct {
	CodeCheckout    *CodeCheckoutStage    `bson:"codeCheckout,omitempty" json:"codeCheckout,omitempty" description:"code checkout stage"`
	UnitTest        *UnitTestStage        `bson:"unitTest,omitempty" json:"unitTest,omitempty" description:"unit test stage"`
	CodeScan        *CodeScanStage        `bson:"codeScan,omitempty" json:"codeScan,omitempty" description:"code scan stage"`
	Package         *PackageStage         `bson:"package,omitempty" json:"package,omitempty" description:"package stage"`
	ImageBuild      *ImageBuildStage      `bson:"imageBuild,omitempty" json:"imageBuild,omitempty" description:"image build stage"`
	IntegrationTest *IntegrationTestStage `bson:"IntegrationTest,omitempty" json:"IntegrationTest,omitempty" description:"integration test stage"`
	ImageRelease    *ImageReleaseStage    `bson:"imageRelease,omitempty" json:"imageRelease,omitempty" description:"image release stage"`
}

// GeneralStage represents the basic config shared by all stages.
type GeneralStage struct {
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to run for this stage"`
}

// CodeCheckoutStage represents the config of code checkout stage.
type CodeCheckoutStage struct {
	CodeSources []*CodeSource `bson:"codeSources,omitempty" json:"codeSources,omitempty" description:"list of code sources to be checked out"`
}

// CodeSourceType represents the type of code source, supports gitlab, github and svn.
type CodeSourceType string

const (
	GitLab CodeSourceType = "gitlab"
	GitHub                = "github"
	SVN                   = "svn"
)

// CodeSource represents the config of code source, only one type is supported.
type CodeSource struct {
	Type CodeSourceType `bson:"type,omitempty" json:"type,omitempty" description:"type of code source, support gitlab, github and svn"`
	// Whether is the main repo. Only support webhook and tag for main repo.
	Main   bool       `bson:"main,omitempty" json:"main,omitempty" description:"whether is the main repo"`
	GitLab *GitSource `bson:"gitLab,omitempty" json:"gitLab,omitempty" description:"code from gitlab"`
	GitHub *GitSource `bson:"gitHub,omitempty" json:"gitHub,omitempty" description:"code from github"`
}

// GitSource represents the config to get code from git.
type GitSource struct {
	Url      string `bson:"url,omitempty" json:"url,omitempty" description:"url of git repo"`
	Ref      string `bson:"ref,omitempty" json:"ref,omitempty" description:"reference of git repo, support branch, tag"`
	Username string `bson:"username,omitempty" json:"useranme,omitempty" description:"username of git"`
	Password string `bson:"password,omitempty" json:"password,omitempty" description:"password of git"`
}

// UnitTestStage represents the config of unit test stage.
type UnitTestStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// CodeScanStage represents the config of code scan stage.
type CodeScanStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// PackageStage represents the config of package stage.
type PackageStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// ImageBuildStage represents the config of image build stage.
type ImageBuildStage struct {
	BuildInfos []*ImageBuildInfo `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// ImageBuildInfo represents the config to build the image.
type ImageBuildInfo struct {
	ContextDir     string `bson:"contextDir,omitempty" json:"contextDir,omitempty" description:"context directory for image build"`
	Dockerfile     string `bson:"dockerfile,omitempty" json:"dockerfile,omitempty" description:"dockerfile content for image build"`
	DockerfilePath string `bson:"dockerfilePath,omitempty" json:"dockerfilePath,omitempty" description:"dockerfile for image build"`
	ImageName      string `bson:"imageName,omitempty" json:"imageName,omitempty" description:"name of the built image"`
}

// IntegrationTestStage represents the config of integration test stage.
type IntegrationTestStage struct {
	IntegrationTestSet *IntegrationTestSet `bson:"integrationTestSet,omitempty" json:"integrationTestSet,omitempty" description:"integration test config"`
	Services           []Service           `bson:"services,omitempty" json:"services,omitempty" description:"list of dependent services for integration test"`
}

// IntegrationTestSet represents the config for integration test set.
type IntegrationTestSet struct {
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"built image to run the integration test"`
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to run for integration test"`
}

// Service represents the dependent service needed for integration test.
type Service struct {
	Name    string   `bson:"name,omitempty" json:"name,omitempty" description:"name of the service"`
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"image name of the service"`
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to start the service"`
	EnvVars []EnvVar `bson:"envVars,omitempty" json:"envVars,omitempty" description:"environment variables of the service"`
}

// ImageReleaseStage represents the config of image release stage.
type ImageReleaseStage struct {
	ReleasePolicy []ImageReleasePolicy `bson:"releasePolicy,omitempty" json:"releasePolicy,omitempty" description:"list of policies for image release"`
}

// ImageReleasePolicyType represents the type of image release policy.
type ImageReleasePolicyType string

const (
	// AlwaysRelease always releases the images.
	AlwaysRelease ImageReleasePolicyType = "Always"

	// NeverRelease never releases the images.
	NeverRelease ImageReleasePolicyType = "Never"

	// IntegrationTestSuccessRelease releases the images only when the integration test success.
	IntegrationTestSuccessRelease ImageReleasePolicyType = "IntegrationTestSuccess"
)

// ImageReleasePolicy represents the policy to release image.
type ImageReleasePolicy struct {
	Image string                 `bson:"image,omitempty" json:"image,omitempty" description:"image to be released"`
	Type  ImageReleasePolicyType `bson:"type,omitempty" json:"type,omitempty" description:"type of image release policy"`
}

// AutoTrigger represents the auto trigger strategy of the pipeline.
type AutoTrigger struct {
	SCMTrigger   *SCMTrigger   `bson:"scmTrigger,omitempty" json:"scmTrigger,omitempty" description:"SCM trigger strategy"`
	TimerTrigger *TimerTrigger `bson:"timerTrigger,omitempty" json:"timerTrigger,omitempty" description:"timer trigger strategy"`
}

// SCMTrigger represents the auto trigger strategy from SCM.
type SCMTrigger struct {
	CommitTrigger *CommitTrigger `bson:"commitTrigger,omitempty" json:"commitTrigger,omitempty" description:"commit trigger strategy"`
}

// GeneralTrigger represents the general config for all auto trigger strategies.
type GeneralTrigger struct {
	FinalStage string `bson:"finalStage,omitempty" json:"finalStage,omitempty" description:"final stage of the auto triggered running"`
}

// CommitTrigger represents the trigger from SCM commit.
type CommitTrigger struct {
	GeneralTrigger
}

// CommitWithCommentsTrigger represents the trigger from SCM commit with specified comments.
type CommitWithCommentsTrigger struct {
	GeneralTrigger
	Comment []string `bson:"comment,omitempty" json:"comment,omitempty" description:"list of comments in commit"`
}

// TimerTrigger represents the auto trigger strategy from timer.
type TimerTrigger struct {
	CronTrigger []*CronTrigger `bson:"cronTrigger,omitempty" json:"cronTrigger,omitempty" description:"list of cron trigger strategies"`
}

// CronTrigger represents the trigger from cron job.
type CronTrigger struct {
	GeneralTrigger
	Expression string `bson:"expression,omitempty" json:"expression,omitempty" description:"expression of cron job"`
}

// PipelineRecord represents the running record of pipeline.
type PipelineRecord struct {
	ID          string       `bson:"_id,omitempty" json:"id,omitempty" description:"id of the pipeline record"`
	PipelineID  string       `bson:"pipelineID,omitempty" json:"pipelineID,omitempty" description:"id of the related pipeline which the pipeline record belongs to"`
	VersionID   string       `bson:"versionID,omitempty" json:"versionID,omitempty" description:"id of the version which the pipeline record is related to"`
	Trigger     string       `bson:"trigger,omitempty" json:"trigger,omitempty" description:"trigger of the pipeline record"`
	StageStatus *StageStatus `bson:"stageStatus,omitempty" json:"stageStatus,omitempty" description:"status of each pipeline stage"`
	Status      Status       `bson:"status,omitempty" json:"status,omitempty" description:"status of the pipeline record"`
	StartTime   time.Time    `bson:"startTime,omitempty" json:"startTime,omitempty" description:"start time of the pipeline record"`
	EndTime     time.Time    `bson:"endTime,omitempty" json:"endTime,omitempty" description:"end time of the pipeline record"`
}

// Status can be the status of some pipeline record or some stage
type Status string

const (
	// Pending represents the status that is triggered but still not running.
	Pending Status = "Pending"
	// Running represents the status that is running.
	Running Status = "Running"
	// Success represents the status that finished and succeeded.
	Success Status = "Success"
	// Failure represents the status that finished but failed.
	Failure Status = "Failed"
	// Abort represents the status that the stage was aborted by some reason, and we can get the reason from the log.
	Abort Status = "Aborted"
)

// TODO The status of every stage may be different.
// StageStatus represents the collections of status for all stages.
type StageStatus struct {
	CodeCheckout    *GeneralStageStatus `bson:"codeCheckout,omitempty" json:"codeCheckout,omitempty" description:"status of code checkout stage"`
	UnitTest        *GeneralStageStatus `bson:"unitTest,omitempty" json:"unitTest,omitempty" description:"status of unit test stage"`
	CodeScan        *GeneralStageStatus `bson:"codeScan,omitempty" json:"codeScan,omitempty" description:"status of code scan stage"`
	Package         *GeneralStageStatus `bson:"package,omitempty" json:"package,omitempty" description:"status of package stage"`
	ImageBuild      *GeneralStageStatus `bson:"imageBuild,omitempty" json:"imageBuild,omitempty" description:"status of image build stage"`
	IntegrationTest *GeneralStageStatus `bson:"integrationTest,omitempty" json:"integrationTest,omitempty" description:"status of integration test stage"`
	ImageRelease    *GeneralStageStatus `bson:"imageRelease,omitempty" json:"imageRelease,omitempty" description:"status of image release stage"`
}

// GeneralStageStatus represents the information of stage.
type GeneralStageStatus struct {
	Status    Status    `bson:"status,omitempty" json:"status,omitempty" description:"status of the stage"`
	StartTime time.Time `bson:"startTime,omitempty" json:"startTime,omitempty" description:"start time of the stage"`
	EndTime   time.Time `bson:"endTime,omitempty" json:"endTime,omitempty" description:"end time of the stage"`
}
<commit_msg>fix typos for apis<commit_after>/*
Copyright 2017 caicloud authors. All rights reserved.

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 api

import "time"

// Project represents a group to manage a set of related applications. It maybe a real project, which contains several or many applications.
type Project struct {
	ID          string    `bson:"_id,omitempty" json:"id,omitempty" description:"id of the project"`
	Name        string    `bson:"name,omitempty" json:"name,omitempty" description:"name of the project, should be unique"`
	Description string    `bson:"description,omitempty" json:"description,omitempty" description:"description of the project"`
	Owner       string    `bson:"owner,omitempty" json:"owner,omitempty" description:"owner of the project"`
	CreatedTime time.Time `bson:"createdTime,omitempty" json:"createdTime,omitempty" description:"created time of the project"`
	UpdatedTime time.Time `bson:"updatedTime,omitempty" json:"updatedTime,omitempty" description:"updated time of the project"`
}

// Pipeline represents a set of configs to describe the workflow of CI/CD.
type Pipeline struct {
	ID          string `bson:"_id,omitempty" json:"id,omitempty" description:"id of the pipeline"`
	Name        string `bson:"name,omitempty" json:"name,omitempty" description:"name of the pipeline，unique in one project"`
	Description string `bson:"description,omitempty" json:"description,omitempty" description:"description of the pipeline"`
	Owner       string `bson:"owner,omitempty" json:"owner,omitempty" description:"owner of the pipeline"`
	ProjectID   string `bson:"projectId,omitempty" json:"projectId,omitempty" description:"id of the project which the pipeline belongs to"`
	// TODO （robin）Remove the association between the pipeline and the service after pipeline replaces service.
	ServiceID   string       `bson:"serviceId,omitempty" json:"serviceId,omitempty" description:"id of the service which the pipeline is related to"`
	Build       *Build       `bson:"build,omitempty" json:"build,omitempty" description:"build spec of the pipeline"`
	AutoTrigger *AutoTrigger `bson:"autoTrigger,omitempty" json:"autoTrigger,omitempty" description:"auto trigger strategy of the pipeline"`
	CreatedTime time.Time    `bson:"createdTime,omitempty" json:"createdTime,omitempty" description:"created time of the pipeline"`
	UpdatedTime time.Time    `bson:"updatedTime,omitempty" json:"updatedTime,omitempty" description:"updated time of the pipeline"`
}

// Build represents the build config and stages of CI.
type Build struct {
	BuilderImage *BuilderImage `bson:"builderImage,omitempty" json:"builderImage,omitempty" description:"image information of the builder"`
	Stages       *BuildStages  `bson:"stages,omitempty" json:"stages,omitempty" description:"stages of CI"`
}

// EnvVar represents the environment variables with name and value.
type EnvVar struct {
	Name  string `bson:"name,omitempty" json:"name,omitempty" description:"name of the environment variable"`
	Value string `bson:"value,omitempty" json:"value,omitempty" description:"value of the environment variable"`
}

// BuilderImage represents the image information of the builder.
type BuilderImage struct {
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"image name of the builder"`
	EnvVars []EnvVar `bson:"envVars,omitempty" json:"envVars,omitempty" description:"environment variables of the builder"`
}

// BuildStages represents the build stages of CI.
type BuildStages struct {
	CodeCheckout    *CodeCheckoutStage    `bson:"codeCheckout,omitempty" json:"codeCheckout,omitempty" description:"code checkout stage"`
	UnitTest        *UnitTestStage        `bson:"unitTest,omitempty" json:"unitTest,omitempty" description:"unit test stage"`
	CodeScan        *CodeScanStage        `bson:"codeScan,omitempty" json:"codeScan,omitempty" description:"code scan stage"`
	Package         *PackageStage         `bson:"package,omitempty" json:"package,omitempty" description:"package stage"`
	ImageBuild      *ImageBuildStage      `bson:"imageBuild,omitempty" json:"imageBuild,omitempty" description:"image build stage"`
	IntegrationTest *IntegrationTestStage `bson:"integrationTest,omitempty" json:"integrationTest,omitempty" description:"integration test stage"`
	ImageRelease    *ImageReleaseStage    `bson:"imageRelease,omitempty" json:"imageRelease,omitempty" description:"image release stage"`
}

// GeneralStage represents the basic config shared by all stages.
type GeneralStage struct {
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to run for this stage"`
}

// CodeCheckoutStage represents the config of code checkout stage.
type CodeCheckoutStage struct {
	CodeSources []*CodeSource `bson:"codeSources,omitempty" json:"codeSources,omitempty" description:"list of code sources to be checked out"`
}

// CodeSourceType represents the type of code source, supports gitlab, github and svn.
type CodeSourceType string

const (
	GitLab CodeSourceType = "gitlab"
	GitHub                = "github"
	SVN                   = "svn"
)

// CodeSource represents the config of code source, only one type is supported.
type CodeSource struct {
	Type CodeSourceType `bson:"type,omitempty" json:"type,omitempty" description:"type of code source, support gitlab, github and svn"`
	// Whether is the main repo. Only support webhook and tag for main repo.
	Main   bool       `bson:"main,omitempty" json:"main,omitempty" description:"whether is the main repo"`
	GitLab *GitSource `bson:"gitLab,omitempty" json:"gitLab,omitempty" description:"code from gitlab"`
	GitHub *GitSource `bson:"gitHub,omitempty" json:"gitHub,omitempty" description:"code from github"`
}

// GitSource represents the config to get code from git.
type GitSource struct {
	Url      string `bson:"url,omitempty" json:"url,omitempty" description:"url of git repo"`
	Ref      string `bson:"ref,omitempty" json:"ref,omitempty" description:"reference of git repo, support branch, tag"`
	Username string `bson:"username,omitempty" json:"username,omitempty" description:"username of git"`
	Password string `bson:"password,omitempty" json:"password,omitempty" description:"password of git"`
}

// UnitTestStage represents the config of unit test stage.
type UnitTestStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// CodeScanStage represents the config of code scan stage.
type CodeScanStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// PackageStage represents the config of package stage.
type PackageStage struct {
	GeneralStage
	Outputs []string `bson:"outputs,omitempty" json:"outputs,omitempty" description:"list of output path of this stage"`
}

// ImageBuildStage represents the config of image build stage.
type ImageBuildStage struct {
	BuildInfos []*ImageBuildInfo `bson:"buildInfos,omitempty" json:"buildInfos,omitempty" description:"list of output path of this stage"`
}

// ImageBuildInfo represents the config to build the image. Only one of Dockerfile and DockerfilePath needs to be set.
// If both of them are set, Dockerfile will be used with high priority.
type ImageBuildInfo struct {
	ContextDir     string `bson:"contextDir,omitempty" json:"contextDir,omitempty" description:"context directory for image build"`
	Dockerfile     string `bson:"dockerfile,omitempty" json:"dockerfile,omitempty" description:"dockerfile content for image build"`
	DockerfilePath string `bson:"dockerfilePath,omitempty" json:"dockerfilePath,omitempty" description:"dockerfile path for image build"`
	ImageName      string `bson:"imageName,omitempty" json:"imageName,omitempty" description:"name of the built image"`
}

// IntegrationTestStage represents the config of integration test stage.
type IntegrationTestStage struct {
	IntegrationTestSet *IntegrationTestSet `bson:"integrationTestSet,omitempty" json:"integrationTestSet,omitempty" description:"integration test config"`
	Services           []Service           `bson:"services,omitempty" json:"services,omitempty" description:"list of dependent services for integration test"`
}

// IntegrationTestSet represents the config for integration test set.
type IntegrationTestSet struct {
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"built image to run the integration test"`
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to run for integration test"`
}

// Service represents the dependent service needed for integration test.
type Service struct {
	Name    string   `bson:"name,omitempty" json:"name,omitempty" description:"name of the service"`
	Image   string   `bson:"image,omitempty" json:"image,omitempty" description:"image name of the service"`
	Command []string `bson:"command,omitempty" json:"command,omitempty" description:"list of commands to start the service"`
	EnvVars []EnvVar `bson:"envVars,omitempty" json:"envVars,omitempty" description:"environment variables of the service"`
}

// ImageReleaseStage represents the config of image release stage.
type ImageReleaseStage struct {
	ReleasePolicy []ImageReleasePolicy `bson:"releasePolicy,omitempty" json:"releasePolicy,omitempty" description:"list of policies for image release"`
}

// ImageReleasePolicyType represents the type of image release policy.
type ImageReleasePolicyType string

const (
	// AlwaysRelease always releases the images.
	AlwaysRelease ImageReleasePolicyType = "Always"

	// NeverRelease never releases the images.
	NeverRelease ImageReleasePolicyType = "Never"

	// IntegrationTestSuccessRelease releases the images only when the integration test success.
	IntegrationTestSuccessRelease ImageReleasePolicyType = "IntegrationTestSuccess"
)

// ImageReleasePolicy represents the policy to release image.
type ImageReleasePolicy struct {
	Image string                 `bson:"image,omitempty" json:"image,omitempty" description:"image to be released"`
	Type  ImageReleasePolicyType `bson:"type,omitempty" json:"type,omitempty" description:"type of image release policy"`
}

// AutoTrigger represents the auto trigger strategy of the pipeline.
type AutoTrigger struct {
	SCMTrigger   *SCMTrigger   `bson:"scmTrigger,omitempty" json:"scmTrigger,omitempty" description:"SCM trigger strategy"`
	TimerTrigger *TimerTrigger `bson:"timerTrigger,omitempty" json:"timerTrigger,omitempty" description:"timer trigger strategy"`
}

// SCMTrigger represents the auto trigger strategy from SCM.
type SCMTrigger struct {
	CommitTrigger *CommitTrigger `bson:"commitTrigger,omitempty" json:"commitTrigger,omitempty" description:"commit trigger strategy"`
}

// GeneralTrigger represents the general config for all auto trigger strategies.
type GeneralTrigger struct {
	FinalStage string `bson:"finalStage,omitempty" json:"finalStage,omitempty" description:"final stage of the auto triggered running"`
}

// CommitTrigger represents the trigger from SCM commit.
type CommitTrigger struct {
	GeneralTrigger
}

// CommitWithCommentsTrigger represents the trigger from SCM commit with specified comments.
type CommitWithCommentsTrigger struct {
	GeneralTrigger
	Comment []string `bson:"comment,omitempty" json:"comment,omitempty" description:"list of comments in commit"`
}

// TimerTrigger represents the auto trigger strategy from timer.
type TimerTrigger struct {
	CronTrigger []*CronTrigger `bson:"cronTrigger,omitempty" json:"cronTrigger,omitempty" description:"list of cron trigger strategies"`
}

// CronTrigger represents the trigger from cron job.
type CronTrigger struct {
	GeneralTrigger
	Expression string `bson:"expression,omitempty" json:"expression,omitempty" description:"expression of cron job"`
}

// PipelineRecord represents the running record of pipeline.
type PipelineRecord struct {
	ID          string       `bson:"_id,omitempty" json:"id,omitempty" description:"id of the pipeline record"`
	PipelineID  string       `bson:"pipelineId,omitempty" json:"pipelineId,omitempty" description:"id of the related pipeline which the pipeline record belongs to"`
	VersionID   string       `bson:"versionId,omitempty" json:"versionId,omitempty" description:"id of the version which the pipeline record is related to"`
	Trigger     string       `bson:"trigger,omitempty" json:"trigger,omitempty" description:"trigger of the pipeline record"`
	StageStatus *StageStatus `bson:"stageStatus,omitempty" json:"stageStatus,omitempty" description:"status of each pipeline stage"`
	Status      Status       `bson:"status,omitempty" json:"status,omitempty" description:"status of the pipeline record"`
	StartTime   time.Time    `bson:"startTime,omitempty" json:"startTime,omitempty" description:"start time of the pipeline record"`
	EndTime     time.Time    `bson:"endTime,omitempty" json:"endTime,omitempty" description:"end time of the pipeline record"`
}

// Status can be the status of some pipeline record or some stage
type Status string

const (
	// Pending represents the status that is triggered but still not running.
	Pending Status = "Pending"
	// Running represents the status that is running.
	Running Status = "Running"
	// Success represents the status that finished and succeeded.
	Success Status = "Success"
	// Failure represents the status that finished but failed.
	Failure Status = "Failed"
	// Abort represents the status that the stage was aborted by some reason, and we can get the reason from the log.
	Abort Status = "Aborted"
)

// TODO The status of every stage may be different.
// StageStatus represents the collections of status for all stages.
type StageStatus struct {
	CodeCheckout    *GeneralStageStatus `bson:"codeCheckout,omitempty" json:"codeCheckout,omitempty" description:"status of code checkout stage"`
	UnitTest        *GeneralStageStatus `bson:"unitTest,omitempty" json:"unitTest,omitempty" description:"status of unit test stage"`
	CodeScan        *GeneralStageStatus `bson:"codeScan,omitempty" json:"codeScan,omitempty" description:"status of code scan stage"`
	Package         *GeneralStageStatus `bson:"package,omitempty" json:"package,omitempty" description:"status of package stage"`
	ImageBuild      *GeneralStageStatus `bson:"imageBuild,omitempty" json:"imageBuild,omitempty" description:"status of image build stage"`
	IntegrationTest *GeneralStageStatus `bson:"integrationTest,omitempty" json:"integrationTest,omitempty" description:"status of integration test stage"`
	ImageRelease    *GeneralStageStatus `bson:"imageRelease,omitempty" json:"imageRelease,omitempty" description:"status of image release stage"`
}

// GeneralStageStatus represents the information of stage.
type GeneralStageStatus struct {
	Status    Status    `bson:"status,omitempty" json:"status,omitempty" description:"status of the stage"`
	StartTime time.Time `bson:"startTime,omitempty" json:"startTime,omitempty" description:"start time of the stage"`
	EndTime   time.Time `bson:"endTime,omitempty" json:"endTime,omitempty" description:"end time of the stage"`
}
<|endoftext|>


<commit_before>// Package file allows reading/writing from/to a Rump file.
package file

import (
	"context"
	"bufio"
	"fmt"
	"os"

	"github.com/stickermule/rump/pkg/message"
)

// File can read and write, to a file Path, using the message Bus.
type File struct {
	Path string
	Bus  message.Bus
}

// New creates the File struct, to be used for reading/writing.
func New(path string, bus message.Bus) *File {
	return &File{
		Path: path,
		Bus:  bus,
	}
}

// Read scans a Rump .dump file and sends Payloads to the message bus.
func (f *File) Read(ctx context.Context) error {
	defer close(f.Bus)
	d, err := os.Open(f.Path)
	defer d.Close()
	if err != nil {
		return err
	}

	// Scan file with default ScanLines
	scanner := bufio.NewScanner(d)

	// Scan line by line
	// file protocol is two lines per key/value pair: key\n value\n
	for scanner.Scan() {
		// Get key on first line
		key := scanner.Text()
		// trigger next scan to get value on next line
		scanner.Scan()
		value := scanner.Text()
		select {
		case <-ctx.Done():
			fmt.Println("")
			fmt.Println("file read: exit")
			return ctx.Err()
		case f.Bus <- message.Payload{Key: key, Value: value}:
			fmt.Printf("r")
		}
	}

	return nil
}

// Write writes to a Rump file Payloads from the message bus.
func (f *File) Write(ctx context.Context) error {
	d, err := os.Create(f.Path)
	defer d.Close()
	if err != nil {
		return err
	}

	// Buffered write to limit system IO calls
	w := bufio.NewWriter(d)

	for f.Bus != nil {
		select {
		// Exit early if context done.
		case <-ctx.Done():
			fmt.Println("")
			fmt.Println("file write: exit")
			return ctx.Err()
		// Get Messages from Bus
		case p, ok := <-f.Bus:
			// if channel closed, set to nil, break loop
			if !ok {
				f.Bus = nil
				continue
			}
			_, err := w.WriteString(p.Key + "\n" + p.Value + "\n")
			if err != nil {
				return err
			}
			fmt.Printf("w")
		}
	}

	// Flush last open buffers
	w.Flush()

	return nil
}
<commit_msg>file: custom double-cross split protocol<commit_after>// Package file allows reading/writing from/to a Rump file.
// Rump file protocol is key✝✝value✝✝key✝✝value✝✝...
package file

import (
	"context"
	"bufio"
	"fmt"
	"os"
	"strings"

	"github.com/stickermule/rump/pkg/message"
)

// File can read and write, to a file Path, using the message Bus.
type File struct {
	Path string
	Bus  message.Bus
}

// split is double-cross (✝✝) custom Scanner Split.
func splitCross(data []byte, atEOF bool) (advance int, token []byte, err error) {

	// end of file
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}

	// Split at separator
	if i := strings.Index(string(data), "✝✝"); i >= 0 {
		// Separator is 6 bytes long
		return i + 6, data[0:i], nil
	}

	return 0, nil, nil
}

// New creates the File struct, to be used for reading/writing.
func New(path string, bus message.Bus) *File {
	return &File{
		Path: path,
		Bus:  bus,
	}
}

// Read scans a Rump file and sends Payloads to the message bus.
func (f *File) Read(ctx context.Context) error {
	defer close(f.Bus)
	d, err := os.Open(f.Path)
	defer d.Close()
	if err != nil {
		return err
	}

	// Scan file, split by double-cross separator
	scanner := bufio.NewScanner(d)
	scanner.Split(splitCross)

	// Scan line by line
	// file protocol is key✝✝value✝✝
	for scanner.Scan() {
		// Get key on first line
		key := scanner.Text()
		// trigger next scan to get value on next line
		scanner.Scan()
		value := scanner.Text()
		select {
		case <-ctx.Done():
			fmt.Println("")
			fmt.Println("file read: exit")
			return ctx.Err()
		case f.Bus <- message.Payload{Key: key, Value: value}:
			fmt.Printf("r")
		}
	}

	return nil
}

// Write writes to a Rump file Payloads from the message bus.
func (f *File) Write(ctx context.Context) error {
	d, err := os.Create(f.Path)
	defer d.Close()
	if err != nil {
		return err
	}

	// Buffered write to limit system IO calls
	w := bufio.NewWriter(d)

	for f.Bus != nil {
		select {
		// Exit early if context done.
		case <-ctx.Done():
			fmt.Println("")
			fmt.Println("file write: exit")
			return ctx.Err()
		// Get Messages from Bus
		case p, ok := <-f.Bus:
			// if channel closed, set to nil, break loop
			if !ok {
				f.Bus = nil
				continue
			}
			_, err := w.WriteString(p.Key + "✝✝" + p.Value + "✝✝")
			if err != nil {
				return err
			}
			fmt.Printf("w")
		}
	}

	// Flush last open buffers
	w.Flush()

	return nil
}
<|endoftext|>


<commit_before>package via

import (
	"github.com/cheekybits/is"
	"testing"
)

func TestAdd(t *testing.T) {
	var (
		is     = is.New(t)
		expect = "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB"
	)
	cid, err := IpfsAdd(testConfig, "testdata/ipfs/readme")
	is.Nil(err)
	is.Equal(cid, expect)
}

func TestHashOnly(t *testing.T) {
	var (
		is     = is.New(t)
		expect = "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB"
	)
	cid, err := HashOnly(testConfig, "testdata/ipfs/readme")
	is.Nil(err)
	is.Equal(cid, expect)
}
<commit_msg>how to generate ipfs test data is now documented<commit_after>package via

import (
	"testing"
)

// testdata/ipfs directory is generated using.
// ipfs get -o testdata/ipfs QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv

func TestAdd(t *testing.T) {
	var (
		expect = "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB"
	)
	got, err := IpfsAdd(testConfig, "testdata/ipfs/readme")
	if err != nil {
		t.Error(err)
	}
	if got != expect {
		t.Errorf("expect %s got %s", expect, got)
	}

}

func TestHashOnly(t *testing.T) {
	var (
		expect = "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB"
	)
	got, err := HashOnly(testConfig, "testdata/ipfs/readme")
	if err != nil {
		t.Error(err)
	}
	if got != expect {
		t.Errorf("expect %s got %s", expect, got)
	}
}
<|endoftext|>


<commit_before>// Copyright 2016-2019 Authors of Cilium
//
// 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 node

import (
	"encoding/json"
	"net"
	"path"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/cidr"
	"github.com/cilium/cilium/pkg/defaults"
	"github.com/cilium/cilium/pkg/kvstore/store"
	"github.com/cilium/cilium/pkg/node/addressing"
	"github.com/cilium/cilium/pkg/option"
)

// Source is the description of the source of an identity
type Source string

const (
	// FromKubernetes is the source used for identities derived from k8s
	// resources (pods)
	FromKubernetes Source = "k8s"

	// FromKVStore is the source used for identities derived from the
	// kvstore
	FromKVStore Source = "kvstore"

	// FromAgentLocal is the source used for identities derived during the
	// agent bootup process. This includes identities for endpoint IPs.
	FromAgentLocal Source = "agent-local"

	// FromLocalNode is the source used for the local node
	FromLocalNode Source = "local-node"
)

// Identity represents the node identity of a node.
type Identity struct {
	Name    string
	Cluster string
}

// String returns the string representation on NodeIdentity.
func (nn Identity) String() string {
	return path.Join(nn.Cluster, nn.Name)
}

// Node contains the nodes name, the list of addresses to this address
//
// +k8s:deepcopy-gen=true
type Node struct {
	// Name is the name of the node. This is typically the hostname of the node.
	Name string

	// Cluster is the name of the cluster the node is associated with
	Cluster string

	IPAddresses []Address

	// IPv4AllocCIDR if set, is the IPv4 address pool out of which the node
	// allocates IPs for local endpoints from
	IPv4AllocCIDR *cidr.CIDR

	// IPv6AllocCIDR if set, is the IPv6 address pool out of which the node
	// allocates IPs for local endpoints from
	IPv6AllocCIDR *cidr.CIDR

	// IPv4HealthIP if not nil, this is the IPv4 address of the
	// cilium-health endpoint located on the node.
	IPv4HealthIP net.IP

	// IPv6HealthIP if not nil, this is the IPv6 address of the
	// cilium-health endpoint located on the node.
	IPv6HealthIP net.IP

	// ClusterID is the unique identifier of the cluster
	ClusterID int

	// Source is the source where the node configuration was generated / created.
	Source Source
}

// Fullname returns the node's full name including the cluster name if a
// cluster name value other than the default value has been specified
func (n *Node) Fullname() string {
	if n.Cluster != defaults.ClusterName {
		return path.Join(n.Cluster, n.Name)
	}

	return n.Name
}

// Address is a node address which contains an IP and the address type.
//
// +k8s:deepcopy-gen=true
type Address struct {
	Type addressing.AddressType
	IP   net.IP
}

func (n *Node) getNodeIP(ipv6 bool) (net.IP, addressing.AddressType) {
	var (
		backupIP net.IP
		ipType   addressing.AddressType
	)
	for _, addr := range n.IPAddresses {
		if (ipv6 && addr.IP.To4() != nil) ||
			(!ipv6 && addr.IP.To4() == nil) {
			continue
		}
		switch addr.Type {
		// Ignore CiliumInternalIPs
		case addressing.NodeCiliumInternalIP:
			continue
		// Always prefer a cluster internal IP
		case addressing.NodeInternalIP:
			return addr.IP, addr.Type
		case addressing.NodeExternalIP:
			// Fall back to external Node IP
			// if no internal IP could be found
			backupIP = addr.IP
			ipType = addr.Type
		default:
			// As a last resort, if no internal or external
			// IP was found, use any node address available
			if backupIP == nil {
				backupIP = addr.IP
				ipType = addr.Type
			}
		}
	}
	return backupIP, ipType
}

// GetNodeIP returns one of the node's IP addresses available with the
// following priority:
// - NodeInternalIP
// - NodeExternalIP
// - other IP address type
func (n *Node) GetNodeIP(ipv6 bool) net.IP {
	result, _ := n.getNodeIP(ipv6)
	return result
}

// GetCiliumInternalIP returns the CiliumInternalIP e.g. the IP associated
// with cilium_host on the node.
func (n *Node) GetCiliumInternalIP(ipv6 bool) net.IP {
	for _, addr := range n.IPAddresses {
		if (ipv6 && addr.IP.To4() != nil) ||
			(!ipv6 && addr.IP.To4() == nil) {
			continue
		}
		if addr.Type == addressing.NodeCiliumInternalIP {
			return addr.IP
		}
	}
	return nil
}

func (n *Node) getPrimaryAddress() *models.NodeAddressing {
	v4, v4Type := n.getNodeIP(false)
	v6, v6Type := n.getNodeIP(true)

	var ipv4AllocStr, ipv6AllocStr string
	if n.IPv4AllocCIDR != nil {
		ipv4AllocStr = n.IPv4AllocCIDR.String()
	}
	if n.IPv6AllocCIDR != nil {
		ipv6AllocStr = n.IPv6AllocCIDR.String()
	}
	return &models.NodeAddressing{
		IPV4: &models.NodeAddressingElement{
			Enabled:     option.Config.EnableIPv4,
			IP:          v4.String(),
			AllocRange:  ipv4AllocStr,
			AddressType: string(v4Type),
		},
		IPV6: &models.NodeAddressingElement{
			Enabled:     option.Config.EnableIPv6,
			IP:          v6.String(),
			AllocRange:  ipv6AllocStr,
			AddressType: string(v6Type),
		},
	}
}

func (n *Node) isPrimaryAddress(addr Address, ipv4 bool) bool {
	return addr.IP.String() == n.GetNodeIP(!ipv4).String()
}

func (n *Node) getSecondaryAddresses() []*models.NodeAddressingElement {
	result := []*models.NodeAddressingElement{}

	for _, addr := range n.IPAddresses {
		ipv4 := false
		if addr.IP.To4() != nil {
			ipv4 = true
		}
		if !n.isPrimaryAddress(addr, ipv4) {
			result = append(result, &models.NodeAddressingElement{
				IP:          addr.IP.String(),
				AddressType: string(addr.Type),
			})
		}
	}

	return result
}

func (n *Node) getHealthAddresses() *models.NodeAddressing {
	if n.IPv4HealthIP == nil || n.IPv6HealthIP == nil {
		return nil
	}
	return &models.NodeAddressing{
		IPV4: &models.NodeAddressingElement{
			Enabled: option.Config.EnableIPv4,
			IP:      n.IPv4HealthIP.String(),
		},
		IPV6: &models.NodeAddressingElement{
			Enabled: option.Config.EnableIPv6,
			IP:      n.IPv6HealthIP.String(),
		},
	}
}

// GetModel returns the API model representation of a node.
func (n *Node) GetModel() *models.NodeElement {
	return &models.NodeElement{
		Name:                  n.Fullname(),
		PrimaryAddress:        n.getPrimaryAddress(),
		SecondaryAddresses:    n.getSecondaryAddresses(),
		HealthEndpointAddress: n.getHealthAddresses(),
	}
}

// Identity returns the identity of the node
func (n *Node) Identity() Identity {
	return Identity{
		Name:    n.Name,
		Cluster: n.Cluster,
	}
}

// IsLocal returns true if this is the node on which the agent itself is
// running on
func (n *Node) IsLocal() bool {
	return n != nil && n.Name == GetName()
}

// PublicAttrEquals returns true only if the public attributes of both nodes
// are the same otherwise returns false.
func (n *Node) PublicAttrEquals(o *Node) bool {
	if (n == nil) != (o == nil) {
		return false
	}

	if n.Name == o.Name &&
		n.Cluster == o.Cluster &&
		n.IPv4HealthIP.Equal(o.IPv4HealthIP) &&
		n.IPv6HealthIP.Equal(o.IPv6HealthIP) &&
		n.ClusterID == o.ClusterID &&
		n.Source == o.Source {

		if len(n.IPAddresses) != len(o.IPAddresses) {
			return false
		}

		for i := range n.IPAddresses {
			if (n.IPAddresses[i].Type != o.IPAddresses[i].Type) ||
				!n.IPAddresses[i].IP.Equal(o.IPAddresses[i].IP) {
				return false
			}
		}

		if (n.IPv4AllocCIDR == nil) != (o.IPv4AllocCIDR == nil) {
			return false
		}
		if n.IPv4AllocCIDR.String() != o.IPv4AllocCIDR.String() {
			return false
		}

		if (n.IPv6AllocCIDR == nil) != (o.IPv6AllocCIDR == nil) {
			return false
		}
		if n.IPv6AllocCIDR.String() != o.IPv6AllocCIDR.String() {
			return false
		}

		return true
	}

	return false
}

// GetKeyName returns the kvstore key to be used for the node
func (n *Node) GetKeyName() string {
	// WARNING - STABLE API: Changing the structure of the key may break
	// backwards compatibility
	return path.Join(n.Cluster, n.Name)
}

// DeepKeyCopy creates a deep copy of the LocalKey
func (n *Node) DeepKeyCopy() store.LocalKey {
	return n.DeepCopy()
}

// Marshal returns the node object as JSON byte slice
func (n *Node) Marshal() ([]byte, error) {
	return json.Marshal(n)
}

// Unmarshal parses the JSON byte slice and updates the node receiver
func (n *Node) Unmarshal(data []byte) error {
	newNode := Node{}
	if err := json.Unmarshal(data, &newNode); err != nil {
		return err
	}

	*n = newNode

	return nil
}
<commit_msg>node: Fix health endpoint IP fetch with IP disable<commit_after>// Copyright 2016-2019 Authors of Cilium
//
// 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 node

import (
	"encoding/json"
	"net"
	"path"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/cidr"
	"github.com/cilium/cilium/pkg/defaults"
	"github.com/cilium/cilium/pkg/kvstore/store"
	"github.com/cilium/cilium/pkg/node/addressing"
	"github.com/cilium/cilium/pkg/option"
)

// Source is the description of the source of an identity
type Source string

const (
	// FromKubernetes is the source used for identities derived from k8s
	// resources (pods)
	FromKubernetes Source = "k8s"

	// FromKVStore is the source used for identities derived from the
	// kvstore
	FromKVStore Source = "kvstore"

	// FromAgentLocal is the source used for identities derived during the
	// agent bootup process. This includes identities for endpoint IPs.
	FromAgentLocal Source = "agent-local"

	// FromLocalNode is the source used for the local node
	FromLocalNode Source = "local-node"
)

// Identity represents the node identity of a node.
type Identity struct {
	Name    string
	Cluster string
}

// String returns the string representation on NodeIdentity.
func (nn Identity) String() string {
	return path.Join(nn.Cluster, nn.Name)
}

// Node contains the nodes name, the list of addresses to this address
//
// +k8s:deepcopy-gen=true
type Node struct {
	// Name is the name of the node. This is typically the hostname of the node.
	Name string

	// Cluster is the name of the cluster the node is associated with
	Cluster string

	IPAddresses []Address

	// IPv4AllocCIDR if set, is the IPv4 address pool out of which the node
	// allocates IPs for local endpoints from
	IPv4AllocCIDR *cidr.CIDR

	// IPv6AllocCIDR if set, is the IPv6 address pool out of which the node
	// allocates IPs for local endpoints from
	IPv6AllocCIDR *cidr.CIDR

	// IPv4HealthIP if not nil, this is the IPv4 address of the
	// cilium-health endpoint located on the node.
	IPv4HealthIP net.IP

	// IPv6HealthIP if not nil, this is the IPv6 address of the
	// cilium-health endpoint located on the node.
	IPv6HealthIP net.IP

	// ClusterID is the unique identifier of the cluster
	ClusterID int

	// Source is the source where the node configuration was generated / created.
	Source Source
}

// Fullname returns the node's full name including the cluster name if a
// cluster name value other than the default value has been specified
func (n *Node) Fullname() string {
	if n.Cluster != defaults.ClusterName {
		return path.Join(n.Cluster, n.Name)
	}

	return n.Name
}

// Address is a node address which contains an IP and the address type.
//
// +k8s:deepcopy-gen=true
type Address struct {
	Type addressing.AddressType
	IP   net.IP
}

func (n *Node) getNodeIP(ipv6 bool) (net.IP, addressing.AddressType) {
	var (
		backupIP net.IP
		ipType   addressing.AddressType
	)
	for _, addr := range n.IPAddresses {
		if (ipv6 && addr.IP.To4() != nil) ||
			(!ipv6 && addr.IP.To4() == nil) {
			continue
		}
		switch addr.Type {
		// Ignore CiliumInternalIPs
		case addressing.NodeCiliumInternalIP:
			continue
		// Always prefer a cluster internal IP
		case addressing.NodeInternalIP:
			return addr.IP, addr.Type
		case addressing.NodeExternalIP:
			// Fall back to external Node IP
			// if no internal IP could be found
			backupIP = addr.IP
			ipType = addr.Type
		default:
			// As a last resort, if no internal or external
			// IP was found, use any node address available
			if backupIP == nil {
				backupIP = addr.IP
				ipType = addr.Type
			}
		}
	}
	return backupIP, ipType
}

// GetNodeIP returns one of the node's IP addresses available with the
// following priority:
// - NodeInternalIP
// - NodeExternalIP
// - other IP address type
func (n *Node) GetNodeIP(ipv6 bool) net.IP {
	result, _ := n.getNodeIP(ipv6)
	return result
}

// GetCiliumInternalIP returns the CiliumInternalIP e.g. the IP associated
// with cilium_host on the node.
func (n *Node) GetCiliumInternalIP(ipv6 bool) net.IP {
	for _, addr := range n.IPAddresses {
		if (ipv6 && addr.IP.To4() != nil) ||
			(!ipv6 && addr.IP.To4() == nil) {
			continue
		}
		if addr.Type == addressing.NodeCiliumInternalIP {
			return addr.IP
		}
	}
	return nil
}

func (n *Node) getPrimaryAddress() *models.NodeAddressing {
	v4, v4Type := n.getNodeIP(false)
	v6, v6Type := n.getNodeIP(true)

	var ipv4AllocStr, ipv6AllocStr string
	if n.IPv4AllocCIDR != nil {
		ipv4AllocStr = n.IPv4AllocCIDR.String()
	}
	if n.IPv6AllocCIDR != nil {
		ipv6AllocStr = n.IPv6AllocCIDR.String()
	}
	return &models.NodeAddressing{
		IPV4: &models.NodeAddressingElement{
			Enabled:     option.Config.EnableIPv4,
			IP:          v4.String(),
			AllocRange:  ipv4AllocStr,
			AddressType: string(v4Type),
		},
		IPV6: &models.NodeAddressingElement{
			Enabled:     option.Config.EnableIPv6,
			IP:          v6.String(),
			AllocRange:  ipv6AllocStr,
			AddressType: string(v6Type),
		},
	}
}

func (n *Node) isPrimaryAddress(addr Address, ipv4 bool) bool {
	return addr.IP.String() == n.GetNodeIP(!ipv4).String()
}

func (n *Node) getSecondaryAddresses() []*models.NodeAddressingElement {
	result := []*models.NodeAddressingElement{}

	for _, addr := range n.IPAddresses {
		ipv4 := false
		if addr.IP.To4() != nil {
			ipv4 = true
		}
		if !n.isPrimaryAddress(addr, ipv4) {
			result = append(result, &models.NodeAddressingElement{
				IP:          addr.IP.String(),
				AddressType: string(addr.Type),
			})
		}
	}

	return result
}

func (n *Node) getHealthAddresses() *models.NodeAddressing {
	if n.IPv4HealthIP == nil && n.IPv6HealthIP == nil {
		return nil
	}
	return &models.NodeAddressing{
		IPV4: &models.NodeAddressingElement{
			Enabled: option.Config.EnableIPv4,
			IP:      n.IPv4HealthIP.String(),
		},
		IPV6: &models.NodeAddressingElement{
			Enabled: option.Config.EnableIPv6,
			IP:      n.IPv6HealthIP.String(),
		},
	}
}

// GetModel returns the API model representation of a node.
func (n *Node) GetModel() *models.NodeElement {
	return &models.NodeElement{
		Name:                  n.Fullname(),
		PrimaryAddress:        n.getPrimaryAddress(),
		SecondaryAddresses:    n.getSecondaryAddresses(),
		HealthEndpointAddress: n.getHealthAddresses(),
	}
}

// Identity returns the identity of the node
func (n *Node) Identity() Identity {
	return Identity{
		Name:    n.Name,
		Cluster: n.Cluster,
	}
}

// IsLocal returns true if this is the node on which the agent itself is
// running on
func (n *Node) IsLocal() bool {
	return n != nil && n.Name == GetName()
}

// PublicAttrEquals returns true only if the public attributes of both nodes
// are the same otherwise returns false.
func (n *Node) PublicAttrEquals(o *Node) bool {
	if (n == nil) != (o == nil) {
		return false
	}

	if n.Name == o.Name &&
		n.Cluster == o.Cluster &&
		n.IPv4HealthIP.Equal(o.IPv4HealthIP) &&
		n.IPv6HealthIP.Equal(o.IPv6HealthIP) &&
		n.ClusterID == o.ClusterID &&
		n.Source == o.Source {

		if len(n.IPAddresses) != len(o.IPAddresses) {
			return false
		}

		for i := range n.IPAddresses {
			if (n.IPAddresses[i].Type != o.IPAddresses[i].Type) ||
				!n.IPAddresses[i].IP.Equal(o.IPAddresses[i].IP) {
				return false
			}
		}

		if (n.IPv4AllocCIDR == nil) != (o.IPv4AllocCIDR == nil) {
			return false
		}
		if n.IPv4AllocCIDR.String() != o.IPv4AllocCIDR.String() {
			return false
		}

		if (n.IPv6AllocCIDR == nil) != (o.IPv6AllocCIDR == nil) {
			return false
		}
		if n.IPv6AllocCIDR.String() != o.IPv6AllocCIDR.String() {
			return false
		}

		return true
	}

	return false
}

// GetKeyName returns the kvstore key to be used for the node
func (n *Node) GetKeyName() string {
	// WARNING - STABLE API: Changing the structure of the key may break
	// backwards compatibility
	return path.Join(n.Cluster, n.Name)
}

// DeepKeyCopy creates a deep copy of the LocalKey
func (n *Node) DeepKeyCopy() store.LocalKey {
	return n.DeepCopy()
}

// Marshal returns the node object as JSON byte slice
func (n *Node) Marshal() ([]byte, error) {
	return json.Marshal(n)
}

// Unmarshal parses the JSON byte slice and updates the node receiver
func (n *Node) Unmarshal(data []byte) error {
	newNode := Node{}
	if err := json.Unmarshal(data, &newNode); err != nil {
		return err
	}

	*n = newNode

	return nil
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC
//
// 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 handlers define HTTP handlers.
package site

import (
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/triage-party/pkg/hubbub"
	"github.com/google/triage-party/pkg/triage"
	"github.com/google/triage-party/pkg/updater"

	"github.com/dustin/go-humanize"
	"github.com/google/go-github/v31/github"
	"gopkg.in/yaml.v2"

	"k8s.io/klog/v2"
)

const VERSION = "v1.0.0-beta.3"

var (
	nonWordRe  = regexp.MustCompile(`\W`)
	MaxPlayers = 20
)

// Config is how external users interact with this package.
type Config struct {
	BaseDirectory string
	Name          string
	WarnAge       time.Duration
	Updater       *updater.Updater
	Party         *triage.Party
}

func New(c *Config) *Handlers {
	return &Handlers{
		baseDir:   c.BaseDirectory,
		updater:   c.Updater,
		party:     c.Party,
		siteName:  c.Name,
		warnAge:   c.WarnAge,
		startTime: time.Now(),
	}
}

// Handlers is a mix of config and client interfaces to connect with.
type Handlers struct {
	baseDir   string
	updater   *updater.Updater
	party     *triage.Party
	siteName  string
	warnAge   time.Duration
	startTime time.Time
}

// Root redirects to leaderboard.
func (h *Handlers) Root() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		sts, err := h.party.ListCollections()
		if err != nil {
			klog.Errorf("collections: %v", err)
			return
		}
		http.Redirect(w, r, fmt.Sprintf("/s/%s", sts[0].ID), http.StatusSeeOther)
	}
}

// Page are values that are passed into the renderer
type Page struct {
	Version     string
	SiteName    string
	ID          string
	Title       string
	Description string
	Warning     string
	Total       int
	TotalShown  int
	Types       string
	UniqueItems []*hubbub.Conversation
	ResultAge   time.Duration

	Player        int
	Players       int
	PlayerChoices []string
	PlayerNums    []int
	Mode          int
	Index         int
	EmbedURL      string

	AverageResponseLatency time.Duration
	TotalPullRequests      int
	TotalIssues            int

	Collection  triage.Collection
	Collections []triage.Collection

	CollectionResult *triage.CollectionResult
	Stats            *triage.CollectionResult
	StatsID          string

	GetVars string
}

// is this request an HTTP refresh?
func isRefresh(r *http.Request) bool {
	cc := r.Header["Cache-Control"]
	if len(cc) == 0 {
		return false
	}
	//	klog.Infof("cc=%s headers=%+v", cc, r.Header)
	return cc[0] == "max-age-0" || cc[0] == "no-cache"
}

// Board shows a stratgy board
func (h *Handlers) Collection() http.HandlerFunc {
	fmap := template.FuncMap{
		"toJS":          toJS,
		"toYAML":        toYAML,
		"toJSfunc":      toJSfunc,
		"toDays":        toDays,
		"HumanDuration": humanDuration,
		"HumanTime":     humanTime,
		"UnixNano":      unixNano,
		"Avatar":        avatar,
	}
	t := template.Must(template.New("collection").Funcs(fmap).ParseFiles(
		filepath.Join(h.baseDir, "collection.tmpl"),
		filepath.Join(h.baseDir, "base.tmpl"),
	))

	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		dataAge := time.Time{}
		id := strings.TrimPrefix(r.URL.Path, "/s/")

		defer func() {
			klog.Infof("Served %q request within %s from data %s old", id, time.Since(start), time.Since(dataAge))
		}()

		playerChoices := []string{"Select a player"}
		players := getInt(r.URL, "players", 1)
		player := getInt(r.URL, "player", 0)
		mode := getInt(r.URL, "mode", 0)
		index := getInt(r.URL, "index", 1)

		for i := 0; i < players; i++ {
			playerChoices = append(playerChoices, fmt.Sprintf("Player %d", i+1))
		}

		playerNums := []int{}
		for i := 0; i < MaxPlayers; i++ {
			playerNums = append(playerNums, i+1)
		}

		klog.Infof("GET %s (%q): %v", r.URL.Path, id, r.Header)
		s, err := h.party.LookupCollection(id)
		if err != nil {
			http.Error(w, fmt.Sprintf("%q not found: old link or typo?", id), http.StatusNotFound)
			klog.Errorf("collection: %v", err)
			return
		}

		sts, err := h.party.ListCollections()
		if err != nil {
			klog.Errorf("collections: %v", err)
			http.Error(w, "list error", http.StatusInternalServerError)
			return
		}

		var result *triage.CollectionResult
		if isRefresh(r) {
			result = h.updater.ForceRefresh(r.Context(), id)
			klog.Infof("refresh %q result: %d items", id, len(result.RuleResults))
		} else {
			result = h.updater.Lookup(r.Context(), id, true)
			if result == nil {
				http.Error(w, fmt.Sprintf("%q no data", id), http.StatusNotFound)
				return
			}
			if result.RuleResults == nil {
				http.Error(w, fmt.Sprintf("%q no outcomes", id), http.StatusNotFound)
				return
			}

			klog.V(2).Infof("lookup %q result: %d items", id, len(result.RuleResults))
		}

		dataAge = result.Time
		warning := ""

		if time.Since(result.Time) > h.warnAge {
			warning = fmt.Sprintf("Serving results from %s ago. Service started %s ago and is downloading new data. Use Shift-Reload to force refresh at any time.", humanDuration(time.Since(result.Time)), humanDuration(time.Since(h.startTime)))
		}

		total := 0
		for _, o := range result.RuleResults {
			total += len(o.Items)
		}

		unique := []*hubbub.Conversation{}
		seen := map[int]bool{}
		for _, o := range result.RuleResults {
			for _, i := range o.Items {
				if !seen[i.ID] {
					unique = append(unique, i)
					seen[i.ID] = true
				}
			}
		}

		if player > 0 && players > 1 {
			result = playerFilter(result, player, players)
		}

		uniqueFiltered := []*hubbub.Conversation{}
		seenFiltered := map[int]bool{}
		for _, o := range result.RuleResults {
			for _, i := range o.Items {
				if !seenFiltered[i.ID] {
					uniqueFiltered = append(uniqueFiltered, i)
					seenFiltered[i.ID] = true
				}
			}
		}

		embedURL := ""
		if mode == 1 {
			searchIndex := 0
			for _, o := range result.RuleResults {
				for _, i := range o.Items {
					searchIndex++
					if searchIndex == index {
						embedURL = i.URL
					}
				}
			}
		}

		getVars := ""
		if players > 0 {
			getVars = fmt.Sprintf("?player=%d&players=%d", player, players)
		}

		p := &Page{
			ID:               s.ID,
			Version:          VERSION,
			SiteName:         h.siteName,
			Title:            s.Name,
			Collection:       s,
			Collections:      sts,
			Description:      s.Description,
			CollectionResult: result,
			Total:            len(unique),
			TotalShown:       len(uniqueFiltered),
			Types:            "Issues",
			PlayerChoices:    playerChoices,
			PlayerNums:       playerNums,
			Player:           player,
			Players:          players,
			Mode:             mode,
			Index:            index,
			EmbedURL:         embedURL,
			Warning:          warning,
			UniqueItems:      uniqueFiltered,
			GetVars:          getVars,
			ResultAge:        time.Since(result.Time),
		}

		for _, s := range sts {
			if s.UsedForStats {
				p.Stats = h.updater.Lookup(r.Context(), s.ID, false)
				p.StatsID = s.ID
			}
		}

		klog.V(2).Infof("page context: %+v", p)
		err = t.ExecuteTemplate(w, "base", p)
		if err != nil {
			klog.Errorf("tmpl: %v", err)
			return
		}
	}
}

// helper to get integers from a URL
func getInt(url *url.URL, key string, fallback int) int {
	vals := url.Query()[key]
	if len(vals) == 1 {
		i, err := strconv.ParseInt(vals[0], 10, 32)
		if err != nil {
			klog.Warningf("bad %s int value: %v", key, vals)
			return fallback
		}
		return int(i)
	}
	return fallback
}

func toYAML(v interface{}) string {
	s, err := yaml.Marshal(v)
	if err != nil {
		return fmt.Sprintf("yaml err: %v", err)
	}
	return string(s)
}

// Acknowledge JS sanitization issues (what data do we trust?)
func toJS(s string) template.JS {
	return template.JS(s)
}

// Acknowledge JS sanitization issues (what data do we trust?)
func toJSfunc(s string) template.JS {
	return template.JS(nonWordRe.ReplaceAllString(s, "_"))
}

func unixNano(t time.Time) int64 {
	return t.UnixNano()
}

func humanDuration(d time.Duration) string {
	return humanTime(time.Now().Add(-d))
}

func toDays(d time.Duration) string {
	return fmt.Sprintf("%0.1fd", d.Hours()/24)
}

func humanTime(t time.Time) string {
	if t.IsZero() {
		return ""
	}
	ds := humanize.Time(t)
	ds = strings.Replace(ds, " ago", "", 1)

	ds = strings.Replace(ds, " minutes", "min", 1)
	ds = strings.Replace(ds, " minute", "min", 1)

	ds = strings.Replace(ds, " hours", "h", 1)
	ds = strings.Replace(ds, " hour", "h", 1)

	ds = strings.Replace(ds, " days", "d", 1)
	ds = strings.Replace(ds, " day", "d", 1)

	ds = strings.Replace(ds, " months", "mo", 1)
	ds = strings.Replace(ds, " month", "mo", 1)

	ds = strings.Replace(ds, " years", "y", 1)
	ds = strings.Replace(ds, " year", "y", 1)

	ds = strings.Replace(ds, " weeks", "wk", 1)
	ds = strings.Replace(ds, " week", "wk", 1)

	return ds
}

func avatar(u *github.User) template.HTML {
	return template.HTML(fmt.Sprintf(`<a href="%s" title="%s"><img src="%s" width="20" height="20"></a>`, u.GetHTMLURL(), u.GetLogin(), u.GetAvatarURL()))
}

// playerFilter filters out results for a particular player
func playerFilter(result *triage.CollectionResult, player int, players int) *triage.CollectionResult {
	klog.Infof("Filtering for player %d of %d ...", player, players)
	os := []*triage.RuleResult{}
	seen := map[string]*triage.Rule{}

	for _, o := range result.RuleResults {
		cs := []*hubbub.Conversation{}

		for _, i := range o.Items {
			if (i.ID % players) == (player - 1) {
				klog.Infof("%d belongs to player %d", i.ID, player)
				cs = append(cs, i)
			}
		}

		os = append(os, triage.SummarizeRuleResult(o.Rule, cs, seen))
	}

	return triage.SummarizeCollectionResult(os)
}
<commit_msg>Version bump to v1.0.0-beta.4<commit_after>// Copyright 2020 Google LLC
//
// 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 handlers define HTTP handlers.
package site

import (
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/triage-party/pkg/hubbub"
	"github.com/google/triage-party/pkg/triage"
	"github.com/google/triage-party/pkg/updater"

	"github.com/dustin/go-humanize"
	"github.com/google/go-github/v31/github"
	"gopkg.in/yaml.v2"

	"k8s.io/klog/v2"
)

const VERSION = "v1.0.0-beta.4"

var (
	nonWordRe  = regexp.MustCompile(`\W`)
	MaxPlayers = 20
)

// Config is how external users interact with this package.
type Config struct {
	BaseDirectory string
	Name          string
	WarnAge       time.Duration
	Updater       *updater.Updater
	Party         *triage.Party
}

func New(c *Config) *Handlers {
	return &Handlers{
		baseDir:   c.BaseDirectory,
		updater:   c.Updater,
		party:     c.Party,
		siteName:  c.Name,
		warnAge:   c.WarnAge,
		startTime: time.Now(),
	}
}

// Handlers is a mix of config and client interfaces to connect with.
type Handlers struct {
	baseDir   string
	updater   *updater.Updater
	party     *triage.Party
	siteName  string
	warnAge   time.Duration
	startTime time.Time
}

// Root redirects to leaderboard.
func (h *Handlers) Root() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		sts, err := h.party.ListCollections()
		if err != nil {
			klog.Errorf("collections: %v", err)
			return
		}
		http.Redirect(w, r, fmt.Sprintf("/s/%s", sts[0].ID), http.StatusSeeOther)
	}
}

// Page are values that are passed into the renderer
type Page struct {
	Version     string
	SiteName    string
	ID          string
	Title       string
	Description string
	Warning     string
	Total       int
	TotalShown  int
	Types       string
	UniqueItems []*hubbub.Conversation
	ResultAge   time.Duration

	Player        int
	Players       int
	PlayerChoices []string
	PlayerNums    []int
	Mode          int
	Index         int
	EmbedURL      string

	AverageResponseLatency time.Duration
	TotalPullRequests      int
	TotalIssues            int

	Collection  triage.Collection
	Collections []triage.Collection

	CollectionResult *triage.CollectionResult
	Stats            *triage.CollectionResult
	StatsID          string

	GetVars string
}

// is this request an HTTP refresh?
func isRefresh(r *http.Request) bool {
	cc := r.Header["Cache-Control"]
	if len(cc) == 0 {
		return false
	}
	//	klog.Infof("cc=%s headers=%+v", cc, r.Header)
	return cc[0] == "max-age-0" || cc[0] == "no-cache"
}

// Board shows a stratgy board
func (h *Handlers) Collection() http.HandlerFunc {
	fmap := template.FuncMap{
		"toJS":          toJS,
		"toYAML":        toYAML,
		"toJSfunc":      toJSfunc,
		"toDays":        toDays,
		"HumanDuration": humanDuration,
		"HumanTime":     humanTime,
		"UnixNano":      unixNano,
		"Avatar":        avatar,
	}
	t := template.Must(template.New("collection").Funcs(fmap).ParseFiles(
		filepath.Join(h.baseDir, "collection.tmpl"),
		filepath.Join(h.baseDir, "base.tmpl"),
	))

	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		dataAge := time.Time{}
		id := strings.TrimPrefix(r.URL.Path, "/s/")

		defer func() {
			klog.Infof("Served %q request within %s from data %s old", id, time.Since(start), time.Since(dataAge))
		}()

		playerChoices := []string{"Select a player"}
		players := getInt(r.URL, "players", 1)
		player := getInt(r.URL, "player", 0)
		mode := getInt(r.URL, "mode", 0)
		index := getInt(r.URL, "index", 1)

		for i := 0; i < players; i++ {
			playerChoices = append(playerChoices, fmt.Sprintf("Player %d", i+1))
		}

		playerNums := []int{}
		for i := 0; i < MaxPlayers; i++ {
			playerNums = append(playerNums, i+1)
		}

		klog.Infof("GET %s (%q): %v", r.URL.Path, id, r.Header)
		s, err := h.party.LookupCollection(id)
		if err != nil {
			http.Error(w, fmt.Sprintf("%q not found: old link or typo?", id), http.StatusNotFound)
			klog.Errorf("collection: %v", err)
			return
		}

		sts, err := h.party.ListCollections()
		if err != nil {
			klog.Errorf("collections: %v", err)
			http.Error(w, "list error", http.StatusInternalServerError)
			return
		}

		var result *triage.CollectionResult
		if isRefresh(r) {
			result = h.updater.ForceRefresh(r.Context(), id)
			klog.Infof("refresh %q result: %d items", id, len(result.RuleResults))
		} else {
			result = h.updater.Lookup(r.Context(), id, true)
			if result == nil {
				http.Error(w, fmt.Sprintf("%q no data", id), http.StatusNotFound)
				return
			}
			if result.RuleResults == nil {
				http.Error(w, fmt.Sprintf("%q no outcomes", id), http.StatusNotFound)
				return
			}

			klog.V(2).Infof("lookup %q result: %d items", id, len(result.RuleResults))
		}

		dataAge = result.Time
		warning := ""

		if time.Since(result.Time) > h.warnAge {
			warning = fmt.Sprintf("Serving results from %s ago. Service started %s ago and is downloading new data. Use Shift-Reload to force refresh at any time.", humanDuration(time.Since(result.Time)), humanDuration(time.Since(h.startTime)))
		}

		total := 0
		for _, o := range result.RuleResults {
			total += len(o.Items)
		}

		unique := []*hubbub.Conversation{}
		seen := map[int]bool{}
		for _, o := range result.RuleResults {
			for _, i := range o.Items {
				if !seen[i.ID] {
					unique = append(unique, i)
					seen[i.ID] = true
				}
			}
		}

		if player > 0 && players > 1 {
			result = playerFilter(result, player, players)
		}

		uniqueFiltered := []*hubbub.Conversation{}
		seenFiltered := map[int]bool{}
		for _, o := range result.RuleResults {
			for _, i := range o.Items {
				if !seenFiltered[i.ID] {
					uniqueFiltered = append(uniqueFiltered, i)
					seenFiltered[i.ID] = true
				}
			}
		}

		embedURL := ""
		if mode == 1 {
			searchIndex := 0
			for _, o := range result.RuleResults {
				for _, i := range o.Items {
					searchIndex++
					if searchIndex == index {
						embedURL = i.URL
					}
				}
			}
		}

		getVars := ""
		if players > 0 {
			getVars = fmt.Sprintf("?player=%d&players=%d", player, players)
		}

		p := &Page{
			ID:               s.ID,
			Version:          VERSION,
			SiteName:         h.siteName,
			Title:            s.Name,
			Collection:       s,
			Collections:      sts,
			Description:      s.Description,
			CollectionResult: result,
			Total:            len(unique),
			TotalShown:       len(uniqueFiltered),
			Types:            "Issues",
			PlayerChoices:    playerChoices,
			PlayerNums:       playerNums,
			Player:           player,
			Players:          players,
			Mode:             mode,
			Index:            index,
			EmbedURL:         embedURL,
			Warning:          warning,
			UniqueItems:      uniqueFiltered,
			GetVars:          getVars,
			ResultAge:        time.Since(result.Time),
		}

		for _, s := range sts {
			if s.UsedForStats {
				p.Stats = h.updater.Lookup(r.Context(), s.ID, false)
				p.StatsID = s.ID
			}
		}

		klog.V(2).Infof("page context: %+v", p)
		err = t.ExecuteTemplate(w, "base", p)
		if err != nil {
			klog.Errorf("tmpl: %v", err)
			return
		}
	}
}

// helper to get integers from a URL
func getInt(url *url.URL, key string, fallback int) int {
	vals := url.Query()[key]
	if len(vals) == 1 {
		i, err := strconv.ParseInt(vals[0], 10, 32)
		if err != nil {
			klog.Warningf("bad %s int value: %v", key, vals)
			return fallback
		}
		return int(i)
	}
	return fallback
}

func toYAML(v interface{}) string {
	s, err := yaml.Marshal(v)
	if err != nil {
		return fmt.Sprintf("yaml err: %v", err)
	}
	return string(s)
}

// Acknowledge JS sanitization issues (what data do we trust?)
func toJS(s string) template.JS {
	return template.JS(s)
}

// Acknowledge JS sanitization issues (what data do we trust?)
func toJSfunc(s string) template.JS {
	return template.JS(nonWordRe.ReplaceAllString(s, "_"))
}

func unixNano(t time.Time) int64 {
	return t.UnixNano()
}

func humanDuration(d time.Duration) string {
	return humanTime(time.Now().Add(-d))
}

func toDays(d time.Duration) string {
	return fmt.Sprintf("%0.1fd", d.Hours()/24)
}

func humanTime(t time.Time) string {
	if t.IsZero() {
		return ""
	}
	ds := humanize.Time(t)
	ds = strings.Replace(ds, " ago", "", 1)

	ds = strings.Replace(ds, " minutes", "min", 1)
	ds = strings.Replace(ds, " minute", "min", 1)

	ds = strings.Replace(ds, " hours", "h", 1)
	ds = strings.Replace(ds, " hour", "h", 1)

	ds = strings.Replace(ds, " days", "d", 1)
	ds = strings.Replace(ds, " day", "d", 1)

	ds = strings.Replace(ds, " months", "mo", 1)
	ds = strings.Replace(ds, " month", "mo", 1)

	ds = strings.Replace(ds, " years", "y", 1)
	ds = strings.Replace(ds, " year", "y", 1)

	ds = strings.Replace(ds, " weeks", "wk", 1)
	ds = strings.Replace(ds, " week", "wk", 1)

	return ds
}

func avatar(u *github.User) template.HTML {
	return template.HTML(fmt.Sprintf(`<a href="%s" title="%s"><img src="%s" width="20" height="20"></a>`, u.GetHTMLURL(), u.GetLogin(), u.GetAvatarURL()))
}

// playerFilter filters out results for a particular player
func playerFilter(result *triage.CollectionResult, player int, players int) *triage.CollectionResult {
	klog.Infof("Filtering for player %d of %d ...", player, players)
	os := []*triage.RuleResult{}
	seen := map[string]*triage.Rule{}

	for _, o := range result.RuleResults {
		cs := []*hubbub.Conversation{}

		for _, i := range o.Items {
			if (i.ID % players) == (player - 1) {
				klog.Infof("%d belongs to player %d", i.ID, player)
				cs = append(cs, i)
			}
		}

		os = append(os, triage.SummarizeRuleResult(o.Rule, cs, seen))
	}

	return triage.SummarizeCollectionResult(os)
}
<|endoftext|>