<commit_before>/*
Copyright 2014 The Kubernetes Authors.

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

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

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

package clientcmd

import (
	"errors"
	"fmt"
	"os"
	"reflect"
	"strings"

	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/validation"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

var (
	ErrNoContext   = errors.New("no context chosen")
	ErrEmptyConfig = errors.New("no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
	// message is for consistency with old behavior
	ErrEmptyCluster = errors.New("cluster has no server defined")
)

type errContextNotFound struct {
	ContextName string
}

func (e *errContextNotFound) Error() string {
	return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
}

// IsContextNotFound returns a boolean indicating whether the error is known to
// report that a context was not found
func IsContextNotFound(err error) bool {
	if err == nil {
		return false
	}
	if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
		return true
	}
	return strings.Contains(err.Error(), "context was not found for specified context")
}

// IsEmptyConfig returns true if the provided error indicates the provided configuration
// is empty.
func IsEmptyConfig(err error) bool {
	switch t := err.(type) {
	case errConfigurationInvalid:
		return len(t) == 1 && t[0] == ErrEmptyConfig
	}
	return err == ErrEmptyConfig
}

// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
type errConfigurationInvalid []error

// errConfigurationInvalid implements error and Aggregate
var _ error = errConfigurationInvalid{}
var _ utilerrors.Aggregate = errConfigurationInvalid{}

func newErrConfigurationInvalid(errs []error) error {
	switch len(errs) {
	case 0:
		return nil
	default:
		return errConfigurationInvalid(errs)
	}
}

// Error implements the error interface
func (e errConfigurationInvalid) Error() string {
	return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
}

// Errors implements the utilerrors.Aggregate interface
func (e errConfigurationInvalid) Errors() []error {
	return e
}

// Is implements the utilerrors.Aggregate interface
func (e errConfigurationInvalid) Is(target error) bool {
	return e.visit(func(err error) bool {
		return errors.Is(err, target)
	})
}

func (e errConfigurationInvalid) visit(f func(err error) bool) bool {
	for _, err := range e {
		switch err := err.(type) {
		case errConfigurationInvalid:
			if match := err.visit(f); match {
				return match
			}
		case utilerrors.Aggregate:
			for _, nestedErr := range err.Errors() {
				if match := f(nestedErr); match {
					return match
				}
			}
		default:
			if match := f(err); match {
				return match
			}
		}
	}

	return false
}

// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
func IsConfigurationInvalid(err error) bool {
	switch err.(type) {
	case *errContextNotFound, errConfigurationInvalid:
		return true
	}
	return IsContextNotFound(err)
}

// Validate checks for errors in the Config.  It does not return early so that it can find as many errors as possible.
func Validate(config clientcmdapi.Config) error {
	validationErrors := make([]error, 0)

	if clientcmdapi.IsConfigEmpty(&config) {
		return newErrConfigurationInvalid([]error{ErrEmptyConfig})
	}

	if len(config.CurrentContext) != 0 {
		if _, exists := config.Contexts[config.CurrentContext]; !exists {
			validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
		}
	}

	for contextName, context := range config.Contexts {
		validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
	}

	for authInfoName, authInfo := range config.AuthInfos {
		validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...)
	}

	for clusterName, clusterInfo := range config.Clusters {
		validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...)
	}

	return newErrConfigurationInvalid(validationErrors)
}

// ConfirmUsable looks a particular context and determines if that particular part of the config is useable.  There might still be errors in the config,
// but no errors in the sections requested or referenced.  It does not return early so that it can find as many errors as possible.
func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
	validationErrors := make([]error, 0)

	if clientcmdapi.IsConfigEmpty(&config) {
		return newErrConfigurationInvalid([]error{ErrEmptyConfig})
	}

	var contextName string
	if len(passedContextName) != 0 {
		contextName = passedContextName
	} else {
		contextName = config.CurrentContext
	}

	if len(contextName) == 0 {
		return ErrNoContext
	}

	context, exists := config.Contexts[contextName]
	if !exists {
		validationErrors = append(validationErrors, &errContextNotFound{contextName})
	}

	if exists {
		validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
		validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...)
		validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...)
	}

	return newErrConfigurationInvalid(validationErrors)
}

// validateClusterInfo looks for conflicts and errors in the cluster info
func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error {
	validationErrors := make([]error, 0)

	emptyCluster := clientcmdapi.NewCluster()
	if reflect.DeepEqual(*emptyCluster, clusterInfo) {
		return []error{ErrEmptyCluster}
	}

	if len(clusterInfo.Server) == 0 {
		if len(clusterName) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
		} else {
			validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
		}
	}
	// Make sure CA data and CA file aren't both specified
	if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
		validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
	}
	if len(clusterInfo.CertificateAuthority) != 0 {
		clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
		if err != nil {
			validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
		} else {
			defer clientCertCA.Close()
		}
	}

	return validationErrors
}

// validateAuthInfo looks for conflicts and errors in the auth info
func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
	validationErrors := make([]error, 0)

	usingAuthPath := false
	methods := make([]string, 0, 3)
	if len(authInfo.Token) != 0 {
		methods = append(methods, "token")
	}
	if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
		methods = append(methods, "basicAuth")
	}

	if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
		// Make sure cert data and file aren't both specified
		if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
		}
		// Make sure key data and file aren't both specified
		if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
		}
		// Make sure a key is specified
		if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
		}

		if len(authInfo.ClientCertificate) != 0 {
			clientCertFile, err := os.Open(authInfo.ClientCertificate)
			if err != nil {
				validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
			} else {
				defer clientCertFile.Close()
			}
		}
		if len(authInfo.ClientKey) != 0 {
			clientKeyFile, err := os.Open(authInfo.ClientKey)
			if err != nil {
				validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
			} else {
				defer clientKeyFile.Close()
			}
		}
	}

	if authInfo.Exec != nil {
		if authInfo.AuthProvider != nil {
			validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName))
		}
		if len(authInfo.Exec.Command) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName))
		}
		if len(authInfo.Exec.APIVersion) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName))
		}
		for _, v := range authInfo.Exec.Env {
			if len(v.Name) == 0 {
				validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName))
			}
		}
	}

	// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
	if (len(methods) > 1) && (!usingAuthPath) {
		validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
	}

	// ImpersonateGroups or ImpersonateUserExtra should be requested with a user
	if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) {
		validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName))
	}
	return validationErrors
}

// validateContext looks for errors in the context.  It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
	validationErrors := make([]error, 0)

	if len(contextName) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context))
	}

	if len(context.AuthInfo) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
	} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
		validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
	}

	if len(context.Cluster) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
	} else if _, exists := config.Clusters[context.Cluster]; !exists {
		validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
	}

	if len(context.Namespace) != 0 {
		if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
		}
	}

	return validationErrors
}
<commit_msg>Fix kubectl version should print version info<commit_after>/*
Copyright 2014 The Kubernetes Authors.

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

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

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

package clientcmd

import (
	"errors"
	"fmt"
	"os"
	"reflect"
	"strings"

	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/validation"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

var (
	ErrNoContext   = errors.New("no context chosen")
	ErrEmptyConfig = NewEmptyConfigError("no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
	// message is for consistency with old behavior
	ErrEmptyCluster = errors.New("cluster has no server defined")
)

// NewEmptyConfigError returns an error wrapping the given message which IsEmptyConfig() will recognize as an empty config error
func NewEmptyConfigError(message string) error {
	return &errEmptyConfig{message}
}

type errEmptyConfig struct {
	message string
}

func (e *errEmptyConfig) Error() string {
	return e.message
}

type errContextNotFound struct {
	ContextName string
}

func (e *errContextNotFound) Error() string {
	return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
}

// IsContextNotFound returns a boolean indicating whether the error is known to
// report that a context was not found
func IsContextNotFound(err error) bool {
	if err == nil {
		return false
	}
	if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
		return true
	}
	return strings.Contains(err.Error(), "context was not found for specified context")
}

// IsEmptyConfig returns true if the provided error indicates the provided configuration
// is empty.
func IsEmptyConfig(err error) bool {
	switch t := err.(type) {
	case errConfigurationInvalid:
		if len(t) != 1 {
			return false
		}
		_, ok := t[0].(*errEmptyConfig)
		return ok
	}
	_, ok := err.(*errEmptyConfig)
	return ok
}

// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
type errConfigurationInvalid []error

// errConfigurationInvalid implements error and Aggregate
var _ error = errConfigurationInvalid{}
var _ utilerrors.Aggregate = errConfigurationInvalid{}

func newErrConfigurationInvalid(errs []error) error {
	switch len(errs) {
	case 0:
		return nil
	default:
		return errConfigurationInvalid(errs)
	}
}

// Error implements the error interface
func (e errConfigurationInvalid) Error() string {
	return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
}

// Errors implements the utilerrors.Aggregate interface
func (e errConfigurationInvalid) Errors() []error {
	return e
}

// Is implements the utilerrors.Aggregate interface
func (e errConfigurationInvalid) Is(target error) bool {
	return e.visit(func(err error) bool {
		return errors.Is(err, target)
	})
}

func (e errConfigurationInvalid) visit(f func(err error) bool) bool {
	for _, err := range e {
		switch err := err.(type) {
		case errConfigurationInvalid:
			if match := err.visit(f); match {
				return match
			}
		case utilerrors.Aggregate:
			for _, nestedErr := range err.Errors() {
				if match := f(nestedErr); match {
					return match
				}
			}
		default:
			if match := f(err); match {
				return match
			}
		}
	}

	return false
}

// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
func IsConfigurationInvalid(err error) bool {
	switch err.(type) {
	case *errContextNotFound, errConfigurationInvalid:
		return true
	}
	return IsContextNotFound(err)
}

// Validate checks for errors in the Config.  It does not return early so that it can find as many errors as possible.
func Validate(config clientcmdapi.Config) error {
	validationErrors := make([]error, 0)

	if clientcmdapi.IsConfigEmpty(&config) {
		return newErrConfigurationInvalid([]error{ErrEmptyConfig})
	}

	if len(config.CurrentContext) != 0 {
		if _, exists := config.Contexts[config.CurrentContext]; !exists {
			validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
		}
	}

	for contextName, context := range config.Contexts {
		validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
	}

	for authInfoName, authInfo := range config.AuthInfos {
		validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...)
	}

	for clusterName, clusterInfo := range config.Clusters {
		validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...)
	}

	return newErrConfigurationInvalid(validationErrors)
}

// ConfirmUsable looks a particular context and determines if that particular part of the config is useable.  There might still be errors in the config,
// but no errors in the sections requested or referenced.  It does not return early so that it can find as many errors as possible.
func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
	validationErrors := make([]error, 0)

	if clientcmdapi.IsConfigEmpty(&config) {
		return newErrConfigurationInvalid([]error{ErrEmptyConfig})
	}

	var contextName string
	if len(passedContextName) != 0 {
		contextName = passedContextName
	} else {
		contextName = config.CurrentContext
	}

	if len(contextName) == 0 {
		return ErrNoContext
	}

	context, exists := config.Contexts[contextName]
	if !exists {
		validationErrors = append(validationErrors, &errContextNotFound{contextName})
	}

	if exists {
		validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
		validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...)
		validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...)
	}

	return newErrConfigurationInvalid(validationErrors)
}

// validateClusterInfo looks for conflicts and errors in the cluster info
func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error {
	validationErrors := make([]error, 0)

	emptyCluster := clientcmdapi.NewCluster()
	if reflect.DeepEqual(*emptyCluster, clusterInfo) {
		return []error{ErrEmptyCluster}
	}

	if len(clusterInfo.Server) == 0 {
		if len(clusterName) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
		} else {
			validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
		}
	}
	// Make sure CA data and CA file aren't both specified
	if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
		validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
	}
	if len(clusterInfo.CertificateAuthority) != 0 {
		clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
		if err != nil {
			validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
		} else {
			defer clientCertCA.Close()
		}
	}

	return validationErrors
}

// validateAuthInfo looks for conflicts and errors in the auth info
func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
	validationErrors := make([]error, 0)

	usingAuthPath := false
	methods := make([]string, 0, 3)
	if len(authInfo.Token) != 0 {
		methods = append(methods, "token")
	}
	if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
		methods = append(methods, "basicAuth")
	}

	if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
		// Make sure cert data and file aren't both specified
		if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
		}
		// Make sure key data and file aren't both specified
		if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
		}
		// Make sure a key is specified
		if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
		}

		if len(authInfo.ClientCertificate) != 0 {
			clientCertFile, err := os.Open(authInfo.ClientCertificate)
			if err != nil {
				validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
			} else {
				defer clientCertFile.Close()
			}
		}
		if len(authInfo.ClientKey) != 0 {
			clientKeyFile, err := os.Open(authInfo.ClientKey)
			if err != nil {
				validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
			} else {
				defer clientKeyFile.Close()
			}
		}
	}

	if authInfo.Exec != nil {
		if authInfo.AuthProvider != nil {
			validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName))
		}
		if len(authInfo.Exec.Command) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName))
		}
		if len(authInfo.Exec.APIVersion) == 0 {
			validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName))
		}
		for _, v := range authInfo.Exec.Env {
			if len(v.Name) == 0 {
				validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName))
			}
		}
	}

	// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
	if (len(methods) > 1) && (!usingAuthPath) {
		validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
	}

	// ImpersonateGroups or ImpersonateUserExtra should be requested with a user
	if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) {
		validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName))
	}
	return validationErrors
}

// validateContext looks for errors in the context.  It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
	validationErrors := make([]error, 0)

	if len(contextName) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context))
	}

	if len(context.AuthInfo) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
	} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
		validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
	}

	if len(context.Cluster) == 0 {
		validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
	} else if _, exists := config.Clusters[context.Cluster]; !exists {
		validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
	}

	if len(context.Namespace) != 0 {
		if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
			validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
		}
	}

	return validationErrors
}
<|endoftext|>


<commit_before>// Binary parse_test_output parses the `go test -test.v` output written to
// os.Stdin and prints a basic JSON representation of the test results.
package main

import (
	"bufio"
	"encoding/json"
	"encoding/xml"
	"flag"
	"io"
	"log"
	"os"
	"regexp"
	"strings"
	"time"

	"tools/go/testsuite"

	"golang.org/x/tools/benchmark/parse"
)

type status string

// Test case result kinds
const (
	Pass status = "PASS"
	Fail        = "FAIL"
	Skip        = "SKIP"
)

type testCase struct {
	Name     string
	Duration time.Duration `json:",omitempty"`
	Status   status

	Log []string `json:",omitempty"`
}

const logPrefix = "\t"

var testCaseRE = regexp.MustCompile(`^--- (PASS|FAIL|SKIP): ([^ ]+) \((.+)\)$`)

var (
	outputFile = flag.String("out", "/dev/stdout", "Path of file to write test results as XML/JSON")
	format     = flag.String("format", "json", "Format to write results (supported: json or xml)")
)

func main() {
	flag.Parse()
	if *format != "xml" && *format != "json" {
		log.Fatalf("Unsupported output format: %q", *format)
	}

	r, w, err := os.Pipe()
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		if _, err := io.Copy(io.MultiWriter(w, os.Stderr), os.Stdin); err != nil {
			log.Fatal(err)
		}
		if err := w.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	var (
		results  []*testCase
		lastTest *testCase

		benchmarks []*parse.Benchmark
	)

	s := bufio.NewScanner(r)
	for s.Scan() {
		if err := s.Err(); err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}

		ss := testCaseRE.FindStringSubmatch(s.Text())
		if len(ss) > 0 {
			dur, err := time.ParseDuration(ss[3])
			if err != nil {
				log.Fatal(err)
			}
			lastTest = &testCase{
				Name:     ss[2],
				Duration: dur,
				Status:   status(ss[1]),
			}
			results = append(results, lastTest)
		} else if lastTest != nil && strings.HasPrefix(s.Text(), logPrefix) {
			lastTest.Log = append(lastTest.Log, strings.TrimPrefix(s.Text(), logPrefix))
		} else {
			lastTest = nil

			if strings.HasPrefix(s.Text(), "Benchmark") {
				b, err := parse.ParseLine(s.Text())
				if err != nil {
					log.Fatal(err)
				}
				benchmarks = append(benchmarks, b)
			}
		}
	}

	f, err := os.Create(*outputFile)
	if err != nil {
		log.Fatalf("Failure to create output file %q: %v", *outputFile, err)
	}

	switch *format {
	case "xml":
		if err := xml.NewEncoder(f).EncodeElement(makeTestSuite(results, benchmarks), xml.StartElement{
			Name: xml.Name{Local: "testsuite"},
		}); err != nil {
			log.Fatalf("Error encoding XML: %v", err)
		}
	case "json":
		if err := json.NewEncoder(f).Encode(struct {
			Tests      []*testCase
			Benchmarks []*parse.Benchmark `json:",omitempty"`
		}{results, benchmarks}); err != nil {
			log.Fatalf("Error encoding JSON: %v", err)
		}
	default:
		panic("unsupported output format: " + *format)
	}

	if err := f.Close(); err != nil {
		log.Fatalf("Error closing output file %q: %v", *outputFile, err)
	}
}

func makeTestSuite(tests []*testCase, benchmarks []*parse.Benchmark) *testsuite.Suite {
	s := &testsuite.Suite{}
	var totalTime time.Duration
	for _, t := range tests {
		totalTime += t.Duration
		c := testsuite.TestCase{
			Name: t.Name,
			Time: durationToMillis(t.Duration),
		}
		switch t.Status {
		case Skip:
			c.Status = testsuite.NotRun
		case Pass:
			c.Status = testsuite.Run
		case Fail:
			c.Status = testsuite.Run
			c.Errors = append(c.Errors, testsuite.TestError{
				Content: strings.Join(t.Log, "\n"),
			})
		default:
			log.Printf("Unknown test case status: %q", t.Status)
		}
		s.TestCase = append(s.TestCase, c)
	}
	s.Time = durationToMillis(totalTime)
	return s
}

func durationToMillis(d time.Duration) float64 {
	return float64(d.Nanoseconds()) / float64(time.Millisecond)
}
<commit_msg>Add fix in parse_test_output for older Go test format<commit_after>// Binary parse_test_output parses the `go test -test.v` output written to
// os.Stdin and prints a basic JSON representation of the test results.
package main

import (
	"bufio"
	"encoding/json"
	"encoding/xml"
	"flag"
	"io"
	"log"
	"os"
	"regexp"
	"strings"
	"time"

	"tools/go/testsuite"

	"golang.org/x/tools/benchmark/parse"
)

type status string

// Test case result kinds
const (
	Pass status = "PASS"
	Fail        = "FAIL"
	Skip        = "SKIP"
)

type testCase struct {
	Name     string
	Duration time.Duration `json:",omitempty"`
	Status   status

	Log []string `json:",omitempty"`
}

const logPrefix = "\t"

var testCaseRE = regexp.MustCompile(`^--- (PASS|FAIL|SKIP): ([^ ]+) \((.+)\)$`)

var (
	outputFile = flag.String("out", "/dev/stdout", "Path of file to write test results as XML/JSON")
	format     = flag.String("format", "json", "Format to write results (supported: json or xml)")
)

func main() {
	flag.Parse()
	if *format != "xml" && *format != "json" {
		log.Fatalf("Unsupported output format: %q", *format)
	}

	r, w, err := os.Pipe()
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		if _, err := io.Copy(io.MultiWriter(w, os.Stderr), os.Stdin); err != nil {
			log.Fatal(err)
		}
		if err := w.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	var (
		results  []*testCase
		lastTest *testCase

		benchmarks []*parse.Benchmark
	)

	s := bufio.NewScanner(r)
	for s.Scan() {
		if err := s.Err(); err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}

		ss := testCaseRE.FindStringSubmatch(s.Text())
		if len(ss) > 0 {
			if fixed := strings.TrimSuffix(ss[3], " seconds"); fixed != ss[3] {
				// Before
				// https://github.com/golang/go/commit/0e92b538a9f6e337b8e48f47f38803e8245c03cc,
				// this duration format wasn't compatible with time.ParseDuration.  This
				// works around the difference.
				ss[3] = fixed + "s"
			}
			dur, err := time.ParseDuration(ss[3])
			if err != nil {
				log.Fatal(err)
			}
			lastTest = &testCase{
				Name:     ss[2],
				Duration: dur,
				Status:   status(ss[1]),
			}
			results = append(results, lastTest)
		} else if lastTest != nil && strings.HasPrefix(s.Text(), logPrefix) {
			lastTest.Log = append(lastTest.Log, strings.TrimPrefix(s.Text(), logPrefix))
		} else {
			lastTest = nil

			if strings.HasPrefix(s.Text(), "Benchmark") {
				b, err := parse.ParseLine(s.Text())
				if err != nil {
					log.Fatal(err)
				}
				benchmarks = append(benchmarks, b)
			}
		}
	}

	f, err := os.Create(*outputFile)
	if err != nil {
		log.Fatalf("Failure to create output file %q: %v", *outputFile, err)
	}

	switch *format {
	case "xml":
		if err := xml.NewEncoder(f).EncodeElement(makeTestSuite(results, benchmarks), xml.StartElement{
			Name: xml.Name{Local: "testsuite"},
		}); err != nil {
			log.Fatalf("Error encoding XML: %v", err)
		}
	case "json":
		if err := json.NewEncoder(f).Encode(struct {
			Tests      []*testCase
			Benchmarks []*parse.Benchmark `json:",omitempty"`
		}{results, benchmarks}); err != nil {
			log.Fatalf("Error encoding JSON: %v", err)
		}
	default:
		panic("unsupported output format: " + *format)
	}

	if err := f.Close(); err != nil {
		log.Fatalf("Error closing output file %q: %v", *outputFile, err)
	}
}

func makeTestSuite(tests []*testCase, benchmarks []*parse.Benchmark) *testsuite.Suite {
	s := &testsuite.Suite{}
	var totalTime time.Duration
	for _, t := range tests {
		totalTime += t.Duration
		c := testsuite.TestCase{
			Name: t.Name,
			Time: durationToMillis(t.Duration),
		}
		switch t.Status {
		case Skip:
			c.Status = testsuite.NotRun
		case Pass:
			c.Status = testsuite.Run
		case Fail:
			c.Status = testsuite.Run
			c.Errors = append(c.Errors, testsuite.TestError{
				Content: strings.Join(t.Log, "\n"),
			})
		default:
			log.Printf("Unknown test case status: %q", t.Status)
		}
		s.TestCase = append(s.TestCase, c)
	}
	s.Time = durationToMillis(totalTime)
	return s
}

func durationToMillis(d time.Duration) float64 {
	return float64(d.Nanoseconds()) / float64(time.Millisecond)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/xml"

	"github.com/mdigger/mx"
)

// Conference описывает параметры конференции.
type Conference struct {
	ID              string `xml:"confId" json:"Id"`
	OwnerID         mx.JID `xml:"ownerId" json:"ownerId"`
	Name            string `xml:"name" json:"name"`
	AccessID        int64  `xml:"accessId" json:"accessId"`
	Description     string `xml:"description" json:"description,omitempty"`
	Type            string `xml:"type" json:"type"`
	StartDate       int64  `xml:"startDate" json:"startDate"`
	Duration        int64  `xml:"duration" json:"duration"`
	WaitForOwner    bool   `xml:"waitForOwner" json:"waitForOwner"`
	DelOnOwnerLeave bool   `xml:"delOnOwnerLeave" json:"delOnOwnerLeave"`
	Ws              string `xml:"ws" json:"ws"`
	WsType          string `xml:"wsType" json:"wsType"`
}

// ConferenceCreate инициализирует новую конференцию.
func (c *MXConn) ConferenceCreate(params *Conference) (*Conference, error) {
	if _, err := c.SendWithResponse(&struct {
		XMLName xml.Name `xml:"CreateConference"`
		*Conference
	}{
		Conference: params,
	}); err != nil {
		return nil, err
	}
	err := c.HandleWait(func(resp *mx.Response) error {
		if err := resp.Decode(params); err != nil {
			return err
		}
		return mx.Stop
	}, mx.ReadTimeout, "ConfAddEvent")
	if err != nil {
		return nil, err
	}
	return params, nil
}

// ConferenceUpdate изменяет информацю о конференции.
func (c *MXConn) ConferenceUpdate(params *Conference) (*Conference, error) {
	if _, err := c.SendWithResponse(&struct {
		XMLName xml.Name `xml:"UpdateConference"`
		*Conference
	}{
		Conference: params,
	}); err != nil {
		return nil, err
	}
	err := c.HandleWait(func(resp *mx.Response) error {
		if err := resp.Decode(params); err != nil {
			return err
		}
		return mx.Stop
	}, mx.ReadTimeout, "ConfUpdEvent")
	if err != nil {
		return nil, err
	}
	return params, nil
}
<commit_msg>DeleteConference<commit_after>package main

import (
	"encoding/xml"

	"github.com/mdigger/mx"
)

// Conference описывает параметры конференции.
type Conference struct {
	ID              string `xml:"confId" json:"Id"`
	OwnerID         mx.JID `xml:"ownerId" json:"ownerId"`
	Name            string `xml:"name" json:"name"`
	AccessID        int64  `xml:"accessId" json:"accessId"`
	Description     string `xml:"description" json:"description,omitempty"`
	Type            string `xml:"type" json:"type"`
	StartDate       int64  `xml:"startDate" json:"startDate"`
	Duration        int64  `xml:"duration" json:"duration"`
	WaitForOwner    bool   `xml:"waitForOwner" json:"waitForOwner"`
	DelOnOwnerLeave bool   `xml:"delOnOwnerLeave" json:"delOnOwnerLeave"`
	Ws              string `xml:"ws" json:"ws"`
	WsType          string `xml:"wsType" json:"wsType"`
}

// ConferenceCreate инициализирует новую конференцию.
func (c *MXConn) ConferenceCreate(params *Conference) (*Conference, error) {
	if _, err := c.SendWithResponse(&struct {
		XMLName xml.Name `xml:"CreateConference"`
		*Conference
	}{
		Conference: params,
	}); err != nil {
		return nil, err
	}
	err := c.HandleWait(func(resp *mx.Response) error {
		if err := resp.Decode(params); err != nil {
			return err
		}
		return mx.Stop
	}, mx.ReadTimeout, "ConfAddEvent")
	if err != nil {
		return nil, err
	}
	return params, nil
}

// ConferenceUpdate изменяет информацю о конференции.
func (c *MXConn) ConferenceUpdate(params *Conference) (*Conference, error) {
	if _, err := c.SendWithResponse(&struct {
		XMLName xml.Name `xml:"UpdateConference"`
		*Conference
	}{
		Conference: params,
	}); err != nil {
		return nil, err
	}
	err := c.HandleWait(func(resp *mx.Response) error {
		if err := resp.Decode(params); err != nil {
			return err
		}
		return mx.Stop
	}, mx.ReadTimeout, "ConfUpdEvent")
	if err != nil {
		return nil, err
	}
	return params, nil
}

// ConferenceDelete удаляет конфиренцию.
func (c *MXConn) ConferenceDelete(id string) error {
	if _, err := c.SendWithResponse(&struct {
		XMLName xml.Name `xml:"DeleteConference"`
		ID      string   `xml:"confId"`
	}{
		ID: id,
	}); err != nil {
		return err
	}
	return c.HandleWait(func(resp *mx.Response) error {
		return mx.Stop
	}, mx.ReadTimeout, "ConfDelEvent")
}
<|endoftext|>


<commit_before>package cache

import (
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/ichiban/jesi/transaction"
)

// Store stores pairs of requests/responses.
type Store struct {
	sync.RWMutex
	Resources       map[ResourceKey]*Resource
	Max             uint64
	InUse           uint64
	Sample          uint
	OriginChangedAt time.Time
}

// Set inserts/updates a new pair of request/response to the cache.
func (s *Store) Set(req *http.Request, rep *Representation) {
	s.init()

	s.Lock()
	defer s.Unlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		res = NewResource(rep)
		s.Resources[resKey] = res
	}

	repKey := NewRepresentationKey(res, req)
	if old, ok := res.Representations[repKey]; ok {
		log.WithFields(log.Fields{
			"id": old.ID,
		}).Info("Removed a representation")
	}
	res.Representations[repKey] = rep

	log.WithFields(log.Fields{
		"id":          rep.ID,
		"transaction": transaction.ID(req),
	}).Info("Added a representation")

	s.InUse += uint64(len(rep.Body))

	if s.Max == 0 {
		return
	}

	for s.InUse > s.Max {
		s.evict()
	}
}

func (s *Store) evict() {
	var minResKey *ResourceKey
	var minRepKey *RepresentationKey
	var minRep *Representation
	var i uint
	for resKey, res := range s.Resources {
		if i >= s.Sample {
			break
		}

		// the 1st representation in the resource
		var repKey RepresentationKey
		var rep *Representation
		for repKey, rep = range res.Representations {
			break
		}

		if minResKey != nil && minRepKey != nil && Less(minRep, rep) {
			continue
		}

		resKey := resKey
		minResKey = &resKey
		minRepKey = &repKey
		minRep = rep

		i++
	}

	if minResKey == nil || minRepKey == nil {
		return
	}

	res, ok := s.Resources[*minResKey]
	if !ok {
		return
	}
	defer func() {
		if len(res.Representations) == 0 {
			delete(s.Resources, *minResKey)
		}
	}()

	delete(res.Representations, *minRepKey)
	s.InUse -= uint64(len(minRep.Body))

	log.WithFields(log.Fields{
		"id": minRep.ID,
	}).Info("Removed a representation")
}

// Get retrieves a cached response.
func (s *Store) Get(req *http.Request) *Representation {
	s.init()

	s.RLock()
	defer s.RUnlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		return nil
	}

	repKey := NewRepresentationKey(res, req)
	rep, ok := res.Representations[repKey]
	if !ok {
		return nil
	}

	rep.LastUsedTime = time.Now()

	log.WithFields(log.Fields{
		"id": rep.ID,
	}).Info("Get a representation")

	return rep.clone()
}

// Purge removes any representations associated to the request.
func (s *Store) Purge(req *http.Request) {
	s.init()

	s.RLock()
	defer s.RUnlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		return
	}
	delete(s.Resources, resKey)

	for _, rep := range res.Representations {
		log.WithFields(log.Fields{
			"id": rep.ID,
		}).Info("Removed a representation")

		s.InUse -= uint64(len(rep.Body))
	}
}

func (s *Store) init() {
	s.Lock()
	defer s.Unlock()

	if s.Resources == nil {
		s.Resources = make(map[ResourceKey]*Resource)
	}
}

// ResourceKey identifies resources with the same URL.
type ResourceKey struct {
	Host  string `json:"host"`
	Path  string `json:"path"`
	Query string `json:"query"`
}

// NewResourceKey returns a resource key of the request.
func NewResourceKey(req *http.Request) ResourceKey {
	return ResourceKey{
		Host:  req.URL.Host,
		Path:  req.URL.Path,
		Query: req.URL.Query().Encode(),
	}
}

// Resource represents cached responses with the same URL.
type Resource struct {
	Fields          []string
	Representations map[RepresentationKey]*Representation
}

// NewResource constructs a new variations for the cached response.
func NewResource(rep *Representation) *Resource {
	var fields []string

	for _, vary := range rep.HeaderMap["Vary"] {
		for _, field := range strings.Split(vary, ",") {
			if field == "*" {
				return nil
			}
			fields = append(fields, http.CanonicalHeaderKey(strings.Trim(field, " ")))
		}
	}

	res := &Resource{
		Fields:          fields,
		Representations: make(map[RepresentationKey]*Representation),
	}

	return res
}

// RepresentationKey identifies a cached response in variations.
type RepresentationKey struct {
	Method string
	Key    string
}

// NewRepresentationKey constructs a new variation key from a request.
func NewRepresentationKey(res *Resource, req *http.Request) RepresentationKey {
	var keys []string
	for _, fields := range res.Fields {
		fields := strings.Split(fields, ",")
		for _, field := range fields {
			keys = append(keys, strings.TrimSpace(field))
		}
	}

	vals := url.Values{}
	for _, key := range keys {
		var values []string
		for _, vals := range req.Header[key] {
			vals := strings.Split(vals, ",")
			for _, val := range vals {
				values = append(values, strings.TrimSpace(val))
			}
		}

		sort.Strings(values)
		for _, val := range values {
			vals.Add(key, val)
		}
	}

	return RepresentationKey{
		Method: req.Method,
		Key:    vals.Encode(),
	}
}

type key struct {
	resource       *ResourceKey
	representation *RepresentationKey
}

// CachedState represents freshness of a cached response.
type CachedState int

const (
	// Miss means it's not in the cache.
	Miss CachedState = iota

	// Fresh means it has a cached response and it's available.
	Fresh

	// Stale means it has a cached response but it's not recommended.
	Stale

	// Revalidate means it has a cached response but needs confirmation from the backend.
	Revalidate
)

// State returns the state of cached response.
func (s *Store) State(req *http.Request, cached *Representation) (CachedState, time.Duration) {
	if cached == nil {
		return Miss, time.Duration(0)
	}

	cached.RLock()
	defer cached.RUnlock()

	if contains(req.Header, pragmaField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if contains(req.Header, cacheControlField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if contains(cached.HeaderMap, cacheControlField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if lifetime, ok := freshnessLifetime(cached); ok {
		age := currentAge(cached)

		// cached responses before the last destructive requests (e.g. POST) are considered outdated.
		if time.Since(s.OriginChangedAt) <= age {
			return Revalidate, time.Duration(0)
		}

		delta := age - lifetime
		if lifetime > age {
			return Fresh, delta
		}

		if contains(cached.HeaderMap, cacheControlField, revalidatePattern) {
			return Revalidate, time.Duration(0)
		}

		return Stale, delta
	}

	return Revalidate, time.Duration(0)
}

func (s CachedState) String() string {
	switch s {
	case Miss:
		return "miss"
	case Fresh:
		return "fresh"
	case Stale:
		return "stale"
	case Revalidate:
		return "revalidate"
	default:
		return "unknown"
	}
}

func Less(a, b *Representation) bool {
	return a.LastUsedTime.Before(b.LastUsedTime)
}
<commit_msg>reduce complexity<commit_after>package cache

import (
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/ichiban/jesi/transaction"
)

// Store stores pairs of requests/responses.
type Store struct {
	sync.RWMutex
	Resources       map[ResourceKey]*Resource
	Max             uint64
	InUse           uint64
	Sample          uint
	OriginChangedAt time.Time
}

// Set inserts/updates a new pair of request/response to the cache.
func (s *Store) Set(req *http.Request, rep *Representation) {
	s.init()

	s.Lock()
	defer s.Unlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		res = NewResource(rep)
		s.Resources[resKey] = res
	}

	repKey := NewRepresentationKey(res, req)
	if old, ok := res.Representations[repKey]; ok {
		log.WithFields(log.Fields{
			"id": old.ID,
		}).Info("Removed a representation")
	}
	res.Representations[repKey] = rep

	log.WithFields(log.Fields{
		"id":          rep.ID,
		"transaction": transaction.ID(req),
	}).Info("Added a representation")

	s.InUse += uint64(len(rep.Body))

	if s.Max == 0 {
		return
	}

	for s.InUse > s.Max {
		s.evict()
	}
}

func (s *Store) evict() {
	var minResKey ResourceKey
	var minRepKey RepresentationKey
	var minRep *Representation
	i := s.Sample
	for resKey, res := range s.Resources {
		if i == 0 {
			break
		}

		// the 1st representation in the resource
		var repKey RepresentationKey
		var rep *Representation
		for repKey, rep = range res.Representations {
			break
		}

		if minRep != nil && less(minRep, rep) {
			continue
		}

		minResKey = resKey
		minRepKey = repKey
		minRep = rep

		i--
	}

	res, ok := s.Resources[minResKey]
	if !ok {
		return
	}
	defer func() {
		if len(res.Representations) == 0 {
			delete(s.Resources, minResKey)
		}
	}()

	delete(res.Representations, minRepKey)
	s.InUse -= uint64(len(minRep.Body))

	log.WithFields(log.Fields{
		"id": minRep.ID,
	}).Info("Removed a representation")
}

// Get retrieves a cached response.
func (s *Store) Get(req *http.Request) *Representation {
	s.init()

	s.RLock()
	defer s.RUnlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		return nil
	}

	repKey := NewRepresentationKey(res, req)
	rep, ok := res.Representations[repKey]
	if !ok {
		return nil
	}

	rep.Lock()
	rep.LastUsedTime = time.Now()
	rep.Unlock()

	log.WithFields(log.Fields{
		"id": rep.ID,
	}).Info("Get a representation")

	return rep.clone()
}

// Purge removes any representations associated to the request.
func (s *Store) Purge(req *http.Request) {
	s.init()

	s.RLock()
	defer s.RUnlock()

	resKey := NewResourceKey(req)
	res, ok := s.Resources[resKey]
	if !ok {
		return
	}
	delete(s.Resources, resKey)

	for _, rep := range res.Representations {
		log.WithFields(log.Fields{
			"id": rep.ID,
		}).Info("Removed a representation")

		s.InUse -= uint64(len(rep.Body))
	}
}

func (s *Store) init() {
	s.Lock()
	defer s.Unlock()

	if s.Resources == nil {
		s.Resources = make(map[ResourceKey]*Resource)
	}
}

// ResourceKey identifies resources with the same URL.
type ResourceKey struct {
	Host  string `json:"host"`
	Path  string `json:"path"`
	Query string `json:"query"`
}

// NewResourceKey returns a resource key of the request.
func NewResourceKey(req *http.Request) ResourceKey {
	return ResourceKey{
		Host:  req.URL.Host,
		Path:  req.URL.Path,
		Query: req.URL.Query().Encode(),
	}
}

// Resource represents cached responses with the same URL.
type Resource struct {
	Fields          []string
	Representations map[RepresentationKey]*Representation
}

// NewResource constructs a new variations for the cached response.
func NewResource(rep *Representation) *Resource {
	var fields []string

	for _, vary := range rep.HeaderMap["Vary"] {
		for _, field := range strings.Split(vary, ",") {
			if field == "*" {
				return nil
			}
			fields = append(fields, http.CanonicalHeaderKey(strings.Trim(field, " ")))
		}
	}

	res := &Resource{
		Fields:          fields,
		Representations: make(map[RepresentationKey]*Representation),
	}

	return res
}

// RepresentationKey identifies a cached response in variations.
type RepresentationKey struct {
	Method string
	Key    string
}

// NewRepresentationKey constructs a new variation key from a request.
func NewRepresentationKey(res *Resource, req *http.Request) RepresentationKey {
	var keys []string
	for _, fields := range res.Fields {
		fields := strings.Split(fields, ",")
		for _, field := range fields {
			keys = append(keys, strings.TrimSpace(field))
		}
	}

	vals := url.Values{}
	for _, key := range keys {
		var values []string
		for _, vals := range req.Header[key] {
			vals := strings.Split(vals, ",")
			for _, val := range vals {
				values = append(values, strings.TrimSpace(val))
			}
		}

		sort.Strings(values)
		for _, val := range values {
			vals.Add(key, val)
		}
	}

	return RepresentationKey{
		Method: req.Method,
		Key:    vals.Encode(),
	}
}

type key struct {
	resource       *ResourceKey
	representation *RepresentationKey
}

// CachedState represents freshness of a cached response.
type CachedState int

const (
	// Miss means it's not in the cache.
	Miss CachedState = iota

	// Fresh means it has a cached response and it's available.
	Fresh

	// Stale means it has a cached response but it's not recommended.
	Stale

	// Revalidate means it has a cached response but needs confirmation from the backend.
	Revalidate
)

// State returns the state of cached response.
func (s *Store) State(req *http.Request, cached *Representation) (CachedState, time.Duration) {
	if cached == nil {
		return Miss, time.Duration(0)
	}

	cached.RLock()
	defer cached.RUnlock()

	if contains(req.Header, pragmaField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if contains(req.Header, cacheControlField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if contains(cached.HeaderMap, cacheControlField, noStore) {
		return Revalidate, time.Duration(0)
	}

	if lifetime, ok := freshnessLifetime(cached); ok {
		age := currentAge(cached)

		// cached responses before the last destructive requests (e.g. POST) are considered outdated.
		if time.Since(s.OriginChangedAt) <= age {
			return Revalidate, time.Duration(0)
		}

		delta := age - lifetime
		if lifetime > age {
			return Fresh, delta
		}

		if contains(cached.HeaderMap, cacheControlField, revalidatePattern) {
			return Revalidate, time.Duration(0)
		}

		return Stale, delta
	}

	return Revalidate, time.Duration(0)
}

func (s CachedState) String() string {
	switch s {
	case Miss:
		return "miss"
	case Fresh:
		return "fresh"
	case Stale:
		return "stale"
	case Revalidate:
		return "revalidate"
	default:
		return "unknown"
	}
}

func less(a, b *Representation) bool {
	return a.LastUsedTime.Before(b.LastUsedTime)
}
<|endoftext|>


<commit_before>package captain // import "github.com/harbur/captain/captain"

import (
	"fmt"
	"os"
	"strings"

	"github.com/spf13/cobra"
)

type Options struct {
	debug  bool
	config string
	images []string
}

var options Options

func handleCmd() {

	var cmdBuild = &cobra.Command{
		Use:   "build [image]",
		Short: "Builds the docker image(s) of your repository",
		Long:  `It will build the docker image(s) described on captain.yml in order they appear on file.`,
		Run: func(cmd *cobra.Command, args []string) {
			config := NewConfig(options, true)

			var images = config.GetImageNames()

			if len(args) == 1 {
				images = filterImages(images, args[0])
			}

			for _, value := range images {
				s := strings.Split(value, "=")
				dockerfile, image := s[0], s[1]

				info("Building image %s", image)
				execute("docker", "build", "-f", dockerfile, "-t", image, ".")

				if isDirty() {
					debug("Skipping tag of %s", image)
				} else {
					var rev = getRevision()
					var imagename = image + ":" + rev
					info("Tagging image as %s", imagename)
					execute("docker", "tag", "-f", image, imagename)

					var branch = getBranch()
					if branch != "HEAD" {
						var branchname = image + ":" + branch
						info("Tagging image as %s", branchname)
						execute("docker", "tag", "-f", image, branchname)
					}
				}
			}
		},
	}

	var cmdTest = &cobra.Command{
		Use:   "test",
		Short: "Runs the unit tests",
		Long:  `It will execute the commands described on unit testing in order they appear on file.`,
		Run: func(cmd *cobra.Command, args []string) {
			config := NewConfig(options, true)

			for _, value := range config.GetUnitTestCommands() {
				info("Running unit test command: %s", value)
				execute("bash", "-c", value)
			}
		},
	}

	var cmdVersion = &cobra.Command{
		Use:   "version",
		Short: "Display version",
		Long:  `Displays the version of Crane.`,
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("v0.0.1")
		},
	}

	var captainCmd = &cobra.Command{
		Use:   "captain",
		Short: "captain - build tool for Docker focused on CI/CD",
		Long: `
Captain, the CLI build tool for Docker made for Continuous Integration / Continuous Delivery.

It works by reading captain.yaml file which describes how to build, test, push and release the docker image(s) of your repository.`,
	}

	captainCmd.PersistentFlags().BoolVarP(&options.debug, "debug", "D", false, "Enable debug mode")
	captainCmd.AddCommand(cmdBuild, cmdTest, cmdVersion)
	captainCmd.Execute()
}

func filterImages(images []string, arg string) []string {
	for _, value := range images {
		s := strings.Split(value, "=")
		_, image := s[0], s[1]
		if image == arg {
			return []string{value}
		}
	}
	err("Build image %s is not defined", arg)
	os.Exit(-1)
	return []string{}
}
<commit_msg>Added debug logs<commit_after>package captain // import "github.com/harbur/captain/captain"

import (
	"fmt"
	"os"
	"strings"

	"github.com/spf13/cobra"
)

type Options struct {
	debug  bool
	config string
	images []string
}

var options Options

func handleCmd() {

	var cmdBuild = &cobra.Command{
		Use:   "build [image]",
		Short: "Builds the docker image(s) of your repository",
		Long:  `It will build the docker image(s) described on captain.yml in order they appear on file.`,
		Run: func(cmd *cobra.Command, args []string) {
			config := NewConfig(options, true)

			var images = config.GetImageNames()

			if len(args) == 1 {
				images = filterImages(images, args[0])
			}

			for _, value := range images {
				s := strings.Split(value, "=")
				dockerfile, image := s[0], s[1]

				info("Building image %s", image)
				execute("docker", "build", "-f", dockerfile, "-t", image, ".")

				if isDirty() {
					debug("Skipping tag of %s", image)
				} else {
					var rev = getRevision()
					var imagename = image + ":" + rev
					info("Tagging image as %s", imagename)
					execute("docker", "tag", "-f", image, imagename)

					var branch = getBranch()
					if branch == "HEAD" {
						debug("Skipping tag of %s in detached mode", image)
					}
					if branch != "HEAD" {
						var branchname = image + ":" + branch
						info("Tagging image as %s", branchname)
						execute("docker", "tag", "-f", image, branchname)
					}
				}
			}
		},
	}

	var cmdTest = &cobra.Command{
		Use:   "test",
		Short: "Runs the unit tests",
		Long:  `It will execute the commands described on unit testing in order they appear on file.`,
		Run: func(cmd *cobra.Command, args []string) {
			config := NewConfig(options, true)

			for _, value := range config.GetUnitTestCommands() {
				info("Running unit test command: %s", value)
				execute("bash", "-c", value)
			}
		},
	}

	var cmdVersion = &cobra.Command{
		Use:   "version",
		Short: "Display version",
		Long:  `Displays the version of Crane.`,
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("v0.0.1")
		},
	}

	var captainCmd = &cobra.Command{
		Use:   "captain",
		Short: "captain - build tool for Docker focused on CI/CD",
		Long: `
Captain, the CLI build tool for Docker made for Continuous Integration / Continuous Delivery.

It works by reading captain.yaml file which describes how to build, test, push and release the docker image(s) of your repository.`,
	}

	captainCmd.PersistentFlags().BoolVarP(&options.debug, "debug", "D", false, "Enable debug mode")
	captainCmd.AddCommand(cmdBuild, cmdTest, cmdVersion)
	captainCmd.Execute()
}

func filterImages(images []string, arg string) []string {
	for _, value := range images {
		s := strings.Split(value, "=")
		_, image := s[0], s[1]
		if image == arg {
			return []string{value}
		}
	}
	err("Build image %s is not defined", arg)
	os.Exit(-1)
	return []string{}
}
<|endoftext|>


<commit_before>package helpers

const (
	GUIDRegex = "[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}"
	//IPAddressRegex = "\\d{1,3}.\\d{1,3}.\\d{1,3}.\\d{1,3}"
	ISO8601Regex = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{1,3}-\\d{2}00"
)
<commit_msg>make timestamp regex match more real possible times<commit_after>package helpers

const (
	GUIDRegex = "[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}"
	//IPAddressRegex = "\\d{1,3}.\\d{1,3}.\\d{1,3}.\\d{1,3}"
	ISO8601Regex = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{1,3}[+-]\\d{4}"
)
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"time"

	"encoding/json"

	"strings"

	csay "github.com/dhruvbird/go-cowsay"
	"github.com/go-xorm/xorm"
	"github.com/gorilla/mux"
	"github.com/ogier/pflag"
	"github.com/olekukonko/tablewriter"
	"github.com/superp00t/godog/phoxy"

	_ "github.com/go-sql-driver/mysql"
)

var (
	DB *xorm.Engine
)

func main() {
	ap := pflag.StringP("api_key", "a", "admin", "the API key for use with the Phoxy administration API")

	name := pflag.StringP("name", "n", "harambe", "username")
	driver := pflag.StringP("driver", "r", "mysql", "SQL driver")
	db := pflag.StringP("db", "d", "", "SQL database")

	endpoint := pflag.StringP("endpoint", "e", "https://ikrypto.club/phoxy/", "connect to the standard XMPP BOSH server")
	logEvents := pflag.BoolP("log_events", "l", false, "log Phoxy events")
	bf := pflag.BoolP("be_filter", "b", false, "filter spam")
	api := pflag.StringP("http_listen", "h", "", "spin up HTTP management server")
	msg := pflag.StringP("msg", "s", "", "greeting message")

	pflag.Parse()
	protocol := phoxy.PHOXY
	if strings.HasSuffix(*endpoint, "http-bind/") {
		protocol = phoxy.BOSH
	}
	opts := phoxy.Opts{
		Type:     protocol,
		Username: *name,
		Chatroom: "lobby",
		Endpoint: *endpoint,
		APIKey:   *ap,
		BeFilter: *bf,
	}

	if *db != "" {
		var err error
		DB, err = xorm.NewEngine(*driver, *db)
		if err != nil {
			log.Fatal(err)
		}

		err = DB.Sync2(new(phoxy.Event))
		if err != nil {
			log.Fatal(err)
		}
	}

	if *api != "" {
		go func() {
			r := mux.NewRouter()

			r.HandleFunc("/messages", func(rw http.ResponseWriter, r *http.Request) {
				enc := json.NewEncoder(rw)
				p := r.URL.Query()
				if p.Get("fmt") == "yes" {
					enc.SetIndent("", "    ")
				}
				var ev []phoxy.Event

				// q := DB.Where("body REGEXP ?", p.Get("r"))

				err := DB.Find(&ev)
				if err != nil {
					log.Println(err)
				}
				enc.Encode(ev)
			})

			log.Fatal(http.ListenAndServe(*api, r))
		}()
	}

	b, err := phoxy.New(&opts)
	if err != nil {
		log.Fatal(err)
	}

	if *logEvents {
		if DB == nil {
			log.Fatal("You need to supply a database to log events.")
		}
		b.Intercept(traceEvent)
	}

	start := time.Now()
	cmd := NewCmdParser()
	topic := "No topic yet"
	unAuthMsg := "You are not authorized to perform this action."

	cmd.Add("ban", "bans a user", func(c *CmdCall) string {
		if len(c.Args) < 1 {
			return "usage: .ban <username>"
		}

		targ := c.Args[0]

		if b.IsAuthorized(b.Opts.Chatroom, c.From) {
			go func() {
				time.Sleep(1000 * time.Millisecond)
				b.Ban(b.Opts.Chatroom, targ)
			}()
			return fmt.Sprintf("%s has been banned.", targ)
		}

		return unAuthMsg
	})

	cmd.Add("interrupt", "interrupt all connections from a user's IP", func(c *CmdCall) string {
		if !b.IsAuthorized(b.Opts.Chatroom, c.From) {
			return unAuthMsg
		}

		if len(c.Args) < 1 {
			return "usage: ipban <username>"
		}

		b.Interrupt(b.Opts.Chatroom, c.Args[0])
		return ""
	})

	cmd.Add("clearjail", "clear server IP jail", func(c *CmdCall) string {
		if !b.IsAuthorized(b.Opts.Chatroom, c.From) {
			return unAuthMsg
		}

		b.ClearJail()
		return "jail cleared."
	})

	cmd.Add("lockdown", "sets the lockdown level (1 to require captcha, 2 for registered users only)", func(c *CmdCall) string {
		if len(c.Args) == 0 {
			return "usage: lockdown <level>"
		}

		if b.AccessLevel(b.Opts.Chatroom, c.From) > 2 {
			b.Lockdown(b.Opts.Chatroom, c.Args[0])
			return ""
		}

		return unAuthMsg
	})

	cmd.Add("cleanup", "destroys recently connected users", func(c *CmdCall) string {
		if len(c.Args) < 1 {
			return "usage: .cleanup <seconds>"
		}

		if b.IsAuthorized(b.Opts.Chatroom, c.From) {
			ti, err := strconv.ParseInt(c.Args[0], 10, 64)
			if err != nil {
				return err.Error()
			}

			dn, err := b.Cleanup(b.Opts.Chatroom, ti)
			return fmt.Sprintf("Cleaned up %d names.", dn)
		}

		return unAuthMsg
	})

	cmd.Add("set_topic", "sets the topic", func(c *CmdCall) string {
		if b.Opts.Type == phoxy.PHOXY && b.AccessLevel(b.Opts.Chatroom, c.From) < 2 {
			return unAuthMsg
		}

		top := strings.Join(c.Args, " ")
		topic = top
		return fmt.Sprintf("topic set to \"%s\"", topic)
	})

	cmd.Add("uptime", "shows how much uptime this bot has", func(c *CmdCall) string {
		return fmt.Sprintf("uptime: %v", time.Since(start))
	})

	// Fun functions
	cmd.Add("ratpost", "Queries a random rat from Reddit", func(c *CmdCall) string {
		if b.AccessLevel(b.Opts.Chatroom, c.From) < 3 {
			return unAuthMsg
		}

		if len(c.Args) != 0 {
			return getRat(b, c.Args[0])
		}
		return getRat(b, "")
	})

	cmd.Add("quote", "Selects a random quote from https://ikrypto.club/quotes/", func(c *CmdCall) string {
		u, err := url.Parse("https://ikrypto.club/quotes/api/quotes")
		if err != nil {
			panic(err)
		}

		if len(c.Args) > 0 {
			q := u.Query()
			q.Set("q", c.Args[0])
			u.RawQuery = q.Encode()
		}

		r, err := http.Get(u.String())
		if err != nil {
			return ""
		}
		var q []Quote
		json.NewDecoder(r.Body).Decode(&q)
		rand.Seed(time.Now().Unix())
		if len(q) == 0 {
			return "no quote found"
		}
		qe := q[rand.Intn(len(q))]
		return qe.Body
	})

	cmd.Add("cowsay", "the cow says stuff", func(c *CmdCall) string {
		if len(c.Args) == 0 {
			return "usage: .cowsay <text>"
		}

		txt := strings.Join(c.Args, " ")
		return "```" + csay.Format(txt)
	})

	cmd.Add("chmod", "chmod <user> <level>", func(c *CmdCall) string {
		acs := b.AccessLevel(b.Opts.Chatroom, c.From)
		if len(c.Args) == 1 {
			acs2 := b.AccessLevel(b.Opts.Chatroom, c.Args[0])
			return fmt.Sprintf("%s's access level is currently %d", c.Args[0], acs2)
		}

		if len(c.Args) == 2 {
			if acs < 6 {
				return unAuthMsg
			}

			if err := b.Chmod(b.Opts.Chatroom, c.Args[0], c.Args[1]); err != nil {
				return "chmod unsuccessful."
			}
		} else {
			return "usage: chmod <user> <level>"
		}
		return ""
	})

	cmd.Add("help", "shows this message", func(c *CmdCall) string {
		buf := new(bytes.Buffer)
		table := tablewriter.NewWriter(buf)
		table.SetHeader([]string{"Command", "Description"})
		table.SetBorder(false)

		var help [][]string
		var ids []string
		for cn, _ := range cmd.Cmds {
			ids = append(ids, cn)
		}

		sort.Strings(ids)

		for _, id := range ids {
			help = append(help, []string{id, cmd.Cmds[id].Description})
		}

		table.AppendBulk(help)
		table.Render()
		return "```" + buf.String()
	})

	b.HandleFunc(phoxy.GROUPMESSAGE, func(ev *phoxy.Event) {
		t := time.Now()
		fmt.Printf("[%d:%d:%d]\t<%s>\t%s\n", t.Hour(), t.Minute(), t.Second(), ev.Username, ev.Body)

		// if IsSpam(ev.Body) {
		// 	b.Interrupt(b.Opts.Chatroom, ev.Username)
		// 	log.Println("Removing spam")
		// 	return
		// }

		if ev.Body == "." {
			b.GroupMessage(".")
			return
		}

		u, err := url.Parse(ev.Body)
		if err == nil {
			if u.Host == "www.youtube.com" {
				var resp noEmbedResp
				err := getJSON("https://noembed.com/embed", map[string]string{"url": u.String()}, &resp)
				if err == nil {
					b.Groupf("\"%s\" by %s", resp.Title, resp.Author)
				}
			}
		}

		msg := cmd.Parse(false, ev.Username, ev.Body)
		if msg != "" {
			b.GroupMessage(msg)
		}
	})

	b.HandleFunc(phoxy.VERIFY, func(ev *phoxy.Event) {
		isp := IsSpam(ev.Username, ev.Body)
		ev.Callback <- !isp
		log.Println(ev.Body)
		log.Println("Is spam", isp)
		if isp {
			log.Println("Banning", ev.Username)
			b.Interrupt(b.Opts.Chatroom, ev.Username)
		}
	})

	b.HandleFunc(phoxy.USERJOIN, func(ev *phoxy.Event) {
		if ev.Username == b.Opts.Username {
			return
		}

		// Don't bother currently logged in users
		if start.UnixNano() > (time.Now().UnixNano() - (10 * time.Second).Nanoseconds()) {
			return
		}

		dur := 4000 * time.Millisecond
		time.Sleep(dur)
		if *msg != "" {
			b.GroupMessage(*msg)
			return
		}
		b.Groupf("Hey, %s! The topic of this conversation is %s", ev.Username, topic)
	})

	err = b.Connect()
	log.Fatal("Error connecting,", err)
}

type Quote struct {
	Id         int64  `json:"id" xorm:"'id'"`
	Body       string `json:"body" xorm:"longtext 'body'"`
	Time       int64  `json:"time" xorm:"'time'"`
	TimeFormat string `json:"-" xorm:"-"`
}

type noEmbedResp struct {
	Title  string `json:"title"`
	Author string `json:"author_name"`
}

func traceEvent(ev *phoxy.Event) {
	if DB != nil {
		DB.Insert(ev)
	}
}

func getJSON(uri string, queryP map[string]string, v interface{}) error {
	if len(queryP) != 0 {
		p, err := url.Parse(uri)
		if err != nil {
			return err
		}

		q := p.Query()
		for k, v := range queryP {
			q.Set(k, v)
		}
		p.RawQuery = q.Encode()
		uri = p.String()
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(uri)

	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0")
	client := &http.Client{}
	r, err := client.Do(req)
	if err != nil {
		return err
	}

	if r.StatusCode != 200 {
		return fmt.Errorf("Server returned %d", r.StatusCode)
	}

	var buf bytes.Buffer
	io.Copy(&buf, r.Body)
	// log.Println(buf.String())
	return json.Unmarshal(buf.Bytes(), &v)
}
<commit_msg>Add urbandictionary command<commit_after>package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"time"

	"encoding/json"

	"strings"

	csay "github.com/dhruvbird/go-cowsay"
	"github.com/go-xorm/xorm"
	"github.com/gorilla/mux"
	"github.com/ogier/pflag"
	"github.com/olekukonko/tablewriter"
	"github.com/superp00t/godog/phoxy"
        "github.com/dpatrie/urbandictionary"

	_ "github.com/go-sql-driver/mysql"
)

var (
	DB *xorm.Engine
)

func main() {
	ap := pflag.StringP("api_key", "a", "admin", "the API key for use with the Phoxy administration API")

	name := pflag.StringP("name", "n", "harambe", "username")
	driver := pflag.StringP("driver", "r", "mysql", "SQL driver")
	db := pflag.StringP("db", "d", "", "SQL database")

	endpoint := pflag.StringP("endpoint", "e", "https://ikrypto.club/phoxy/", "connect to the standard XMPP BOSH server")
	logEvents := pflag.BoolP("log_events", "l", false, "log Phoxy events")
	bf := pflag.BoolP("be_filter", "b", false, "filter spam")
	api := pflag.StringP("http_listen", "h", "", "spin up HTTP management server")
	msg := pflag.StringP("msg", "s", "", "greeting message")

	pflag.Parse()
	protocol := phoxy.PHOXY
	if strings.HasSuffix(*endpoint, "http-bind/") {
		protocol = phoxy.BOSH
	}
	opts := phoxy.Opts{
		Type:     protocol,
		Username: *name,
		Chatroom: "lobby",
		Endpoint: *endpoint,
		APIKey:   *ap,
		BeFilter: *bf,
	}

	if *db != "" {
		var err error
		DB, err = xorm.NewEngine(*driver, *db)
		if err != nil {
			log.Fatal(err)
		}

		err = DB.Sync2(new(phoxy.Event))
		if err != nil {
			log.Fatal(err)
		}
	}

	if *api != "" {
		go func() {
			r := mux.NewRouter()

			r.HandleFunc("/messages", func(rw http.ResponseWriter, r *http.Request) {
				enc := json.NewEncoder(rw)
				p := r.URL.Query()
				if p.Get("fmt") == "yes" {
					enc.SetIndent("", "    ")
				}
				var ev []phoxy.Event

				// q := DB.Where("body REGEXP ?", p.Get("r"))

				err := DB.Find(&ev)
				if err != nil {
					log.Println(err)
				}
				enc.Encode(ev)
			})

			log.Fatal(http.ListenAndServe(*api, r))
		}()
	}

	b, err := phoxy.New(&opts)
	if err != nil {
		log.Fatal(err)
	}

	if *logEvents {
		if DB == nil {
			log.Fatal("You need to supply a database to log events.")
		}
		b.Intercept(traceEvent)
	}

	start := time.Now()
	cmd := NewCmdParser()
	topic := "No topic yet"
	unAuthMsg := "You are not authorized to perform this action."

	cmd.Add("ban", "bans a user", func(c *CmdCall) string {
		if len(c.Args) < 1 {
			return "usage: .ban <username>"
		}

		targ := c.Args[0]

		if b.IsAuthorized(b.Opts.Chatroom, c.From) {
			go func() {
				time.Sleep(1000 * time.Millisecond)
				b.Ban(b.Opts.Chatroom, targ)
			}()
			return fmt.Sprintf("%s has been banned.", targ)
		}

		return unAuthMsg
	})

	cmd.Add("interrupt", "interrupt all connections from a user's IP", func(c *CmdCall) string {
		if !b.IsAuthorized(b.Opts.Chatroom, c.From) {
			return unAuthMsg
		}

		if len(c.Args) < 1 {
			return "usage: ipban <username>"
		}

		b.Interrupt(b.Opts.Chatroom, c.Args[0])
		return ""
	})

	cmd.Add("clearjail", "clear server IP jail", func(c *CmdCall) string {
		if !b.IsAuthorized(b.Opts.Chatroom, c.From) {
			return unAuthMsg
		}

		b.ClearJail()
		return "jail cleared."
	})

	cmd.Add("lockdown", "sets the lockdown level (1 to require captcha, 2 for registered users only)", func(c *CmdCall) string {
		if len(c.Args) == 0 {
			return "usage: lockdown <level>"
		}

		if b.AccessLevel(b.Opts.Chatroom, c.From) > 2 {
			b.Lockdown(b.Opts.Chatroom, c.Args[0])
			return ""
		}

		return unAuthMsg
	})

	cmd.Add("cleanup", "destroys recently connected users", func(c *CmdCall) string {
		if len(c.Args) < 1 {
			return "usage: .cleanup <seconds>"
		}

		if b.IsAuthorized(b.Opts.Chatroom, c.From) {
			ti, err := strconv.ParseInt(c.Args[0], 10, 64)
			if err != nil {
				return err.Error()
			}

			dn, err := b.Cleanup(b.Opts.Chatroom, ti)
			return fmt.Sprintf("Cleaned up %d names.", dn)
		}

		return unAuthMsg
	})

	cmd.Add("set_topic", "sets the topic", func(c *CmdCall) string {
		if b.Opts.Type == phoxy.PHOXY && b.AccessLevel(b.Opts.Chatroom, c.From) < 2 {
			return unAuthMsg
		}

		top := strings.Join(c.Args, " ")
		topic = top
		return fmt.Sprintf("topic set to \"%s\"", topic)
	})

	cmd.Add("uptime", "shows how much uptime this bot has", func(c *CmdCall) string {
		return fmt.Sprintf("uptime: %v", time.Since(start))
	})

	// Fun functions
	cmd.Add("ratpost", "Queries a random rat from Reddit", func(c *CmdCall) string {
		if b.AccessLevel(b.Opts.Chatroom, c.From) < 3 {
			return unAuthMsg
		}

		if len(c.Args) != 0 {
			return getRat(b, c.Args[0])
		}
		return getRat(b, "")
	})

	cmd.Add("urbandict", "Searches a term on the Urban Dictionary", func(c *CmdCall) string {
		if len(c.Args) == 0 {
			return "usage: urbandict <searchterm>"
		}

		searchterm := c.Args[0]

		UDresponse, err := urbandictionary.Query(searchterm)
		if err != nil{
			return err.Error()
		}

		if len(UDresponse.Results) == 0 {
			return "No definition found"
		}

		return UDresponse.Results[0].Definition
	})

	cmd.Add("quote", "Selects a random quote from https://ikrypto.club/quotes/", func(c *CmdCall) string {
		u, err := url.Parse("https://ikrypto.club/quotes/api/quotes")
		if err != nil {
			panic(err)
		}

		if len(c.Args) > 0 {
			q := u.Query()
			q.Set("q", c.Args[0])
			u.RawQuery = q.Encode()
		}

		r, err := http.Get(u.String())
		if err != nil {
			return ""
		}
		var q []Quote
		json.NewDecoder(r.Body).Decode(&q)
		rand.Seed(time.Now().Unix())
		if len(q) == 0 {
			return "no quote found"
		}
		qe := q[rand.Intn(len(q))]
		return qe.Body
	})

	cmd.Add("cowsay", "the cow says stuff", func(c *CmdCall) string {
		if len(c.Args) == 0 {
			return "usage: .cowsay <text>"
		}

		txt := strings.Join(c.Args, " ")
		return "```" + csay.Format(txt)
	})

	cmd.Add("chmod", "chmod <user> <level>", func(c *CmdCall) string {
		acs := b.AccessLevel(b.Opts.Chatroom, c.From)
		if len(c.Args) == 1 {
			acs2 := b.AccessLevel(b.Opts.Chatroom, c.Args[0])
			return fmt.Sprintf("%s's access level is currently %d", c.Args[0], acs2)
		}

		if len(c.Args) == 2 {
			if acs < 6 {
				return unAuthMsg
			}

			if err := b.Chmod(b.Opts.Chatroom, c.Args[0], c.Args[1]); err != nil {
				return "chmod unsuccessful."
			}
		} else {
			return "usage: chmod <user> <level>"
		}
		return ""
	})

	cmd.Add("help", "shows this message", func(c *CmdCall) string {
		log.Println("help called")
		buf := new(bytes.Buffer)
		table := tablewriter.NewWriter(buf)
		table.SetHeader([]string{"Command", "Description"})
		table.SetBorder(false)

		var help [][]string
		var ids []string
		for cn, _ := range cmd.Cmds {
			ids = append(ids, cn)
		}

		sort.Strings(ids)

		for _, id := range ids {
			help = append(help, []string{id, cmd.Cmds[id].Description})
		}

		table.AppendBulk(help)
		table.Render()
		return "```" + buf.String()
	})

	b.HandleFunc(phoxy.GROUPMESSAGE, func(ev *phoxy.Event) {
		t := time.Now()
		fmt.Printf("[%d:%d:%d]\t<%s>\t%s\n", t.Hour(), t.Minute(), t.Second(), ev.Username, ev.Body)

		// if IsSpam(ev.Body) {
		// 	b.Interrupt(b.Opts.Chatroom, ev.Username)
		// 	log.Println("Removing spam")
		// 	return
		// }

		if ev.Body == "." {
			b.GroupMessage(".")
			return
		}

		u, err := url.Parse(ev.Body)
		if err == nil {
			if u.Host == "www.youtube.com" {
				var resp noEmbedResp
				err := getJSON("https://noembed.com/embed", map[string]string{"url": u.String()}, &resp)
				if err == nil {
					b.Groupf("\"%s\" by %s", resp.Title, resp.Author)
				}
			}
		}

		msg := cmd.Parse(false, ev.Username, ev.Body)
		if msg != "" {
			b.GroupMessage(msg)
		}
	})

	b.HandleFunc(phoxy.VERIFY, func(ev *phoxy.Event) {
		isp := IsSpam(ev.Username, ev.Body)
		ev.Callback <- !isp
		log.Println(ev.Body)
		log.Println("Is spam", isp)
		if isp {
			log.Println("Banning", ev.Username)
			b.Interrupt(b.Opts.Chatroom, ev.Username)
		}
	})

	b.HandleFunc(phoxy.USERJOIN, func(ev *phoxy.Event) {
		if ev.Username == b.Opts.Username {
			return
		}

		// Don't bother currently logged in users
		if start.UnixNano() > (time.Now().UnixNano() - (10 * time.Second).Nanoseconds()) {
			return
		}

		dur := 4000 * time.Millisecond
		time.Sleep(dur)
		if *msg != "" {
			b.GroupMessage(*msg)
			return
		}
		b.Groupf("Hey, %s! The topic of this conversation is %s", ev.Username, topic)
	})

	err = b.Connect()
	log.Fatal("Error connecting,", err)
}

type Quote struct {
	Id         int64  `json:"id" xorm:"'id'"`
	Body       string `json:"body" xorm:"longtext 'body'"`
	Time       int64  `json:"time" xorm:"'time'"`
	TimeFormat string `json:"-" xorm:"-"`
}

type noEmbedResp struct {
	Title  string `json:"title"`
	Author string `json:"author_name"`
}

func traceEvent(ev *phoxy.Event) {
	if DB != nil {
		DB.Insert(ev)
	}
}

func getJSON(uri string, queryP map[string]string, v interface{}) error {
	if len(queryP) != 0 {
		p, err := url.Parse(uri)
		if err != nil {
			return err
		}

		q := p.Query()
		for k, v := range queryP {
			q.Set(k, v)
		}
		p.RawQuery = q.Encode()
		uri = p.String()
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(uri)

	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0")
	client := &http.Client{}
	r, err := client.Do(req)
	if err != nil {
		return err
	}

	if r.StatusCode != 200 {
		return fmt.Errorf("Server returned %d", r.StatusCode)
	}

	var buf bytes.Buffer
	io.Copy(&buf, r.Body)
	// log.Println(buf.String())
	return json.Unmarshal(buf.Bytes(), &v)
}
<|endoftext|>


<commit_before>package slacker

import (
	"fmt"
	"regexp"
)

// Bot is the basic type for defining a simple chat bot.
type Bot struct {
	slack *Slack
	rules []Rule
}

// Rule is a basic type representing bot a bot response rule.
type Rule struct {
	matchRe *regexp.Regexp
	exec    func(string, []string) string
}

// NewBot creates a new bot that connects to Slack with the given bot token.
// It will connect automatically and start processing responses in a goroutine.
func NewBot(token string) (*Bot, error) {
	slack, err := Connect(token)
	if err != nil {
		return nil, err
	}
	bot := &Bot{slack, []Rule{}}
	go bot.run()
	return bot, nil
}

// ClearResponses can be used to delete all of the rules that a bot knows about.
func (b *Bot) ClearResponses() {
	b.rules = b.rules[:0]
}

func (b *Bot) run() {
	for {
		message := <-b.slack.rtm.in
		fmt.Printf("Incoming %v\n", message)
		kind, ok := message["type"]
		if !ok {
			continue
		}
		if kind != "message" {
			continue
		}
		text := message["text"].(string)
		channel := message["channel"].(string)
		user := message["user"].(string)

		for _, rule := range b.rules {
			parts := rule.matchRe.FindStringSubmatch(text)
			if parts != nil {
				b.slack.rtm.Say(channel, rule.exec(user, parts))
			}
		}
	}
}

// RespondWith responds to the given regular expression, if it matches
// anywhere in the string, by executing the given function to construct
// a response. The exec arguments are the user id and the array of
// the regular expression groups.
func (b *Bot) RespondWith(re string, exec func(string, []string) string) {
	b.rules = append(b.rules, Rule{regexp.MustCompile(re), exec})
}
<commit_msg>Check for non-nil messages before reading them<commit_after>package slacker

import (
	"fmt"
	"regexp"
)

// Bot is the basic type for defining a simple chat bot.
type Bot struct {
	slack *Slack
	rules []Rule
}

// Rule is a basic type representing bot a bot response rule.
type Rule struct {
	matchRe *regexp.Regexp
	exec    func(string, []string) string
}

// NewBot creates a new bot that connects to Slack with the given bot token.
// It will connect automatically and start processing responses in a goroutine.
func NewBot(token string) (*Bot, error) {
	slack, err := Connect(token)
	if err != nil {
		return nil, err
	}
	bot := &Bot{slack, []Rule{}}
	go bot.run()
	return bot, nil
}

// ClearResponses can be used to delete all of the rules that a bot knows about.
func (b *Bot) ClearResponses() {
	b.rules = b.rules[:0]
}

func (b *Bot) run() {
	for {
		message := <-b.slack.rtm.in
		fmt.Printf("Incoming %v\n", message)
		kind, ok := message["type"]
		if !ok {
			continue
		}
		if kind != "message" {
			continue
		}
		if message["text"] == nil || message["channel"] == nil || message["user"] == nil {
			continue
		}
		text := message["text"].(string)
		channel := message["channel"].(string)
		user := message["user"].(string)

		for _, rule := range b.rules {
			parts := rule.matchRe.FindStringSubmatch(text)
			if parts != nil {
				b.slack.rtm.Say(channel, rule.exec(user, parts))
			}
		}
	}
}

// RespondWith responds to the given regular expression, if it matches
// anywhere in the string, by executing the given function to construct
// a response. The exec arguments are the user id and the array of
// the regular expression groups.
func (b *Bot) RespondWith(re string, exec func(string, []string) string) {
	b.rules = append(b.rules, Rule{regexp.MustCompile(re), exec})
}
<|endoftext|>


<commit_before>package main

import (
	"github.com/go-telegram-bot-api/telegram-bot-api"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

var store = make(map[string]map[string]float64)

func InitBot(token string) (*tgbotapi.BotAPI, error) {
	bot, err := tgbotapi.NewBotAPI(token)
	if err != nil {
		return nil, err
	}

	//bot.Debug = true
	log.Println("Account: ", bot.Self.UserName)

	return bot, nil
}

func InitWebHook(bot *tgbotapi.BotAPI, config Config) (tgbotapi.UpdatesChannel, error) {
	URL, err := url.Parse(config.Url)
	if err != nil {
		return nil, err
	}

	bot.SetWebhook(tgbotapi.WebhookConfig{URL: URL})

	updates := bot.ListenForWebhook(URL.Path)

	go http.ListenAndServe("localhost:"+config.Port, nil)
	if err != nil {
		return nil, err
	}

	return updates, nil
}

func ProcessUpdates(updates tgbotapi.UpdatesChannel, bot *tgbotapi.BotAPI) {

	for update := range updates {
		if update.Message == nil {
			continue
		}

		log.Println(update.Message.Chat.ID, update.Message.From.ID, update.Message.Text)
		text := update.Message.Text
		var response string

		account := strconv.FormatInt(int64(update.Message.From.ID), 10)
		money, err := strconv.ParseFloat(strings.Split(text, " ")[0], 64)
		if err != nil {
			log.Println("[ERROR]: ", err)
		}

		switch true {
		case strings.Contains(text, "/add"):
			response = addMoney(money, account, "default")
		case strings.Contains(text, "/rem"):
			response = "removing money"
		case strings.Contains(text, "/status"):
			response = "getting status"
		default:
			response = "i don't get what you want from me."
		}
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, response)
		msg.ReplyToMessageID = update.Message.MessageID

		bot.Send(msg)
	}
}

func addMoney(money float64, account, category string) string {
	val, ok := store[account]
	if !ok {
		log.Println("store value: ", val)
		store[account] = make(map[string]float64)
		log.Println("store value: ", val)
	}
	log.Println("category", category)
	return "add: " + strconv.FormatFloat(money, 'f', -1, 64)
}
<commit_msg>remade operations<commit_after>package main

import (
	"github.com/go-telegram-bot-api/telegram-bot-api"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

var store = make(map[string]map[string]float64)

func InitBot(token string) (*tgbotapi.BotAPI, error) {
	bot, err := tgbotapi.NewBotAPI(token)
	if err != nil {
		return nil, err
	}

	//bot.Debug = true
	log.Println("Account: ", bot.Self.UserName)

	return bot, nil
}

func InitWebHook(bot *tgbotapi.BotAPI, config Config) (tgbotapi.UpdatesChannel, error) {
	URL, err := url.Parse(config.Url)
	if err != nil {
		return nil, err
	}

	bot.SetWebhook(tgbotapi.WebhookConfig{URL: URL})

	updates := bot.ListenForWebhook(URL.Path)

	go http.ListenAndServe("localhost:"+config.Port, nil)
	if err != nil {
		return nil, err
	}

	return updates, nil
}

func ProcessUpdates(updates tgbotapi.UpdatesChannel, bot *tgbotapi.BotAPI) {

	for update := range updates {
		if update.Message == nil {
			continue
		}

		log.Println(update.Message.Chat.ID, update.Message.From.ID, update.Message.Text)
		text := update.Message.Text
		var response string

		account := strconv.FormatInt(int64(update.Message.From.ID), 10)
		money, err := strconv.ParseFloat(strings.Split(text, " ")[1], 64)
		if err != nil {
			log.Println("[ERROR]: ", err)
		}

		switch true {
		case strings.Contains(text, "/start"):
			response = startMessage()
		case strings.HasPrefix(text, "+"):
			response = addMoney(money, account, strings.Split(text, " ")[2])
		case strings.HasPrefix(text, "-"):
			response = "removing money"
		case strings.Contains(text, "/status"):
			response = "getting status"
		default:
			response = "i don't get what you want from me."
		}
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, response)
		msg.ReplyToMessageID = update.Message.MessageID

		bot.Send(msg)
	}
}

func startMessage() string {
	return `This is check wallet bot, usage:
			+ <money> <category(optional)> - add money to wallet
			- <money> <category(optional)> - remove money from wallet`
}

func addMoney(money float64, account, category string) string {
	val, ok := store[account]
	if !ok {
		log.Println("store value: ", val)
		store[account] = make(map[string]float64)
		log.Println("store value: ", val)
	}
	log.Println("category", category)
	return "add: " + strconv.FormatFloat(money, 'f', -1, 64)
}
<|endoftext|>


<commit_before>package config

import (
	"fmt"
	"reflect"

	"github.com/dnephin/configtf"
	pth "github.com/dnephin/configtf/path"
	shlex "github.com/kballard/go-shellquote"
)

// JobConfig A **job** resource uses an `image`_ to run a job in a container.
// A **job** resource that doesn't have an **artifact** is never considered
// up-to-date and will always run.  If a job resource has an **artifact**
// the last modified time of that file will be used as the modified time for the
// **job**.
//
// The `image`_ specified in **use** and any `mount`_ resources listed in
// **mounts** are automatically added as dependencies and will always be
// created first.
// name: job
// example: Run a container using the ``builder`` image to compile some source
// code to ``./dist/app-binary``.
//
// .. code-block:: yaml
//
//     job=compile:
//         use: builder
//         mounts: [source, dist]
//         artifact: dist/app-binary
//
type JobConfig struct {
	// Use The name of an `image`_ resource. The referenced image is used
	// to created the container for the **job**.
	Use string `config:"required"`
	// Artifact File paths or globs identifying the files created by the **job**.
	// Paths are relative to the current working directory.
	// type: list of file paths or glob patterns
	Artifact PathGlobs
	// Command The command to run in the container.
	// type: shell quoted string
	// example: ``"bash -c 'echo something'"``
	Command ShlexSlice
	// Entrypoint Override the image entrypoint
	// type: shell quoted string
	Entrypoint ShlexSlice
	// Sources File paths or globs of the files used to create the
	// artifact. The modified time of these files are compared to the modified time
	// of the artifact to determine if the **job** is stale. If the **sources**
	// list is defined the modified time of **mounts** and the **use** image are
	// ignored.
	// type: list of file paths or glob patterns
	Sources PathGlobs
	// Mounts A list of `mount`_ resources to use when creating the container.
	// type: list of mount resources
	Mounts []string
	// Privileged Gives extended privileges to the container
	Privileged bool
	// Interactive Makes the container interative and enables a tty.
	Interactive bool
	// Env Environment variables to pass to the container. This field
	// supports :doc:`variables`.
	// type: list of ``key=value`` strings
	Env []string
	// ProvideDocker Exposes the docker engine to the container by either
	// mounting the unix socket or setting the **DOCKER_HOST** environment
	// variable. All environment variables with a  **DOCKER_** prefix in the
	// environment are set on the container.
	ProvideDocker bool
	// NetMode The network mode to use. This field supports :doc:`variables`.
	NetMode string
	// WorkingDir The directory to set as the active working directory in the
	// container. This field supports :doc:`variables`.
	WorkingDir string
	// User Username or UID to use in the container. Format ``user[:group]``.
	User string
	// Ports Publish ports to the host
	// type: list of 'host_port:container_port'
	Ports []string
	// Devices Maps the host devices you want to connect to a container
	// type: list of device specs
	// example: ``{Host: /dev/fb0, Container: /dev/fb0, Permissions: rwm}``
	Devices []Device
	// Labels sets the labels of the running job container
	// type: map of string keys to string values
	Labels map[string]string
	Dependent
	Annotations
}

// Device is the defined structure to attach host devices to containers
type Device struct {
	Host        string
	Container   string
	Permissions string
}

// Dependencies returns the list of implicit and explicit dependencies
func (c *JobConfig) Dependencies() []string {
	return append([]string{c.Use}, append(c.Depends, c.Mounts...)...)
}

// Validate checks that all fields have acceptable values
func (c *JobConfig) Validate(path pth.Path, config *Config) *pth.Error {
	validators := []validator{
		newValidator("use", func() error { return c.validateUse(config) }),
		newValidator("mounts", func() error { return c.validateMounts(config) }),
		newValidator("artifact", c.Artifact.Validate),
		newValidator("sources", c.Sources.Validate),
	}
	for _, validator := range validators {
		if err := validator.validate(); err != nil {
			return pth.Errorf(path.Add(validator.name), err.Error())
		}
	}
	return nil
}

func (c *JobConfig) validateUse(config *Config) error {
	err := fmt.Errorf("%s is not an image resource", c.Use)

	res, ok := config.Resources[c.Use]
	if !ok {
		return err
	}

	switch res.(type) {
	case *ImageConfig:
	default:
		return err
	}

	return nil
}

func (c *JobConfig) validateMounts(config *Config) error {
	for _, mount := range c.Mounts {
		err := fmt.Errorf("%s is not a mount resource", mount)

		res, ok := config.Resources[mount]
		if !ok {
			return err
		}

		switch res.(type) {
		case *MountConfig:
		default:
			return err
		}
	}
	return nil
}

func (c *JobConfig) String() string {
	artifact, command := "", ""
	if !c.Artifact.Empty() {
		artifact = fmt.Sprintf(" to create '%s'", &c.Artifact)
	}
	// TODO: look for entrypoint as well as command
	if !c.Command.Empty() {
		command = fmt.Sprintf("'%s' using ", c.Command.String())
	}
	return fmt.Sprintf("Run %sthe '%s' image%s", command, c.Use, artifact)
}

// Resolve resolves variables in the resource
func (c *JobConfig) Resolve(resolver Resolver) (Resource, error) {
	conf := *c
	var err error
	conf.Env, err = resolver.ResolveSlice(c.Env)
	if err != nil {
		return &conf, err
	}
	conf.WorkingDir, err = resolver.Resolve(c.WorkingDir)
	if err != nil {
		return &conf, err
	}
	conf.User, err = resolver.Resolve(c.User)
	if err != nil {
		return &conf, err
	}
	conf.NetMode, err = resolver.Resolve(c.NetMode)
	return &conf, err
}

// ShlexSlice is a type used for config transforming a string into a []string
// using shelx.
type ShlexSlice struct {
	original string
	parsed   []string
}

func (s *ShlexSlice) String() string {
	return s.original
}

// Value returns the slice value
func (s *ShlexSlice) Value() []string {
	return s.parsed
}

// Empty returns true if the instance contains the zero value
func (s *ShlexSlice) Empty() bool {
	return s.original == ""
}

// TransformConfig is used to transform a string from a config file into a
// sliced value, using shlex.
func (s *ShlexSlice) TransformConfig(raw reflect.Value) error {
	if !raw.IsValid() {
		return fmt.Errorf("must be a string, was undefined")
	}

	var err error
	switch value := raw.Interface().(type) {
	case string:
		s.original = value
		s.parsed, err = shlex.Split(value)
		if err != nil {
			return fmt.Errorf("failed to parse command %q: %s", value, err)
		}
	default:
		return fmt.Errorf("must be a string, not %T", value)
	}
	return nil
}

func jobFromConfig(name string, values map[string]interface{}) (Resource, error) {
	cmd := &JobConfig{}
	return cmd, configtf.Transform(name, values, cmd)
}

func init() {
	RegisterResource("job", jobFromConfig)
	// Backwards compatibility for v0.4, remove in v1.0
	RegisterResource("run", jobFromConfig)
}
<commit_msg>Document no-bind-mount<commit_after>package config

import (
	"fmt"
	"reflect"

	"github.com/dnephin/configtf"
	pth "github.com/dnephin/configtf/path"
	shlex "github.com/kballard/go-shellquote"
)

// JobConfig A **job** resource uses an `image`_ to run a job in a container.
//
// A **job** resource that doesn't have an ``artifact`` is never considered
// up-to-date and will always run.  If a job resource has an ``artifact``
// the job will be skipped if the artifact is newer than the source.
// The last modified time of the ``artifact`` files is compared against the
// last modified time of the files in ``sources``, or if ``sources`` is left
// unset, the last modified time of the ``use`` image and all the files in
// the ``mounts``.
//
// ``mounts`` are provided to the container as bind mounts. If the ``DOBI_NO_BIND_MOUNT``
// environment variable, or `--no-build-mount` flag is set, then ``mounts``
// will be copied into the container, and all artifacts will be copied out of the
// container to the host after the job is complete.
//
// The `image`_ specified in ``use`` and any `mount`_ resources listed in
// ``mounts`` are automatically added as dependencies and will always be
// created first.
//
// name: job
// example: Run a container using the ``builder`` image to compile some source
// code to ``./dist/app-binary``.
//
// .. code-block:: yaml
//
//     job=compile:
//         use: builder
//         mounts: [source, dist]
//         artifact: dist/app-binary
//
type JobConfig struct {
	// Use The name of an `image`_ resource. The referenced image is used
	// to created the container for the **job**.
	Use string `config:"required"`
	// Artifact File paths or globs identifying the files created by the **job**.
	// Paths to directories must end with a path separator (``/``).
	// Paths are relative to the ``dobi.yaml``
	// type: list of file paths or glob patterns
	Artifact PathGlobs
	// Command The command to run in the container.
	// type: shell quoted string
	// example: ``"bash -c 'echo something'"``
	Command ShlexSlice
	// Entrypoint Override the image entrypoint
	// type: shell quoted string
	Entrypoint ShlexSlice
	// Sources File paths or globs of the files used to create the
	// artifact. The modified time of these files are compared to the modified time
	// of the artifact to determine if the **job** is stale. If the **sources**
	// list is defined the modified time of **mounts** and the **use** image are
	// ignored.
	// type: list of file paths or glob patterns
	Sources PathGlobs
	// Mounts A list of `mount`_ resources to use when creating the container.
	// type: list of mount resources
	Mounts []string
	// Privileged Gives extended privileges to the container
	Privileged bool
	// Interactive Makes the container interative and enables a tty.
	Interactive bool
	// Env Environment variables to pass to the container. This field
	// supports :doc:`variables`.
	// type: list of ``key=value`` strings
	Env []string
	// ProvideDocker Exposes the docker engine to the container by either
	// mounting the unix socket or setting the ``DOCKER_HOST`` environment
	// variable. All environment variables with a  ``DOCKER_`` prefix in the
	// environment are set on the container.
	ProvideDocker bool
	// NetMode The network mode to use. This field supports :doc:`variables`.
	NetMode string
	// WorkingDir The directory to set as the active working directory in the
	// container. This field supports :doc:`variables`.
	WorkingDir string
	// User Username or UID to use in the container. Format ``user[:group]``.
	User string
	// Ports Publish ports to the host
	// type: list of 'host_port:container_port'
	Ports []string
	// Devices Maps the host devices you want to connect to a container
	// type: list of device specs
	// example: ``{Host: /dev/fb0, Container: /dev/fb0, Permissions: rwm}``
	Devices []Device
	// Labels sets the labels of the running job container
	// type: map of string keys to string values
	Labels map[string]string
	Dependent
	Annotations
}

// Device is the defined structure to attach host devices to containers
type Device struct {
	Host        string
	Container   string
	Permissions string
}

// Dependencies returns the list of implicit and explicit dependencies
func (c *JobConfig) Dependencies() []string {
	return append([]string{c.Use}, append(c.Depends, c.Mounts...)...)
}

// Validate checks that all fields have acceptable values
func (c *JobConfig) Validate(path pth.Path, config *Config) *pth.Error {
	validators := []validator{
		newValidator("use", func() error { return c.validateUse(config) }),
		newValidator("mounts", func() error { return c.validateMounts(config) }),
		newValidator("artifact", c.Artifact.Validate),
		newValidator("sources", c.Sources.Validate),
	}
	for _, validator := range validators {
		if err := validator.validate(); err != nil {
			return pth.Errorf(path.Add(validator.name), err.Error())
		}
	}
	return nil
}

func (c *JobConfig) validateUse(config *Config) error {
	err := fmt.Errorf("%s is not an image resource", c.Use)

	res, ok := config.Resources[c.Use]
	if !ok {
		return err
	}

	switch res.(type) {
	case *ImageConfig:
	default:
		return err
	}

	return nil
}

func (c *JobConfig) validateMounts(config *Config) error {
	for _, mount := range c.Mounts {
		err := fmt.Errorf("%s is not a mount resource", mount)

		res, ok := config.Resources[mount]
		if !ok {
			return err
		}

		switch res.(type) {
		case *MountConfig:
		default:
			return err
		}
	}
	return nil
}

func (c *JobConfig) String() string {
	artifact, command := "", ""
	if !c.Artifact.Empty() {
		artifact = fmt.Sprintf(" to create '%s'", &c.Artifact)
	}
	// TODO: look for entrypoint as well as command
	if !c.Command.Empty() {
		command = fmt.Sprintf("'%s' using ", c.Command.String())
	}
	return fmt.Sprintf("Run %sthe '%s' image%s", command, c.Use, artifact)
}

// Resolve resolves variables in the resource
func (c *JobConfig) Resolve(resolver Resolver) (Resource, error) {
	conf := *c
	var err error
	conf.Env, err = resolver.ResolveSlice(c.Env)
	if err != nil {
		return &conf, err
	}
	conf.WorkingDir, err = resolver.Resolve(c.WorkingDir)
	if err != nil {
		return &conf, err
	}
	conf.User, err = resolver.Resolve(c.User)
	if err != nil {
		return &conf, err
	}
	conf.NetMode, err = resolver.Resolve(c.NetMode)
	return &conf, err
}

// ShlexSlice is a type used for config transforming a string into a []string
// using shelx.
type ShlexSlice struct {
	original string
	parsed   []string
}

func (s *ShlexSlice) String() string {
	return s.original
}

// Value returns the slice value
func (s *ShlexSlice) Value() []string {
	return s.parsed
}

// Empty returns true if the instance contains the zero value
func (s *ShlexSlice) Empty() bool {
	return s.original == ""
}

// TransformConfig is used to transform a string from a config file into a
// sliced value, using shlex.
func (s *ShlexSlice) TransformConfig(raw reflect.Value) error {
	if !raw.IsValid() {
		return fmt.Errorf("must be a string, was undefined")
	}

	var err error
	switch value := raw.Interface().(type) {
	case string:
		s.original = value
		s.parsed, err = shlex.Split(value)
		if err != nil {
			return fmt.Errorf("failed to parse command %q: %s", value, err)
		}
	default:
		return fmt.Errorf("must be a string, not %T", value)
	}
	return nil
}

func jobFromConfig(name string, values map[string]interface{}) (Resource, error) {
	cmd := &JobConfig{}
	return cmd, configtf.Transform(name, values, cmd)
}

func init() {
	RegisterResource("job", jobFromConfig)
	// Backwards compatibility for v0.4, remove in v1.0
	RegisterResource("run", jobFromConfig)
}
<|endoftext|>