<commit_before>/*
Copyright 2016 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 filters

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	auditinternal "k8s.io/apiserver/pkg/apis/audit"
	"k8s.io/apiserver/pkg/audit"
	"k8s.io/apiserver/pkg/audit/policy"
	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
	"k8s.io/apiserver/pkg/endpoints/request"
)

// WithAudit decorates a http.Handler with audit logging information for all the
// requests coming to the server. Audit level is decided according to requests'
// attributes and audit policy. Logs are emitted to the audit sink to
// process events. If sink or audit policy is nil, no decoration takes place.
func WithAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, sink audit.Sink, policy policy.Checker, longRunningCheck request.LongRunningRequestCheck) http.Handler {
	if sink == nil || policy == nil {
		return handler
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx, ev, omitStages, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
		if err != nil {
			utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err))
			responsewriters.InternalError(w, req, errors.New("failed to create audit event"))
			return
		}
		if ev == nil || ctx == nil {
			handler.ServeHTTP(w, req)
			return
		}

		ev.Stage = auditinternal.StageRequestReceived
		processAuditEvent(sink, ev, omitStages)

		// intercept the status code
		var longRunningSink audit.Sink
		if longRunningCheck != nil {
			ri, _ := request.RequestInfoFrom(ctx)
			if longRunningCheck(req, ri) {
				longRunningSink = sink
			}
		}
		respWriter := decorateResponseWriter(w, ev, longRunningSink, omitStages)

		// send audit event when we leave this func, either via a panic or cleanly. In the case of long
		// running requests, this will be the second audit event.
		defer func() {
			if r := recover(); r != nil {
				defer panic(r)
				ev.Stage = auditinternal.StagePanic
				ev.ResponseStatus = &metav1.Status{
					Code:    http.StatusInternalServerError,
					Status:  metav1.StatusFailure,
					Reason:  metav1.StatusReasonInternalError,
					Message: fmt.Sprintf("APIServer panic'd: %v", r),
				}
				processAuditEvent(sink, ev, omitStages)
				return
			}

			// if no StageResponseStarted event was sent b/c neither a status code nor a body was sent, fake it here
			// But Audit-Id http header will only be sent when http.ResponseWriter.WriteHeader is called.
			fakedSuccessStatus := &metav1.Status{
				Code:    http.StatusOK,
				Status:  metav1.StatusSuccess,
				Message: "Connection closed early",
			}
			if ev.ResponseStatus == nil && longRunningSink != nil {
				ev.ResponseStatus = fakedSuccessStatus
				ev.Stage = auditinternal.StageResponseStarted
				processAuditEvent(longRunningSink, ev, omitStages)
			}

			ev.Stage = auditinternal.StageResponseComplete
			if ev.ResponseStatus == nil {
				ev.ResponseStatus = fakedSuccessStatus
			}
			processAuditEvent(sink, ev, omitStages)
		}()
		handler.ServeHTTP(respWriter, req)
	})
}

// createAuditEventAndAttachToContext is responsible for creating the audit event
// and attaching it to the appropriate request context. It returns:
// - context with audit event attached to it
// - created audit event
// - error if anything bad happened
func createAuditEventAndAttachToContext(requestContextMapper request.RequestContextMapper, req *http.Request, policy policy.Checker) (request.Context, *auditinternal.Event, []auditinternal.Stage, error) {
	ctx, ok := requestContextMapper.Get(req)
	if !ok {
		return nil, nil, nil, fmt.Errorf("no context found for request")
	}

	attribs, err := GetAuthorizerAttributes(ctx)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
	}

	level, omitStages := policy.LevelAndStages(attribs)
	audit.ObservePolicyLevel(level)
	if level == auditinternal.LevelNone {
		// Don't audit.
		return nil, nil, nil, nil
	}

	ev, err := audit.NewEventFromRequest(req, level, attribs)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
	}

	ctx = request.WithAuditEvent(ctx, ev)
	if err := requestContextMapper.Update(req, ctx); err != nil {
		return nil, nil, nil, fmt.Errorf("failed to attach audit event to context: %v", err)
	}

	return ctx, ev, omitStages, nil
}

func processAuditEvent(sink audit.Sink, ev *auditinternal.Event, omitStages []auditinternal.Stage) {
	for _, stage := range omitStages {
		if ev.Stage == stage {
			return
		}
	}
	audit.ObserveEvent()
	sink.ProcessEvents(ev)
}

func decorateResponseWriter(responseWriter http.ResponseWriter, ev *auditinternal.Event, sink audit.Sink, omitStages []auditinternal.Stage) http.ResponseWriter {
	delegate := &auditResponseWriter{
		ResponseWriter: responseWriter,
		event:          ev,
		sink:           sink,
		omitStages:     omitStages,
	}

	// check if the ResponseWriter we're wrapping is the fancy one we need
	// or if the basic is sufficient
	_, cn := responseWriter.(http.CloseNotifier)
	_, fl := responseWriter.(http.Flusher)
	_, hj := responseWriter.(http.Hijacker)
	if cn && fl && hj {
		return &fancyResponseWriterDelegator{delegate}
	}
	return delegate
}

var _ http.ResponseWriter = &auditResponseWriter{}

// auditResponseWriter intercepts WriteHeader, sets it in the event. If the sink is set, it will
// create immediately an event (for long running requests).
type auditResponseWriter struct {
	http.ResponseWriter
	event      *auditinternal.Event
	once       sync.Once
	sink       audit.Sink
	omitStages []auditinternal.Stage
}

func (a *auditResponseWriter) setHttpHeader() {
	a.ResponseWriter.Header().Set(auditinternal.HeaderAuditID, string(a.event.AuditID))
}

func (a *auditResponseWriter) processCode(code int) {
	a.once.Do(func() {
		if a.event.ResponseStatus == nil {
			a.event.ResponseStatus = &metav1.Status{}
		}
		a.event.ResponseStatus.Code = int32(code)
		a.event.Stage = auditinternal.StageResponseStarted

		if a.sink != nil {
			processAuditEvent(a.sink, a.event, a.omitStages)
		}
	})
}

func (a *auditResponseWriter) Write(bs []byte) (int, error) {
	// the Go library calls WriteHeader internally if no code was written yet. But this will go unnoticed for us
	a.processCode(http.StatusOK)
	a.setHttpHeader()
	return a.ResponseWriter.Write(bs)
}

func (a *auditResponseWriter) WriteHeader(code int) {
	a.processCode(code)
	a.setHttpHeader()
	a.ResponseWriter.WriteHeader(code)
}

// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc)
// working.
type fancyResponseWriterDelegator struct {
	*auditResponseWriter
}

func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
	return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

func (f *fancyResponseWriterDelegator) Flush() {
	f.ResponseWriter.(http.Flusher).Flush()
}

func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	// fake a response status before protocol switch happens
	f.processCode(http.StatusSwitchingProtocols)

	// This will be ignored if WriteHeader() function has aready been called.
	// It's not guaranteed Audit-ID http header is sent for all requests.
	// For example, when user run "kubectl exec", apiserver uses a proxy handler
	// to deal with the request, users can only get http headers returned by kubelet node.
	f.setHttpHeader()

	return f.ResponseWriter.(http.Hijacker).Hijack()
}

var _ http.CloseNotifier = &fancyResponseWriterDelegator{}
var _ http.Flusher = &fancyResponseWriterDelegator{}
var _ http.Hijacker = &fancyResponseWriterDelegator{}
<commit_msg>Fill in creationtimestamp in audit events<commit_after>/*
Copyright 2016 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 filters

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	auditinternal "k8s.io/apiserver/pkg/apis/audit"
	"k8s.io/apiserver/pkg/audit"
	"k8s.io/apiserver/pkg/audit/policy"
	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
	"k8s.io/apiserver/pkg/endpoints/request"
)

// WithAudit decorates a http.Handler with audit logging information for all the
// requests coming to the server. Audit level is decided according to requests'
// attributes and audit policy. Logs are emitted to the audit sink to
// process events. If sink or audit policy is nil, no decoration takes place.
func WithAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, sink audit.Sink, policy policy.Checker, longRunningCheck request.LongRunningRequestCheck) http.Handler {
	if sink == nil || policy == nil {
		return handler
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx, ev, omitStages, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
		if err != nil {
			utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err))
			responsewriters.InternalError(w, req, errors.New("failed to create audit event"))
			return
		}
		if ev == nil || ctx == nil {
			handler.ServeHTTP(w, req)
			return
		}

		ev.Stage = auditinternal.StageRequestReceived
		processAuditEvent(sink, ev, omitStages)

		// intercept the status code
		var longRunningSink audit.Sink
		if longRunningCheck != nil {
			ri, _ := request.RequestInfoFrom(ctx)
			if longRunningCheck(req, ri) {
				longRunningSink = sink
			}
		}
		respWriter := decorateResponseWriter(w, ev, longRunningSink, omitStages)

		// send audit event when we leave this func, either via a panic or cleanly. In the case of long
		// running requests, this will be the second audit event.
		defer func() {
			if r := recover(); r != nil {
				defer panic(r)
				ev.Stage = auditinternal.StagePanic
				ev.ResponseStatus = &metav1.Status{
					Code:    http.StatusInternalServerError,
					Status:  metav1.StatusFailure,
					Reason:  metav1.StatusReasonInternalError,
					Message: fmt.Sprintf("APIServer panic'd: %v", r),
				}
				processAuditEvent(sink, ev, omitStages)
				return
			}

			// if no StageResponseStarted event was sent b/c neither a status code nor a body was sent, fake it here
			// But Audit-Id http header will only be sent when http.ResponseWriter.WriteHeader is called.
			fakedSuccessStatus := &metav1.Status{
				Code:    http.StatusOK,
				Status:  metav1.StatusSuccess,
				Message: "Connection closed early",
			}
			if ev.ResponseStatus == nil && longRunningSink != nil {
				ev.ResponseStatus = fakedSuccessStatus
				ev.Stage = auditinternal.StageResponseStarted
				processAuditEvent(longRunningSink, ev, omitStages)
			}

			ev.Stage = auditinternal.StageResponseComplete
			if ev.ResponseStatus == nil {
				ev.ResponseStatus = fakedSuccessStatus
			}
			processAuditEvent(sink, ev, omitStages)
		}()
		handler.ServeHTTP(respWriter, req)
	})
}

// createAuditEventAndAttachToContext is responsible for creating the audit event
// and attaching it to the appropriate request context. It returns:
// - context with audit event attached to it
// - created audit event
// - error if anything bad happened
func createAuditEventAndAttachToContext(requestContextMapper request.RequestContextMapper, req *http.Request, policy policy.Checker) (request.Context, *auditinternal.Event, []auditinternal.Stage, error) {
	ctx, ok := requestContextMapper.Get(req)
	if !ok {
		return nil, nil, nil, fmt.Errorf("no context found for request")
	}

	attribs, err := GetAuthorizerAttributes(ctx)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
	}

	level, omitStages := policy.LevelAndStages(attribs)
	audit.ObservePolicyLevel(level)
	if level == auditinternal.LevelNone {
		// Don't audit.
		return nil, nil, nil, nil
	}

	ev, err := audit.NewEventFromRequest(req, level, attribs)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
	}

	ctx = request.WithAuditEvent(ctx, ev)
	if err := requestContextMapper.Update(req, ctx); err != nil {
		return nil, nil, nil, fmt.Errorf("failed to attach audit event to context: %v", err)
	}

	return ctx, ev, omitStages, nil
}

func processAuditEvent(sink audit.Sink, ev *auditinternal.Event, omitStages []auditinternal.Stage) {
	for _, stage := range omitStages {
		if ev.Stage == stage {
			return
		}
	}
	ev.CreationTimestamp = metav1.NewTime(time.Now())
	audit.ObserveEvent()
	sink.ProcessEvents(ev)
}

func decorateResponseWriter(responseWriter http.ResponseWriter, ev *auditinternal.Event, sink audit.Sink, omitStages []auditinternal.Stage) http.ResponseWriter {
	delegate := &auditResponseWriter{
		ResponseWriter: responseWriter,
		event:          ev,
		sink:           sink,
		omitStages:     omitStages,
	}

	// check if the ResponseWriter we're wrapping is the fancy one we need
	// or if the basic is sufficient
	_, cn := responseWriter.(http.CloseNotifier)
	_, fl := responseWriter.(http.Flusher)
	_, hj := responseWriter.(http.Hijacker)
	if cn && fl && hj {
		return &fancyResponseWriterDelegator{delegate}
	}
	return delegate
}

var _ http.ResponseWriter = &auditResponseWriter{}

// auditResponseWriter intercepts WriteHeader, sets it in the event. If the sink is set, it will
// create immediately an event (for long running requests).
type auditResponseWriter struct {
	http.ResponseWriter
	event      *auditinternal.Event
	once       sync.Once
	sink       audit.Sink
	omitStages []auditinternal.Stage
}

func (a *auditResponseWriter) setHttpHeader() {
	a.ResponseWriter.Header().Set(auditinternal.HeaderAuditID, string(a.event.AuditID))
}

func (a *auditResponseWriter) processCode(code int) {
	a.once.Do(func() {
		if a.event.ResponseStatus == nil {
			a.event.ResponseStatus = &metav1.Status{}
		}
		a.event.ResponseStatus.Code = int32(code)
		a.event.Stage = auditinternal.StageResponseStarted

		if a.sink != nil {
			processAuditEvent(a.sink, a.event, a.omitStages)
		}
	})
}

func (a *auditResponseWriter) Write(bs []byte) (int, error) {
	// the Go library calls WriteHeader internally if no code was written yet. But this will go unnoticed for us
	a.processCode(http.StatusOK)
	a.setHttpHeader()
	return a.ResponseWriter.Write(bs)
}

func (a *auditResponseWriter) WriteHeader(code int) {
	a.processCode(code)
	a.setHttpHeader()
	a.ResponseWriter.WriteHeader(code)
}

// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc)
// working.
type fancyResponseWriterDelegator struct {
	*auditResponseWriter
}

func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
	return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

func (f *fancyResponseWriterDelegator) Flush() {
	f.ResponseWriter.(http.Flusher).Flush()
}

func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	// fake a response status before protocol switch happens
	f.processCode(http.StatusSwitchingProtocols)

	// This will be ignored if WriteHeader() function has aready been called.
	// It's not guaranteed Audit-ID http header is sent for all requests.
	// For example, when user run "kubectl exec", apiserver uses a proxy handler
	// to deal with the request, users can only get http headers returned by kubelet node.
	f.setHttpHeader()

	return f.ResponseWriter.(http.Hijacker).Hijack()
}

var _ http.CloseNotifier = &fancyResponseWriterDelegator{}
var _ http.Flusher = &fancyResponseWriterDelegator{}
var _ http.Hijacker = &fancyResponseWriterDelegator{}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Fission 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 fission_cli

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/hashicorp/go-multierror"
	"github.com/satori/go.uuid"
	"github.com/urfave/cli"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
	ferror "github.com/fission/fission/pkg/error"
	"github.com/fission/fission/pkg/fission-cli/cmd/httptrigger"
	"github.com/fission/fission/pkg/fission-cli/cmd/spec"
	"github.com/fission/fission/pkg/fission-cli/log"
	"github.com/fission/fission/pkg/fission-cli/util"
)

// returns one of http.Method*
func getMethod(method string) string {
	switch strings.ToUpper(method) {
	case "GET":
		return http.MethodGet
	case "HEAD":
		return http.MethodHead
	case "POST":
		return http.MethodPost
	case "PUT":
		return http.MethodPut
	case "PATCH":
		return http.MethodPatch
	case "DELETE":
		return http.MethodDelete
	case "CONNECT":
		return http.MethodConnect
	case "OPTIONS":
		return http.MethodOptions
	case "TRACE":
		return http.MethodTrace
	}
	log.Fatal(fmt.Sprintf("Invalid HTTP Method %v", method))
	return ""
}

func setHtFunctionRef(functionList []string, functionWeightsList []int) (*fv1.FunctionReference, error) {
	if len(functionList) == 1 {
		return &fv1.FunctionReference{
			Type: fv1.FunctionReferenceTypeFunctionName,
			Name: functionList[0],
		}, nil
	} else if len(functionList) == 2 {
		if len(functionWeightsList) != 2 {
			return nil, fmt.Errorf("weights of the function need to be specified when 2 functions are supplied")
		}

		totalWeight := functionWeightsList[0] + functionWeightsList[1]
		if totalWeight != 100 {
			log.Fatal("The function weights should add up to 100")
		}

		functionWeights := make(map[string]int)
		for index := range functionList {
			functionWeights[functionList[index]] = functionWeightsList[index]
		}

		return &fv1.FunctionReference{
			Type:            fv1.FunctionReferenceTypeFunctionWeights,
			FunctionWeights: functionWeights,
		}, nil
	}

	return nil, fmt.Errorf("the number of functions in a trigger can be 1 or 2(for canary feature along with their weights)")
}

func htCreate(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))

	functionList := c.StringSlice("function")
	functionWeightsList := c.IntSlice("weight")

	if len(functionList) == 0 {
		log.Fatal("Need a function name to create a trigger, use --function")
	}

	functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
	if err != nil {
		log.Fatal(err.Error())
	}

	triggerName := c.String("name")
	fnNamespace := c.String("fnNamespace")
	toSpec := c.Bool("spec")

	m := &metav1.ObjectMeta{
		Name:      triggerName,
		Namespace: fnNamespace,
	}

	htTrigger, err := client.HTTPTriggerGet(m)
	if err != nil && !ferror.IsNotFound(err) {
		log.Fatal(err.Error())
	}
	if htTrigger != nil {
		util.CheckErr(fmt.Errorf("duplicate trigger exists"), "choose a different name or leave it empty for fission to auto-generate it")
	}

	triggerUrl := c.String("url")
	if len(triggerUrl) == 0 {
		log.Fatal("Need a trigger URL, use --url")
	}
	if !strings.HasPrefix(triggerUrl, "/") {
		triggerUrl = fmt.Sprintf("/%s", triggerUrl)
	}

	method := c.String("method")
	if len(method) == 0 {
		method = "GET"
	}

	// For Specs, the spec validate checks for function reference
	if !toSpec {
		err = util.CheckFunctionExistence(client, functionList, fnNamespace)
		if err != nil {
			log.Warn(err.Error())
		}
	}

	createIngress := c.Bool("createingress")
	ingressConfig, err := httptrigger.GetIngressConfig(
		c.StringSlice("ingressannotation"), c.String("ingressrule"),
		c.String("ingresstls"), triggerUrl, nil)
	util.CheckErr(err, "parse ingress configuration")

	host := c.String("host")
	if c.IsSet("host") {
		log.Warn(fmt.Sprintf("--host is now marked as deprecated, see 'help' for details"))
	}

	// just name triggers by uuid.
	if triggerName == "" {
		triggerName = uuid.NewV4().String()
	}

	ht := &fv1.HTTPTrigger{
		Metadata: metav1.ObjectMeta{
			Name:      triggerName,
			Namespace: fnNamespace,
		},
		Spec: fv1.HTTPTriggerSpec{
			Host:              host,
			RelativeURL:       triggerUrl,
			Method:            getMethod(method),
			FunctionReference: *functionRef,
			CreateIngress:     createIngress,
			IngressConfig:     *ingressConfig,
		},
	}

	// if we're writing a spec, don't call the API
	if toSpec {
		specFile := fmt.Sprintf("route-%v.yaml", triggerName)
		err := spec.SpecSave(*ht, specFile)
		util.CheckErr(err, "create HTTP trigger spec")
		return nil
	}

	_, err = client.HTTPTriggerCreate(ht)
	util.CheckErr(err, "create HTTP trigger")

	fmt.Printf("trigger '%v' created\n", triggerName)
	return err
}

func htGet(c *cli.Context) error {
	cliClient := util.GetApiClient(c.GlobalString("server"))

	name := c.String("name")
	ns := c.String("fnNamespace")

	m := &metav1.ObjectMeta{
		Name:      name,
		Namespace: ns,
	}

	htTrigger, err := cliClient.HTTPTriggerGet(m)
	util.CheckErr(err, "get http trigger")

	w := tabwriter.NewWriter(os.Stdout, 0, 1, 1, ' ', 0)

	fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "UID", "METHOD", "RELATIVE-URL", "FUNCTION-REFERENCE-TYPE", "FUNCTION(s)")

	function := ""
	if htTrigger.Spec.FunctionReference.Type == fv1.FunctionReferenceTypeFunctionName {
		function = htTrigger.Spec.FunctionReference.Name
	} else {
		for k, v := range htTrigger.Spec.FunctionReference.FunctionWeights {
			function += fmt.Sprintf("%s:%v ", k, v)
		}
	}

	fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
		htTrigger.Metadata.Name, htTrigger.Metadata.UID, htTrigger.Spec.Method, htTrigger.Spec.RelativeURL,
		htTrigger.Spec.FunctionReference.Type, function)

	w.Flush()

	return err
}

func htUpdate(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))
	htName := c.String("name")
	if len(htName) == 0 {
		log.Fatal("Need name of trigger, use --name")
	}
	triggerNamespace := c.String("triggerNamespace")

	ht, err := client.HTTPTriggerGet(&metav1.ObjectMeta{
		Name:      htName,
		Namespace: triggerNamespace,
	})
	util.CheckErr(err, "get HTTP trigger")

	if c.IsSet("function") {
		// get the functions and their weights if specified
		functionList := c.StringSlice("function")
		err := util.CheckFunctionExistence(client, functionList, triggerNamespace)
		if err != nil {
			if err != nil {
				log.Warn(err.Error())
			}
		}

		var functionWeightsList []int
		if c.IsSet("weight") {
			functionWeightsList = c.IntSlice("weight")
		}

		// set function reference
		functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
		if err != nil {
			log.Fatal(err.Error())
		}

		ht.Spec.FunctionReference = *functionRef
	}

	if c.IsSet("createingress") {
		ht.Spec.CreateIngress = c.Bool("createingress")
	}

	if c.IsSet("host") {
		ht.Spec.Host = c.String("host")
		log.Warn(fmt.Sprintf("--host is now marked as deprecated, see 'help' for details"))
	}

	if c.IsSet("ingressrule") || c.IsSet("ingressannotation") || c.IsSet("ingresstls") {
		_, err = httptrigger.GetIngressConfig(
			c.StringSlice("ingressannotation"), c.String("ingressrule"),
			c.String("ingresstls"), ht.Spec.RelativeURL, &ht.Spec.IngressConfig)
		util.CheckErr(err, "parse ingress configuration")
	}

	_, err = client.HTTPTriggerUpdate(ht)
	util.CheckErr(err, "update HTTP trigger")

	fmt.Printf("trigger '%v' updated\n", htName)
	return nil
}

func htDelete(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))

	htName := c.String("name")
	fnName := c.String("function")
	if len(htName) == 0 && len(fnName) == 0 {
		log.Fatal("Need --name or --function")
	} else if len(htName) > 0 && len(fnName) > 0 {
		log.Fatal("Need either of --name or --function and not both arguments")
	}

	triggerNamespace := c.String("triggerNamespace")

	triggers, err := client.HTTPTriggerList(triggerNamespace)
	util.CheckErr(err, "get HTTP trigger list")

	var triggersToDelete []string

	if len(fnName) > 0 {
		for _, trigger := range triggers {
			// TODO: delete canary http triggers as well.
			if trigger.Spec.FunctionReference.Name == fnName {
				triggersToDelete = append(triggersToDelete, trigger.Metadata.Name)
			}
		}
	} else {
		triggersToDelete = []string{htName}
	}

	errs := &multierror.Error{}

	for _, name := range triggersToDelete {
		err := client.HTTPTriggerDelete(&metav1.ObjectMeta{
			Name:      name,
			Namespace: triggerNamespace,
		})
		if err != nil {
			errs = multierror.Append(errs, err)
		} else {
			fmt.Printf("trigger '%v' deleted\n", name)
		}
	}

	util.CheckErr(errs.ErrorOrNil(), "delete trigger(s)")

	return nil
}

func htList(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))
	triggerNamespace := c.String("triggerNamespace")
	fnName := c.String("function")

	hts, err := client.HTTPTriggerList(triggerNamespace)
	util.CheckErr(err, "list HTTP triggers")

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)

	fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "METHOD", "HOST", "URL", "INGRESS", "FUNCTION_NAME")

	for _, ht := range hts {
		// TODO: list canary http triggers as well.
		if len(fnName) == 0 || (len(fnName) > 0 && fnName == ht.Spec.FunctionReference.Name) {
			fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
				ht.Metadata.Name, ht.Spec.Method, ht.Spec.Host, ht.Spec.RelativeURL, ht.Spec.CreateIngress, ht.Spec.FunctionReference.Name)
		}
	}
	w.Flush()

	return nil
}
<commit_msg>Fix empty host value when list http triggers (#1328)<commit_after>/*
Copyright 2016 The Fission 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 fission_cli

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/hashicorp/go-multierror"
	"github.com/satori/go.uuid"
	"github.com/urfave/cli"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
	ferror "github.com/fission/fission/pkg/error"
	"github.com/fission/fission/pkg/fission-cli/cmd/httptrigger"
	"github.com/fission/fission/pkg/fission-cli/cmd/spec"
	"github.com/fission/fission/pkg/fission-cli/log"
	"github.com/fission/fission/pkg/fission-cli/util"
)

// returns one of http.Method*
func getMethod(method string) string {
	switch strings.ToUpper(method) {
	case "GET":
		return http.MethodGet
	case "HEAD":
		return http.MethodHead
	case "POST":
		return http.MethodPost
	case "PUT":
		return http.MethodPut
	case "PATCH":
		return http.MethodPatch
	case "DELETE":
		return http.MethodDelete
	case "CONNECT":
		return http.MethodConnect
	case "OPTIONS":
		return http.MethodOptions
	case "TRACE":
		return http.MethodTrace
	}
	log.Fatal(fmt.Sprintf("Invalid HTTP Method %v", method))
	return ""
}

func setHtFunctionRef(functionList []string, functionWeightsList []int) (*fv1.FunctionReference, error) {
	if len(functionList) == 1 {
		return &fv1.FunctionReference{
			Type: fv1.FunctionReferenceTypeFunctionName,
			Name: functionList[0],
		}, nil
	} else if len(functionList) == 2 {
		if len(functionWeightsList) != 2 {
			return nil, fmt.Errorf("weights of the function need to be specified when 2 functions are supplied")
		}

		totalWeight := functionWeightsList[0] + functionWeightsList[1]
		if totalWeight != 100 {
			log.Fatal("The function weights should add up to 100")
		}

		functionWeights := make(map[string]int)
		for index := range functionList {
			functionWeights[functionList[index]] = functionWeightsList[index]
		}

		return &fv1.FunctionReference{
			Type:            fv1.FunctionReferenceTypeFunctionWeights,
			FunctionWeights: functionWeights,
		}, nil
	}

	return nil, fmt.Errorf("the number of functions in a trigger can be 1 or 2(for canary feature along with their weights)")
}

func htCreate(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))

	functionList := c.StringSlice("function")
	functionWeightsList := c.IntSlice("weight")

	if len(functionList) == 0 {
		log.Fatal("Need a function name to create a trigger, use --function")
	}

	functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
	if err != nil {
		log.Fatal(err.Error())
	}

	triggerName := c.String("name")
	fnNamespace := c.String("fnNamespace")
	toSpec := c.Bool("spec")

	m := &metav1.ObjectMeta{
		Name:      triggerName,
		Namespace: fnNamespace,
	}

	htTrigger, err := client.HTTPTriggerGet(m)
	if err != nil && !ferror.IsNotFound(err) {
		log.Fatal(err.Error())
	}
	if htTrigger != nil {
		util.CheckErr(fmt.Errorf("duplicate trigger exists"), "choose a different name or leave it empty for fission to auto-generate it")
	}

	triggerUrl := c.String("url")
	if len(triggerUrl) == 0 {
		log.Fatal("Need a trigger URL, use --url")
	}
	if !strings.HasPrefix(triggerUrl, "/") {
		triggerUrl = fmt.Sprintf("/%s", triggerUrl)
	}

	method := c.String("method")
	if len(method) == 0 {
		method = "GET"
	}

	// For Specs, the spec validate checks for function reference
	if !toSpec {
		err = util.CheckFunctionExistence(client, functionList, fnNamespace)
		if err != nil {
			log.Warn(err.Error())
		}
	}

	createIngress := c.Bool("createingress")
	ingressConfig, err := httptrigger.GetIngressConfig(
		c.StringSlice("ingressannotation"), c.String("ingressrule"),
		c.String("ingresstls"), triggerUrl, nil)
	util.CheckErr(err, "parse ingress configuration")

	host := c.String("host")
	if c.IsSet("host") {
		log.Warn(fmt.Sprintf("--host is now marked as deprecated, see 'help' for details"))
	}

	// just name triggers by uuid.
	if triggerName == "" {
		triggerName = uuid.NewV4().String()
	}

	ht := &fv1.HTTPTrigger{
		Metadata: metav1.ObjectMeta{
			Name:      triggerName,
			Namespace: fnNamespace,
		},
		Spec: fv1.HTTPTriggerSpec{
			Host:              host,
			RelativeURL:       triggerUrl,
			Method:            getMethod(method),
			FunctionReference: *functionRef,
			CreateIngress:     createIngress,
			IngressConfig:     *ingressConfig,
		},
	}

	// if we're writing a spec, don't call the API
	if toSpec {
		specFile := fmt.Sprintf("route-%v.yaml", triggerName)
		err := spec.SpecSave(*ht, specFile)
		util.CheckErr(err, "create HTTP trigger spec")
		return nil
	}

	_, err = client.HTTPTriggerCreate(ht)
	util.CheckErr(err, "create HTTP trigger")

	fmt.Printf("trigger '%v' created\n", triggerName)
	return err
}

func htGet(c *cli.Context) error {
	cliClient := util.GetApiClient(c.GlobalString("server"))

	name := c.String("name")
	ns := c.String("fnNamespace")

	if len(name) <= 0 {
		log.Fatal("Need a trigger name, use --name")
	}

	m := &metav1.ObjectMeta{
		Name:      name,
		Namespace: ns,
	}
	ht, err := cliClient.HTTPTriggerGet(m)
	util.CheckErr(err, "get http trigger")

	printHtSummary([]fv1.HTTPTrigger{*ht})
	return err
}

func htUpdate(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))
	htName := c.String("name")
	if len(htName) == 0 {
		log.Fatal("Need name of trigger, use --name")
	}
	triggerNamespace := c.String("triggerNamespace")

	ht, err := client.HTTPTriggerGet(&metav1.ObjectMeta{
		Name:      htName,
		Namespace: triggerNamespace,
	})
	util.CheckErr(err, "get HTTP trigger")

	if c.IsSet("function") {
		// get the functions and their weights if specified
		functionList := c.StringSlice("function")
		err := util.CheckFunctionExistence(client, functionList, triggerNamespace)
		if err != nil {
			if err != nil {
				log.Warn(err.Error())
			}
		}

		var functionWeightsList []int
		if c.IsSet("weight") {
			functionWeightsList = c.IntSlice("weight")
		}

		// set function reference
		functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
		if err != nil {
			log.Fatal(err.Error())
		}

		ht.Spec.FunctionReference = *functionRef
	}

	if c.IsSet("createingress") {
		ht.Spec.CreateIngress = c.Bool("createingress")
	}

	if c.IsSet("host") {
		ht.Spec.Host = c.String("host")
		log.Warn(fmt.Sprintf("--host is now marked as deprecated, see 'help' for details"))
	}

	if c.IsSet("ingressrule") || c.IsSet("ingressannotation") || c.IsSet("ingresstls") {
		_, err = httptrigger.GetIngressConfig(
			c.StringSlice("ingressannotation"), c.String("ingressrule"),
			c.String("ingresstls"), ht.Spec.RelativeURL, &ht.Spec.IngressConfig)
		util.CheckErr(err, "parse ingress configuration")
	}

	_, err = client.HTTPTriggerUpdate(ht)
	util.CheckErr(err, "update HTTP trigger")

	fmt.Printf("trigger '%v' updated\n", htName)
	return nil
}

func htDelete(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))

	htName := c.String("name")
	fnName := c.String("function")
	if len(htName) == 0 && len(fnName) == 0 {
		log.Fatal("Need --name or --function")
	} else if len(htName) > 0 && len(fnName) > 0 {
		log.Fatal("Need either of --name or --function and not both arguments")
	}

	triggerNamespace := c.String("triggerNamespace")

	triggers, err := client.HTTPTriggerList(triggerNamespace)
	util.CheckErr(err, "get HTTP trigger list")

	var triggersToDelete []string

	if len(fnName) > 0 {
		for _, trigger := range triggers {
			// TODO: delete canary http triggers as well.
			if trigger.Spec.FunctionReference.Name == fnName {
				triggersToDelete = append(triggersToDelete, trigger.Metadata.Name)
			}
		}
	} else {
		triggersToDelete = []string{htName}
	}

	errs := &multierror.Error{}

	for _, name := range triggersToDelete {
		err := client.HTTPTriggerDelete(&metav1.ObjectMeta{
			Name:      name,
			Namespace: triggerNamespace,
		})
		if err != nil {
			errs = multierror.Append(errs, err)
		} else {
			fmt.Printf("trigger '%v' deleted\n", name)
		}
	}

	util.CheckErr(errs.ErrorOrNil(), "delete trigger(s)")

	return nil
}

func htList(c *cli.Context) error {
	client := util.GetApiClient(c.GlobalString("server"))
	triggerNamespace := c.String("triggerNamespace")
	fnName := c.String("function")

	hts, err := client.HTTPTriggerList(triggerNamespace)
	util.CheckErr(err, "list HTTP triggers")

	var triggers []fv1.HTTPTrigger
	for _, ht := range hts {
		// TODO: list canary http triggers as well.
		if len(fnName) == 0 || (len(fnName) > 0 && fnName == ht.Spec.FunctionReference.Name) {
			triggers = append(triggers, ht)
		}
	}

	printHtSummary(triggers)
	return nil
}

func printHtSummary(triggers []fv1.HTTPTrigger) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
	fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "METHOD", "URL", "FUNCTION(s)", "INGRESS", "HOST", "PATH", "TLS", "ANNOTATIONS")
	for _, trigger := range triggers {
		function := ""
		if trigger.Spec.FunctionReference.Type == fv1.FunctionReferenceTypeFunctionName {
			function = trigger.Spec.FunctionReference.Name
		} else {
			for k, v := range trigger.Spec.FunctionReference.FunctionWeights {
				function += fmt.Sprintf("%s:%v ", k, v)
			}
		}

		host := trigger.Spec.Host
		if len(trigger.Spec.IngressConfig.Host) > 0 {
			host = trigger.Spec.IngressConfig.Host
		}
		path := trigger.Spec.RelativeURL
		if len(trigger.Spec.IngressConfig.Path) > 0 {
			path = trigger.Spec.IngressConfig.Path
		}

		var msg []string
		for k, v := range trigger.Spec.IngressConfig.Annotations {
			msg = append(msg, fmt.Sprintf("%v: %v", k, v))
		}
		ann := strings.Join(msg, ", ")

		fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
			trigger.Metadata.Name, trigger.Spec.Method, trigger.Spec.RelativeURL, function, trigger.Spec.CreateIngress, host, path, trigger.Spec.IngressConfig.TLS, ann)
	}
	w.Flush()
}
<|endoftext|>


<commit_before>/*
 Copyright 2010 Jeremy Wall (jeremy@marzhillstudios.com)
 Use of this source code is governed by the Artistic License 2.0.
 That License is included in the LICENSE file.
*/
package transform

import (
	. "html"
	v "container/vector"
	"log"
	s "strings"
)

type SelectorQuery struct {
	*v.Vector
}

type SelectorPart struct {
	Type    byte // a bitmask of the selector types
	Val     string // the value we are matching against
}

type Selector struct {
	TagName string // "*" for any tag otherwise the name of the tag
	Parts []SelectorPart
	Attr map[string]string
}

const (
	TAGNAME byte = iota // zero value so the default
	CLASS   byte = '.'
	ID      byte = '#'
	PSEUDO  byte = ':'
	ANY     byte = '*'
	ATTR    byte = '['
)

const (
	SELECTOR_CHARS string = ".:#["
)

func matchAttrib(nodeAttr []Attribute, matchAttr map[string]string) bool {
	attribResult := true
	for key, val := range matchAttr {
		exists := false
		matched := false
		for _, attr := range nodeAttr {
			if key == attr.Key {
				exists = true
				if val == attr.Val {
					matched = true
				}
				attribResult = attribResult && exists && matched
			}
		}
		attribResult = attribResult && exists
	}
	return attribResult
}

func (part SelectorPart) match(node *Node) bool {
	switch part.Type {
		case CLASS:
			classAttr := make(map[string]string)
			classAttr["class"] = part.Val
			return matchAttrib(node.Attr, classAttr)
		case ID:
			idAttr := make(map[string]string)
			idAttr["id"] = part.Val
			return matchAttrib(node.Attr, idAttr)
		case PSEUDO:
	}
	return false
}

func (sel *Selector) Match(node *Node) bool {
	tagNameResult := true
	if sel.TagName != "" && sel.TagName != "*" && sel.TagName != node.Data {
		tagNameResult = tagNameResult && false
	}
	attribResult := matchAttrib(node.Attr, sel.Attr)
	// TODO(jwall): hook in the whole part matching
	partsResult := true
	for _, part := range sel.Parts {
		partsResult = partsResult && part.match(node)
	}
	return partsResult && tagNameResult && attribResult
}

func newAnyTagClassOrIdSelector(str string) *Selector {
	return &Selector{
	Parts:    []SelectorPart{
			SelectorPart{
			Type: str[0],
			Val:     str[1:],
		}},
	TagName: "*",
	}
}

func newAnyTagSelector(str string) *Selector {
	return &Selector{
		TagName: "*",
	}
}

func splitAttrs(str string) []string {
	attrs := s.FieldsFunc(str[1:len(str)-1], func(c int) bool {
		if c == '=' {
			return true
		}
		return false
	})
	return attrs
}

func newAnyTagAttrSelector(str string) *Selector {
	parts := s.SplitAfter(str, "]", -1)
	sel := Selector{
		TagName: "*",
		Attr:    map[string]string{},
	}
	for _, part := range parts[0:len(parts)-1] {
		attrs := splitAttrs(part)
		sel.Attr[attrs[0]] = attrs[1]
	}
	return &sel
}

func newTagNameSelector(str string) *Selector {
	return &Selector{
		TagName: str,
	}
}

func newTagNameWithConstraints(str string, i int) *Selector {
	// TODO(jwall): indexAny use [CLASS,...]
	var selector = new(Selector)
	switch str[i] {
	case CLASS, ID, PSEUDO: // with class or id
		selector = newAnyTagClassOrIdSelector(str[i:])
	case ATTR: // with attribute
		selector = newAnyTagAttrSelector(str[i:])
	default:
		panic("Invalid constraint type for the tagname selector")
	}
	selector.TagName = str[0:i]
	//selector.Type = TAGNAME
	return selector
}

func partition(s string, f func(c int) bool) []string {
	// TODO(jwall): make this more efficient
	parts := []string{}
	start := 0
	for i, char := range s {
		if i < 1 { // we don't want empty first partitions
			continue
		}
		if f(char) {
			parts = append(parts, s[start:i])
			start = i
		}
	}
	parts = append(parts, s[start:])
	return parts
}

func MergeSelectors(sel1 *Selector, sel2 *Selector) {
	if sel2.TagName != "" && sel2.TagName != "*" {
		if sel1.TagName == "" || sel1.TagName == "*" {
			sel1.TagName = sel2.TagName
		} else {
			log.Panicf("Can't merge multiple tagnames in" +
				" selectors: First [%s] Second [%s]",
				sel1.TagName, sel2.TagName)
		}
	}
	if sel1.Parts == nil {
		sel1.Parts = make([]SelectorPart, 0)
	}
	newParts := make([]SelectorPart, len(sel1.Parts) + len(sel2.Parts))
	copy(newParts, sel1.Parts)
	copy(newParts[len(sel1.Parts):], sel2.Parts)
	sel1.Parts = newParts
	if sel1.Attr == nil {
		sel1.Attr = make(map[string]string)
	}
	for key, val := range sel2.Attr {
		sel1.Attr[key] = val
	}
}

func NewSelector(str string) *Selector {
	str = s.TrimSpace(str) // trim whitespace
	// TODO(jwall): support combinators > + \S
	// TODO(jwall): splitAfter one of ".:#["
	parts := partition(str, func(c int) bool {
		for _, c2 := range SELECTOR_CHARS {
			if c == c2 {
				return true
			}
		}
		return false
	})
	result := new(Selector)
	result.TagName = "*"
	for _, p := range parts {
		selector := new(Selector)
		switch p[0] {
		case CLASS, ID: // Any tagname with class or id
			selector = newAnyTagClassOrIdSelector(p)
		case ANY: // Any tagname
			selector = newAnyTagSelector(p)
		case ATTR: // any tagname with attribute
			// TODO(jwall): expanded attribute selectors
			selector = newAnyTagAttrSelector(p)
		default: // TAGNAME
			// TODO(jwall): indexAny use [CLASS,...]
			if i := s.IndexAny(p, SELECTOR_CHARS); i != -1 {
				selector = newTagNameWithConstraints(p, i)
			} else { // just a tagname
				selector = newTagNameSelector(p)
			}
		}
		MergeSelectors(result, selector)
	}
	return result
}

func NewSelectorQuery(sel ...string) *SelectorQuery {
	q := SelectorQuery{Vector: new(v.Vector)}
	for _, str := range sel {
		selPart := NewSelector(str)
		if selPart == nil {
			log.Panic("Invalid Selector in query")
		}
		q.Push(selPart)
	}
	return &q
}

func testSelectorAttrs(attrs []Attribute, sel *Selector) bool {
	result := false
	for key, val := range sel.Attr {
		result = result || testAttr(attrs, key, val)
	}
	return result
}

func testAttr(attrs []Attribute, key string, val string) bool {
	for _, attr := range attrs {
		// TODO(jwall): we need to handle the multiple match types
		// [att] [att=val] [att~=val] [att|=val]?
		if attr.Key == key && attr.Val == val {
			return true
		}
	}
	return false
}

func testNode(node *Node, sel Selector) bool {
	/*
	if sel.TagName == "*" {
		attrs := node.Attr
		switch sel.Type {
		case ID:
			if testAttr(attrs, "id",  sel.Val) {
				return true
			}
		case CLASS:
			if testAttr(attrs, "class", sel.Val) {
				return true
			}
		case ATTR:
			if testSelectorAttrs(attrs, &sel) {
				return true
			}
			//case PSEUDO:
			//TODO(jwall): implement these
		}
	} else {
		if node.Data == sel.TagName {
			attrs := node.Attr
			switch sel.Type {
			case ID:
				if testAttr(attrs, "id", sel.Val) {
					return true
				}
			case CLASS:
				if testAttr(attrs, "class", sel.Val) {
					return true
				}
			case ATTR:
				if testSelectorAttrs(attrs, &sel) {
					return true
				}
			//case PSEUDO:
			//TODO(jwall): implement these
			default:
				return true
			}
		}
	}
	*/
	return false
}

/*
 Apply the css selector to a document.

 Returns a Vector of nodes that the selector matched.
*/
func (sel *SelectorQuery) Apply(doc *Document) *v.Vector {
	interesting := new(v.Vector)
	f := func(n *Node) {
		if sel.At(0).(*Selector).Match(n) {
			interesting.Push(n)
		}
	}
	doc.Walk(f)
	return interesting
}

/*
 Replace each node the selector matches with the passed in node.

 Applies the selector against the doc and replaces the returned
 Nodes with the passed in HtmlNode.
*/
func (sel *SelectorQuery) Replace(doc *Document, n *Node) {
	nv := sel.Apply(doc)
	for i := 0; i <= nv.Len(); i++ {
		// Change to take into account new usage of *Node
		//nv.At(i).(*Node).Copy(n)
	}
	return
}
<commit_msg>Removed old non applicable code yay!!<commit_after>/*
 Copyright 2010 Jeremy Wall (jeremy@marzhillstudios.com)
 Use of this source code is governed by the Artistic License 2.0.
 That License is included in the LICENSE file.
*/
package transform

import (
	. "html"
	v "container/vector"
	"log"
	s "strings"
)

type SelectorQuery struct {
	*v.Vector
}

type SelectorPart struct {
	Type    byte // a bitmask of the selector types
	Val     string // the value we are matching against
}

type Selector struct {
	TagName string // "*" for any tag otherwise the name of the tag
	Parts []SelectorPart
	Attr map[string]string
}

const (
	TAGNAME byte = iota // zero value so the default
	CLASS   byte = '.'
	ID      byte = '#'
	PSEUDO  byte = ':'
	ANY     byte = '*'
	ATTR    byte = '['
)

const (
	SELECTOR_CHARS string = ".:#["
)

func matchAttrib(nodeAttr []Attribute, matchAttr map[string]string) bool {
	attribResult := true
	for key, val := range matchAttr {
		exists := false
		matched := false
		for _, attr := range nodeAttr {
			if key == attr.Key {
				exists = true
				if val == attr.Val {
					matched = true
				}
				attribResult = attribResult && exists && matched
			}
		}
		attribResult = attribResult && exists
	}
	return attribResult
}

func (part SelectorPart) match(node *Node) bool {
	switch part.Type {
		case CLASS:
			classAttr := make(map[string]string)
			classAttr["class"] = part.Val
			return matchAttrib(node.Attr, classAttr)
		case ID:
			idAttr := make(map[string]string)
			idAttr["id"] = part.Val
			return matchAttrib(node.Attr, idAttr)
		case PSEUDO:
	}
	return false
}

func (sel *Selector) Match(node *Node) bool {
	tagNameResult := true
	if sel.TagName != "" && sel.TagName != "*" && sel.TagName != node.Data {
		tagNameResult = tagNameResult && false
	}
	attribResult := matchAttrib(node.Attr, sel.Attr)
	// TODO(jwall): hook in the whole part matching
	partsResult := true
	for _, part := range sel.Parts {
		partsResult = partsResult && part.match(node)
	}
	return partsResult && tagNameResult && attribResult
}

func newAnyTagClassOrIdSelector(str string) *Selector {
	return &Selector{
	Parts:    []SelectorPart{
			SelectorPart{
			Type: str[0],
			Val:     str[1:],
		}},
	TagName: "*",
	}
}

func newAnyTagSelector(str string) *Selector {
	return &Selector{
		TagName: "*",
	}
}

func splitAttrs(str string) []string {
	attrs := s.FieldsFunc(str[1:len(str)-1], func(c int) bool {
		if c == '=' {
			return true
		}
		return false
	})
	return attrs
}

func newAnyTagAttrSelector(str string) *Selector {
	parts := s.SplitAfter(str, "]", -1)
	sel := Selector{
		TagName: "*",
		Attr:    map[string]string{},
	}
	for _, part := range parts[0:len(parts)-1] {
		attrs := splitAttrs(part)
		sel.Attr[attrs[0]] = attrs[1]
	}
	return &sel
}

func newTagNameSelector(str string) *Selector {
	return &Selector{
		TagName: str,
	}
}

func newTagNameWithConstraints(str string, i int) *Selector {
	// TODO(jwall): indexAny use [CLASS,...]
	var selector = new(Selector)
	switch str[i] {
	case CLASS, ID, PSEUDO: // with class or id
		selector = newAnyTagClassOrIdSelector(str[i:])
	case ATTR: // with attribute
		selector = newAnyTagAttrSelector(str[i:])
	default:
		panic("Invalid constraint type for the tagname selector")
	}
	selector.TagName = str[0:i]
	//selector.Type = TAGNAME
	return selector
}

func partition(s string, f func(c int) bool) []string {
	// TODO(jwall): make this more efficient
	parts := []string{}
	start := 0
	for i, char := range s {
		if i < 1 { // we don't want empty first partitions
			continue
		}
		if f(char) {
			parts = append(parts, s[start:i])
			start = i
		}
	}
	parts = append(parts, s[start:])
	return parts
}

func MergeSelectors(sel1 *Selector, sel2 *Selector) {
	if sel2.TagName != "" && sel2.TagName != "*" {
		if sel1.TagName == "" || sel1.TagName == "*" {
			sel1.TagName = sel2.TagName
		} else {
			log.Panicf("Can't merge multiple tagnames in" +
				" selectors: First [%s] Second [%s]",
				sel1.TagName, sel2.TagName)
		}
	}
	if sel1.Parts == nil {
		sel1.Parts = make([]SelectorPart, 0)
	}
	newParts := make([]SelectorPart, len(sel1.Parts) + len(sel2.Parts))
	copy(newParts, sel1.Parts)
	copy(newParts[len(sel1.Parts):], sel2.Parts)
	sel1.Parts = newParts
	if sel1.Attr == nil {
		sel1.Attr = make(map[string]string)
	}
	for key, val := range sel2.Attr {
		sel1.Attr[key] = val
	}
}

func NewSelector(str string) *Selector {
	str = s.TrimSpace(str) // trim whitespace
	// TODO(jwall): support combinators > + \S
	// TODO(jwall): splitAfter one of ".:#["
	parts := partition(str, func(c int) bool {
		for _, c2 := range SELECTOR_CHARS {
			if c == c2 {
				return true
			}
		}
		return false
	})
	result := new(Selector)
	result.TagName = "*"
	for _, p := range parts {
		selector := new(Selector)
		switch p[0] {
		case CLASS, ID: // Any tagname with class or id
			selector = newAnyTagClassOrIdSelector(p)
		case ANY: // Any tagname
			selector = newAnyTagSelector(p)
		case ATTR: // any tagname with attribute
			// TODO(jwall): expanded attribute selectors
			selector = newAnyTagAttrSelector(p)
		default: // TAGNAME
			// TODO(jwall): indexAny use [CLASS,...]
			if i := s.IndexAny(p, SELECTOR_CHARS); i != -1 {
				selector = newTagNameWithConstraints(p, i)
			} else { // just a tagname
				selector = newTagNameSelector(p)
			}
		}
		MergeSelectors(result, selector)
	}
	return result
}

func NewSelectorQuery(sel ...string) *SelectorQuery {
	q := SelectorQuery{Vector: new(v.Vector)}
	for _, str := range sel {
		selPart := NewSelector(str)
		if selPart == nil {
			log.Panic("Invalid Selector in query")
		}
		q.Push(selPart)
	}
	return &q
}

/*
 Apply the css selector to a document.

 Returns a Vector of nodes that the selector matched.
*/
func (sel *SelectorQuery) Apply(doc *Document) *v.Vector {
	interesting := new(v.Vector)
	f := func(n *Node) {
		if sel.At(0).(*Selector).Match(n) {
			interesting.Push(n)
		}
	}
	doc.Walk(f)
	return interesting
}

/*
 Replace each node the selector matches with the passed in node.

 Applies the selector against the doc and replaces the returned
 Nodes with the passed in HtmlNode.
*/
func (sel *SelectorQuery) Replace(doc *Document, n *Node) {
	nv := sel.Apply(doc)
	for i := 0; i <= nv.Len(); i++ {
		// Change to take into account new usage of *Node
		//nv.At(i).(*Node).Copy(n)
	}
	return
}
<|endoftext|>


<commit_before>/*
Copyright 2017 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 cmd

import (
	"fmt"
	"io"
	"strings"

	"github.com/spf13/cobra"

	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/kubernetes/pkg/apis/rbac"
	internalversionrbac "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion"
	"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)

var (
	roleLong = templates.LongDesc(`
		Create a role with single rule.`)

	roleExample = templates.Examples(`
		# Create a Role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
		kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods

		# Create a Role named "pod-reader" with ResourceName specified
		kubectl create role pod-reader --verb=get --verg=list --verb=watch --resource=pods --resource-name=readablepod`)

	// Valid resource verb list for validation.
	validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "redirect", "deletecollection"}
)

type CreateRoleOptions struct {
	Name          string
	Verbs         []string
	Resources     []schema.GroupVersionResource
	ResourceNames []string

	DryRun       bool
	OutputFormat string
	Namespace    string
	Client       internalversionrbac.RbacInterface
	Mapper       meta.RESTMapper
	Out          io.Writer
	PrintObject  func(obj runtime.Object) error
}

// Role is a command to ease creating Roles.
func NewCmdCreateRole(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
	c := &CreateRoleOptions{
		Out: cmdOut,
	}
	cmd := &cobra.Command{
		Use:     "role NAME --verb=verb --resource=resource.group [--resource-name=resourcename] [--dry-run]",
		Short:   roleLong,
		Long:    roleLong,
		Example: roleExample,
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(c.Complete(f, cmd, args))
			cmdutil.CheckErr(c.Validate())
			cmdutil.CheckErr(c.RunCreateRole())
		},
	}
	cmdutil.AddApplyAnnotationFlags(cmd)
	cmdutil.AddValidateFlags(cmd)
	cmdutil.AddPrinterFlags(cmd)
	cmdutil.AddDryRunFlag(cmd)
	cmd.Flags().StringSliceVar(&c.Verbs, "verb", []string{}, "verb that applies to the resources contained in the rule")
	cmd.Flags().StringSlice("resource", []string{}, "resource that the rule applies to")
	cmd.Flags().StringSliceVar(&c.ResourceNames, "resource-name", []string{}, "resource in the white list that the rule applies to")

	return cmd
}

func (c *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
	name, err := NameFromCommandArgs(cmd, args)
	if err != nil {
		return err
	}
	c.Name = name

	// Remove duplicate verbs.
	verbs := []string{}
	for _, v := range c.Verbs {
		// VerbAll respresents all kinds of verbs.
		if v == "*" {
			verbs = []string{"*"}
			break
		}
		if !arrayContains(verbs, v) {
			verbs = append(verbs, v)
		}
	}
	c.Verbs = verbs

	// Support resource.group pattern. If no API Group specified, use "" as core API Group.
	// e.g. --resource=pods,deployments.extensions
	resources := cmdutil.GetFlagStringSlice(cmd, "resource")
	for _, r := range resources {
		sections := strings.Split(r, ".")

		if len(sections) == 1 {
			c.Resources = append(c.Resources, schema.GroupVersionResource{Resource: r})
		} else {
			c.Resources = append(c.Resources, schema.GroupVersionResource{Resource: sections[0], Group: strings.Join(sections[1:], ".")})
		}
	}

	// Remove duplicate resource names.
	resourceNames := []string{}
	for _, n := range c.ResourceNames {
		if !arrayContains(resourceNames, n) {
			resourceNames = append(resourceNames, n)
		}
	}
	c.ResourceNames = resourceNames

	// Complete other options for Run.
	c.Mapper, _ = f.Object()

	c.DryRun = cmdutil.GetDryRunFlag(cmd)
	c.OutputFormat = cmdutil.GetFlagString(cmd, "output")

	c.Namespace, _, err = f.DefaultNamespace()
	if err != nil {
		return err
	}

	c.PrintObject = func(obj runtime.Object) error {
		return f.PrintObject(cmd, c.Mapper, obj, c.Out)
	}

	clientSet, err := f.ClientSet()
	if err != nil {
		return err
	}
	c.Client = clientSet.Rbac()

	return nil
}

func (c *CreateRoleOptions) Validate() error {
	if c.Name == "" {
		return fmt.Errorf("name must be specified")
	}

	// validate verbs.
	if len(c.Verbs) == 0 {
		return fmt.Errorf("at least one verb must be specified")
	}

	for _, v := range c.Verbs {
		if !arrayContains(validResourceVerbs, v) {
			return fmt.Errorf("invalid verb: '%s'", v)
		}
	}

	// validate resources.
	if len(c.Resources) == 0 {
		return fmt.Errorf("at least one resource must be specified")
	}

	for _, r := range c.Resources {
		_, err := c.Mapper.ResourceFor(r)
		if err != nil {
			return err
		}
	}

	// validate resource names, can not apply resource names to multiple resources.
	if len(c.ResourceNames) > 0 && len(c.Resources) > 1 {
		return fmt.Errorf("resource name(s) can not be applied to multiple resources")
	}

	return nil
}

func (c *CreateRoleOptions) RunCreateRole() error {
	role := &rbac.Role{}
	role.Name = c.Name
	rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames)
	if err != nil {
		return err
	}
	role.Rules = rules

	// Create role.
	if !c.DryRun {
		_, err = c.Client.Roles(c.Namespace).Create(role)
		if err != nil {
			return err
		}
	}

	if useShortOutput := c.OutputFormat == "name"; useShortOutput || len(c.OutputFormat) == 0 {
		cmdutil.PrintSuccess(c.Mapper, useShortOutput, c.Out, "roles", c.Name, c.DryRun, "created")
		return nil
	}

	return c.PrintObject(role)
}

func arrayContains(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []schema.GroupVersionResource, resourceNames []string) ([]rbac.PolicyRule, error) {
	// groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value
	// is a string array of resources under this api group.
	// E.g.  groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]}
	groupResourceMapping := map[string][]string{}

	// This loop does the following work:
	// 1. Constructs groupResourceMapping based on input resources.
	// 2. Prevents pointing to non-existent resources.
	// 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions
	for _, r := range resources {
		resource, err := mapper.ResourceFor(r)
		if err != nil {
			return []rbac.PolicyRule{}, err
		}
		if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) {
			groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource)
		}
	}

	// Create separate rule for each of the api group.
	rules := []rbac.PolicyRule{}
	for _, g := range sets.StringKeySet(groupResourceMapping).List() {
		rule := rbac.PolicyRule{}
		rule.Verbs = verbs
		rule.Resources = groupResourceMapping[g]
		rule.APIGroups = []string{g}
		rule.ResourceNames = resourceNames
		rules = append(rules, rule)
	}

	return rules, nil
}
<commit_msg>add "use" to validResourceVerbs<commit_after>/*
Copyright 2017 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 cmd

import (
	"fmt"
	"io"
	"strings"

	"github.com/spf13/cobra"

	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/kubernetes/pkg/apis/rbac"
	internalversionrbac "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion"
	"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)

var (
	roleLong = templates.LongDesc(`
		Create a role with single rule.`)

	roleExample = templates.Examples(`
		# Create a Role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
		kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods

		# Create a Role named "pod-reader" with ResourceName specified
		kubectl create role pod-reader --verb=get --verg=list --verb=watch --resource=pods --resource-name=readablepod`)

	// Valid resource verb list for validation.
	validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "redirect", "deletecollection", "use"}
)

type CreateRoleOptions struct {
	Name          string
	Verbs         []string
	Resources     []schema.GroupVersionResource
	ResourceNames []string

	DryRun       bool
	OutputFormat string
	Namespace    string
	Client       internalversionrbac.RbacInterface
	Mapper       meta.RESTMapper
	Out          io.Writer
	PrintObject  func(obj runtime.Object) error
}

// Role is a command to ease creating Roles.
func NewCmdCreateRole(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
	c := &CreateRoleOptions{
		Out: cmdOut,
	}
	cmd := &cobra.Command{
		Use:     "role NAME --verb=verb --resource=resource.group [--resource-name=resourcename] [--dry-run]",
		Short:   roleLong,
		Long:    roleLong,
		Example: roleExample,
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(c.Complete(f, cmd, args))
			cmdutil.CheckErr(c.Validate())
			cmdutil.CheckErr(c.RunCreateRole())
		},
	}
	cmdutil.AddApplyAnnotationFlags(cmd)
	cmdutil.AddValidateFlags(cmd)
	cmdutil.AddPrinterFlags(cmd)
	cmdutil.AddDryRunFlag(cmd)
	cmd.Flags().StringSliceVar(&c.Verbs, "verb", []string{}, "verb that applies to the resources contained in the rule")
	cmd.Flags().StringSlice("resource", []string{}, "resource that the rule applies to")
	cmd.Flags().StringSliceVar(&c.ResourceNames, "resource-name", []string{}, "resource in the white list that the rule applies to")

	return cmd
}

func (c *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
	name, err := NameFromCommandArgs(cmd, args)
	if err != nil {
		return err
	}
	c.Name = name

	// Remove duplicate verbs.
	verbs := []string{}
	for _, v := range c.Verbs {
		// VerbAll respresents all kinds of verbs.
		if v == "*" {
			verbs = []string{"*"}
			break
		}
		if !arrayContains(verbs, v) {
			verbs = append(verbs, v)
		}
	}
	c.Verbs = verbs

	// Support resource.group pattern. If no API Group specified, use "" as core API Group.
	// e.g. --resource=pods,deployments.extensions
	resources := cmdutil.GetFlagStringSlice(cmd, "resource")
	for _, r := range resources {
		sections := strings.Split(r, ".")

		if len(sections) == 1 {
			c.Resources = append(c.Resources, schema.GroupVersionResource{Resource: r})
		} else {
			c.Resources = append(c.Resources, schema.GroupVersionResource{Resource: sections[0], Group: strings.Join(sections[1:], ".")})
		}
	}

	// Remove duplicate resource names.
	resourceNames := []string{}
	for _, n := range c.ResourceNames {
		if !arrayContains(resourceNames, n) {
			resourceNames = append(resourceNames, n)
		}
	}
	c.ResourceNames = resourceNames

	// Complete other options for Run.
	c.Mapper, _ = f.Object()

	c.DryRun = cmdutil.GetDryRunFlag(cmd)
	c.OutputFormat = cmdutil.GetFlagString(cmd, "output")

	c.Namespace, _, err = f.DefaultNamespace()
	if err != nil {
		return err
	}

	c.PrintObject = func(obj runtime.Object) error {
		return f.PrintObject(cmd, c.Mapper, obj, c.Out)
	}

	clientSet, err := f.ClientSet()
	if err != nil {
		return err
	}
	c.Client = clientSet.Rbac()

	return nil
}

func (c *CreateRoleOptions) Validate() error {
	if c.Name == "" {
		return fmt.Errorf("name must be specified")
	}

	// validate verbs.
	if len(c.Verbs) == 0 {
		return fmt.Errorf("at least one verb must be specified")
	}

	for _, v := range c.Verbs {
		if !arrayContains(validResourceVerbs, v) {
			return fmt.Errorf("invalid verb: '%s'", v)
		}
	}

	// validate resources.
	if len(c.Resources) == 0 {
		return fmt.Errorf("at least one resource must be specified")
	}

	for _, r := range c.Resources {
		_, err := c.Mapper.ResourceFor(r)
		if err != nil {
			return err
		}
	}

	// validate resource names, can not apply resource names to multiple resources.
	if len(c.ResourceNames) > 0 && len(c.Resources) > 1 {
		return fmt.Errorf("resource name(s) can not be applied to multiple resources")
	}

	return nil
}

func (c *CreateRoleOptions) RunCreateRole() error {
	role := &rbac.Role{}
	role.Name = c.Name
	rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames)
	if err != nil {
		return err
	}
	role.Rules = rules

	// Create role.
	if !c.DryRun {
		_, err = c.Client.Roles(c.Namespace).Create(role)
		if err != nil {
			return err
		}
	}

	if useShortOutput := c.OutputFormat == "name"; useShortOutput || len(c.OutputFormat) == 0 {
		cmdutil.PrintSuccess(c.Mapper, useShortOutput, c.Out, "roles", c.Name, c.DryRun, "created")
		return nil
	}

	return c.PrintObject(role)
}

func arrayContains(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []schema.GroupVersionResource, resourceNames []string) ([]rbac.PolicyRule, error) {
	// groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value
	// is a string array of resources under this api group.
	// E.g.  groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]}
	groupResourceMapping := map[string][]string{}

	// This loop does the following work:
	// 1. Constructs groupResourceMapping based on input resources.
	// 2. Prevents pointing to non-existent resources.
	// 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions
	for _, r := range resources {
		resource, err := mapper.ResourceFor(r)
		if err != nil {
			return []rbac.PolicyRule{}, err
		}
		if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) {
			groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource)
		}
	}

	// Create separate rule for each of the api group.
	rules := []rbac.PolicyRule{}
	for _, g := range sets.StringKeySet(groupResourceMapping).List() {
		rule := rbac.PolicyRule{}
		rule.Verbs = verbs
		rule.Resources = groupResourceMapping[g]
		rule.APIGroups = []string{g}
		rule.ResourceNames = resourceNames
		rules = append(rules, rule)
	}

	return rules, nil
}
<|endoftext|>


<commit_before>package core

import (
	"database/sql"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"sync"
	"time"

	"github.com/mdlayher/wavepipe/data"

	"github.com/wtolson/go-taglib"
)

// fsFileSource represents a file source which indexes files in the local filesystem
type fsFileSource struct{}

// folderArtPair contains a folder ID and associated art ID
type folderArtPair struct {
	folderID int
	artID    int
}

// MediaScan scans for media files in the local filesystem
func (fsFileSource) MediaScan(mediaFolder string, verbose bool, walkCancelChan chan struct{}) error {
	// Halt walk if needed
	var mutex sync.RWMutex
	haltWalk := false
	go func() {
		// Wait for signal
		<-walkCancelChan

		// Halt!
		mutex.Lock()
		haltWalk = true
		mutex.Unlock()
	}()

	// Track metrics about the walk
	artCount := 0
	artistCount := 0
	albumCount := 0
	songCount := 0
	songUpdateCount := 0
	folderCount := 0
	startTime := time.Now()

	// Track all folder IDs containing new art, and hold their art IDs
	artFiles := make([]folderArtPair, 0)

	// Cache entries which have been seen previously, to reduce database load
	folderCache := map[string]*data.Folder{}
	artistCache := map[string]*data.Artist{}
	albumCache := map[string]*data.Album{}

	if verbose {
		log.Println("fs: beginning media scan:", mediaFolder)
	} else {
		log.Println("fs: scanning:", mediaFolder)
	}

	// Invoke a recursive file walk on the given media folder, passing closure variables into
	// walkFunc to enable additional functionality
	err := filepath.Walk(mediaFolder, func(currPath string, info os.FileInfo, err error) error {
		// Stop walking immediately if needed
		mutex.RLock()
		if haltWalk {
			return errors.New("media scan: halted by channel")
		}
		mutex.RUnlock()

		// Make sure path is actually valid
		if info == nil {
			return errors.New("media scan: invalid path: " + currPath)
		}

		// Check for an existing folder for this item
		folder := new(data.Folder)
		if info.IsDir() {
			// If directory, use this path
			folder.Path = currPath
		} else {
			// If file, use the directory path
			folder.Path = path.Dir(currPath)
		}

		// Check for a cached folder, or attempt to load it
		if tempFolder, ok := folderCache[folder.Path]; ok {
			folder = tempFolder
		} else if err := folder.Load(); err != nil && err == sql.ErrNoRows {
			// Make sure items actually exist at this path
			files, err := ioutil.ReadDir(folder.Path)
			if err != nil {
				log.Println(err)
				return err
			}

			// No items, skip it
			if len(files) == 0 {
				return nil
			}

			// Set short title
			folder.Title = path.Base(folder.Path)

			// Check for a parent folder
			pFolder := new(data.Folder)

			// If scan is triggered by a file, we have to check the dir twice to get parent
			if info.IsDir() {
				pFolder.Path = path.Dir(currPath)
			} else {
				pFolder.Path = path.Dir(path.Dir(currPath))
			}

			// Load parent
			if err := pFolder.Load(); err != nil && err != sql.ErrNoRows {
				log.Println(err)
				return err
			}

			// Copy parent folder's ID
			folder.ParentID = pFolder.ID

			// Save new folder
			if err := folder.Save(); err != nil {
				log.Println(err)
				return err
			}

			// Continue traversal
			folderCount++
		}

		// Cache this folder
		folderCache[folder.Path] = folder

		// Check for a valid media or art extension
		ext := path.Ext(currPath)
		if !mediaSet.Has(ext) && !artSet.Has(ext) {
			return nil
		}

		// If item is art, check for existing art
		if artSet.Has(ext) {
			// Attempt to load existing art
			art := new(data.Art)
			art.FileName = currPath
			if err := art.Load(); err == sql.ErrNoRows {
				// On new art, capture art information from filesystem
				art.FileSize = info.Size()
				art.LastModified = info.ModTime().Unix()

				// Save new art
				if err := art.Save(); err != nil {
					log.Println(err)
				} else if err == nil {
					artCount++

					// Add folder ID and to new art ID to slice
					artFiles = append(artFiles, folderArtPair{
						folderID: folder.ID,
						artID:    art.ID,
					})
				}
			}

			// Continue to next file
			return nil
		}

		// Attempt to scan media file with taglib
		file, err := taglib.Read(currPath)
		if err != nil {
			log.Println(err)
			return fmt.Errorf("%s: %s", currPath, err.Error())
		}
		defer file.Close()

		// Generate a song model from the TagLib file
		song, err := data.SongFromFile(file)
		if err != nil {
			log.Println(err)
			return err
		}

		// Populate filesystem-related struct fields using OS info
		song.FileName = currPath
		song.FileSize = info.Size()
		song.LastModified = info.ModTime().Unix()

		// Use this folder's ID
		song.FolderID = folder.ID

		// Check for a valid wavepipe file type integer
		ext = path.Ext(info.Name())
		fileType, ok := data.FileTypeMap[ext]
		if !ok {
			return fmt.Errorf("fs: invalid file type: %s", ext)
		}
		song.FileTypeID = fileType

		// Generate an artist model from this song's metadata
		artist := data.ArtistFromSong(song)

		// Check for existing artist
		// Note: if the artist exists, this operation also loads necessary scanning information
		// such as their artist ID, for use in album and song generation
		if tempArtist, ok := artistCache[artist.Title]; ok {
			artist = tempArtist
		} else if err := artist.Load(); err == sql.ErrNoRows {
			// Save new artist
			if err := artist.Save(); err != nil {
				log.Println(err)
			} else if err == nil {
				log.Printf("Artist: [#%05d] %s", artist.ID, artist.Title)
				artistCount++
			}
		}

		// Cache this artist
		artistCache[artist.Title] = artist

		// Generate the album model from this song's metadata
		album := data.AlbumFromSong(song)
		album.ArtistID = artist.ID

		// Generate cache key
		albumCacheKey := strconv.Itoa(album.ArtistID) + "_" + album.Title

		// Check for existing album
		// Note: if the album exists, this operation also loads necessary scanning information
		// such as the album ID, for use in song generation
		if tempAlbum, ok := albumCache[albumCacheKey]; ok {
			album = tempAlbum
		} else if err := album.Load(); err == sql.ErrNoRows {
			// Save album
			if err := album.Save(); err != nil {
				log.Println(err)
			} else if err == nil {
				log.Printf("  - Album: [#%05d] %s - %d - %s", album.ID, album.Artist, album.Year, album.Title)
				albumCount++
			}
		}

		// Cache this album
		albumCache[albumCacheKey] = album

		// Add ID fields to song
		song.ArtistID = artist.ID
		song.AlbumID = album.ID

		// Make a duplicate song to check if song has been modified since last scan
		song2 := new(data.Song)
		song2.FileName = song.FileName

		// Check for existing song
		if err := song2.Load(); err == sql.ErrNoRows {
			// Save song (don't log these because they really slow things down)
			if err2 := song.Save(); err2 != nil && err2 != sql.ErrNoRows {
				log.Println(err2)
			} else if err2 == nil {
				songCount++
			}
		} else {
			// Song already existed.  Check if it's been updated
			if song.LastModified > song2.LastModified {
				// Update existing song
				song.ID = song2.ID
				if err2 := song.Update(); err2 != nil {
					log.Println(err2)
				}

				songUpdateCount++
			}
		}

		// Successful media scan
		return nil
	})

	// Check for filesystem walk errors
	if err != nil {
		return err
	}

	// Iterate all new folder/art ID pairs
	for _, a := range artFiles {
		// Fetch all songs for the folder from the pair
		songs, err := data.DB.SongsForFolder(a.folderID)
		if err != nil {
			return err
		}

		// Iterate and update songs with their new art ID
		for _, s := range songs {
			s.ArtID = a.artID
			if err := s.Update(); err != nil {
				return err
			}
		}
	}

	// Print metrics
	if verbose {
		log.Printf("fs: media scan complete [time: %s]", time.Since(startTime).String())
		log.Printf("fs: added: [art: %d] [artists: %d] [albums: %d] [songs: %d] [folders: %d]",
			artCount, artistCount, albumCount, songCount, folderCount)
		log.Printf("fs: updated: [songs: %d]", songUpdateCount)
	}

	// No errors
	return nil
}

// OrphanScan scans for missing "orphaned" media files in the local filesystem
func (fsFileSource) OrphanScan(baseFolder string, subFolder string, verbose bool, orphanCancelChan chan struct{}) error {
	// Halt scan if needed
	var mutex sync.RWMutex
	haltOrphanScan := false
	go func() {
		// Wait for signal
		<-orphanCancelChan

		// Halt!
		mutex.Lock()
		haltOrphanScan = true
		mutex.Unlock()
	}()

	// Track metrics about the scan
	artCount := 0
	folderCount := 0
	songCount := 0
	startTime := time.Now()

	// Check if a baseFolder is set, meaning remove ANYTHING not under this base
	if baseFolder != "" {
		if verbose {
			log.Println("fs: orphan scanning base folder:", baseFolder)
		}

		// Scan for all art NOT under the base folder
		art, err := data.DB.ArtNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all art which is not in this path
		for _, a := range art {
			// Remove art from database
			if err := a.Delete(); err != nil {
				log.Println(err)
				return err
			}

			artCount++
		}

		// Scan for all songs NOT under the base folder
		songs, err := data.DB.SongsNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all songs which are not in this path
		for _, s := range songs {
			// Remove song from database
			if err := s.Delete(); err != nil {
				log.Println(err)
				return err
			}

			songCount++
		}

		// Scan for all folders NOT under the base folder
		folders, err := data.DB.FoldersNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all folders which are not in this path
		for _, f := range folders {
			// Remove folder from database
			if err := f.Delete(); err != nil {
				log.Println(err)
				return err
			}

			folderCount++
		}
	}

	// If no subfolder set, use the base folder to check file existence
	if subFolder == "" {
		subFolder = baseFolder
	}

	if verbose {
		log.Println("fs: orphan scanning subfolder:", subFolder)
	} else {
		log.Println("fs: removing:", subFolder)
	}

	// Scan for all art in subfolder
	art, err := data.DB.ArtInPath(subFolder)
	if err != nil {
		log.Println(err)
		return err
	}

	// Iterate all art in this path
	for _, a := range art {
		// Check that the art still exists in this place
		if _, err := os.Stat(a.FileName); os.IsNotExist(err) {
			// Remove art from database
			if err := a.Delete(); err != nil {
				log.Println(err)
				return err
			}

			artCount++
		}
	}

	// Scan for all songs in subfolder
	songs, err := data.DB.SongsInPath(subFolder)
	if err != nil {
		log.Println(err)
		return err
	}

	// Iterate all songs in this path
	for _, s := range songs {
		// Check that the song still exists in this place
		if _, err := os.Stat(s.FileName); os.IsNotExist(err) {
			// Remove song from database
			if err := s.Delete(); err != nil {
				log.Println(err)
				return err
			}

			songCount++
		}
	}

	// Scan for all folders in subfolder
	folders, err := data.DB.FoldersInPath(subFolder)
	if err != nil {
		return err
	}

	// Iterate all folders in this path
	for _, f := range folders {
		// Check that the folder still has items within it
		files, err := ioutil.ReadDir(f.Path)
		if err != nil && !os.IsNotExist(err) {
			log.Println(err)
			return err
		}

		// Delete any folders with 0 items
		if len(files) == 0 {
			if err := f.Delete(); err != nil {
				log.Println(err)
				return err
			}

			folderCount++
		}
	}

	// Now that songs have been purged, check for albums
	albumCount, err := data.DB.PurgeOrphanAlbums()
	if err != nil {
		log.Println(err)
		return err
	}

	// Check for artists
	artistCount, err := data.DB.PurgeOrphanArtists()
	if err != nil {
		log.Println(err)
		return err
	}

	// Print metrics
	if verbose {
		log.Printf("fs: orphan scan complete [time: %s]", time.Since(startTime).String())
		log.Printf("fs: removed: [art: %d] [artists: %d] [albums: %d] [songs: %d] [folders: %d]",
			artCount, artistCount, albumCount, songCount, folderCount)
	}
	return nil
}
<commit_msg>core/fsFileSource: close file handles as soon as possible<commit_after>package core

import (
	"database/sql"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"sync"
	"time"

	"github.com/mdlayher/wavepipe/data"

	"github.com/wtolson/go-taglib"
)

// fsFileSource represents a file source which indexes files in the local filesystem
type fsFileSource struct{}

// folderArtPair contains a folder ID and associated art ID
type folderArtPair struct {
	folderID int
	artID    int
}

// MediaScan scans for media files in the local filesystem
func (fsFileSource) MediaScan(mediaFolder string, verbose bool, walkCancelChan chan struct{}) error {
	// Halt walk if needed
	var mutex sync.RWMutex
	haltWalk := false
	go func() {
		// Wait for signal
		<-walkCancelChan

		// Halt!
		mutex.Lock()
		haltWalk = true
		mutex.Unlock()
	}()

	// Track metrics about the walk
	artCount := 0
	artistCount := 0
	albumCount := 0
	songCount := 0
	songUpdateCount := 0
	folderCount := 0
	startTime := time.Now()

	// Track all folder IDs containing new art, and hold their art IDs
	artFiles := make([]folderArtPair, 0)

	// Cache entries which have been seen previously, to reduce database load
	folderCache := map[string]*data.Folder{}
	artistCache := map[string]*data.Artist{}
	albumCache := map[string]*data.Album{}

	if verbose {
		log.Println("fs: beginning media scan:", mediaFolder)
	} else {
		log.Println("fs: scanning:", mediaFolder)
	}

	// Invoke a recursive file walk on the given media folder, passing closure variables into
	// walkFunc to enable additional functionality
	err := filepath.Walk(mediaFolder, func(currPath string, info os.FileInfo, err error) error {
		// Stop walking immediately if needed
		mutex.RLock()
		if haltWalk {
			return errors.New("media scan: halted by channel")
		}
		mutex.RUnlock()

		// Make sure path is actually valid
		if info == nil {
			return errors.New("media scan: invalid path: " + currPath)
		}

		// Check for an existing folder for this item
		folder := new(data.Folder)
		if info.IsDir() {
			// If directory, use this path
			folder.Path = currPath
		} else {
			// If file, use the directory path
			folder.Path = path.Dir(currPath)
		}

		// Check for a cached folder, or attempt to load it
		if tempFolder, ok := folderCache[folder.Path]; ok {
			folder = tempFolder
		} else if err := folder.Load(); err != nil && err == sql.ErrNoRows {
			// Make sure items actually exist at this path
			files, err := ioutil.ReadDir(folder.Path)
			if err != nil {
				log.Println(err)
				return err
			}

			// No items, skip it
			if len(files) == 0 {
				return nil
			}

			// Set short title
			folder.Title = path.Base(folder.Path)

			// Check for a parent folder
			pFolder := new(data.Folder)

			// If scan is triggered by a file, we have to check the dir twice to get parent
			if info.IsDir() {
				pFolder.Path = path.Dir(currPath)
			} else {
				pFolder.Path = path.Dir(path.Dir(currPath))
			}

			// Load parent
			if err := pFolder.Load(); err != nil && err != sql.ErrNoRows {
				log.Println(err)
				return err
			}

			// Copy parent folder's ID
			folder.ParentID = pFolder.ID

			// Save new folder
			if err := folder.Save(); err != nil {
				log.Println(err)
				return err
			}

			// Continue traversal
			folderCount++
		}

		// Cache this folder
		folderCache[folder.Path] = folder

		// Check for a valid media or art extension
		ext := path.Ext(currPath)
		if !mediaSet.Has(ext) && !artSet.Has(ext) {
			return nil
		}

		// If item is art, check for existing art
		if artSet.Has(ext) {
			// Attempt to load existing art
			art := new(data.Art)
			art.FileName = currPath
			if err := art.Load(); err == sql.ErrNoRows {
				// On new art, capture art information from filesystem
				art.FileSize = info.Size()
				art.LastModified = info.ModTime().Unix()

				// Save new art
				if err := art.Save(); err != nil {
					log.Println(err)
				} else if err == nil {
					artCount++

					// Add folder ID and to new art ID to slice
					artFiles = append(artFiles, folderArtPair{
						folderID: folder.ID,
						artID:    art.ID,
					})
				}
			}

			// Continue to next file
			return nil
		}

		// Attempt to scan media file with taglib
		file, err := taglib.Read(currPath)
		if err != nil {
			log.Println(err)
			return fmt.Errorf("%s: %s", currPath, err.Error())
		}

		// Generate a song model from the TagLib file
		song, err := data.SongFromFile(file)
		if err != nil {
			log.Println(err)
			return err
		}

		// Close file handle; no longer needed
		file.Close()

		// Populate filesystem-related struct fields using OS info
		song.FileName = currPath
		song.FileSize = info.Size()
		song.LastModified = info.ModTime().Unix()

		// Use this folder's ID
		song.FolderID = folder.ID

		// Check for a valid wavepipe file type integer
		ext = path.Ext(info.Name())
		fileType, ok := data.FileTypeMap[ext]
		if !ok {
			return fmt.Errorf("fs: invalid file type: %s", ext)
		}
		song.FileTypeID = fileType

		// Generate an artist model from this song's metadata
		artist := data.ArtistFromSong(song)

		// Check for existing artist
		// Note: if the artist exists, this operation also loads necessary scanning information
		// such as their artist ID, for use in album and song generation
		if tempArtist, ok := artistCache[artist.Title]; ok {
			artist = tempArtist
		} else if err := artist.Load(); err == sql.ErrNoRows {
			// Save new artist
			if err := artist.Save(); err != nil {
				log.Println(err)
			} else if err == nil {
				log.Printf("Artist: [#%05d] %s", artist.ID, artist.Title)
				artistCount++
			}
		}

		// Cache this artist
		artistCache[artist.Title] = artist

		// Generate the album model from this song's metadata
		album := data.AlbumFromSong(song)
		album.ArtistID = artist.ID

		// Generate cache key
		albumCacheKey := strconv.Itoa(album.ArtistID) + "_" + album.Title

		// Check for existing album
		// Note: if the album exists, this operation also loads necessary scanning information
		// such as the album ID, for use in song generation
		if tempAlbum, ok := albumCache[albumCacheKey]; ok {
			album = tempAlbum
		} else if err := album.Load(); err == sql.ErrNoRows {
			// Save album
			if err := album.Save(); err != nil {
				log.Println(err)
			} else if err == nil {
				log.Printf("  - Album: [#%05d] %s - %d - %s", album.ID, album.Artist, album.Year, album.Title)
				albumCount++
			}
		}

		// Cache this album
		albumCache[albumCacheKey] = album

		// Add ID fields to song
		song.ArtistID = artist.ID
		song.AlbumID = album.ID

		// Make a duplicate song to check if song has been modified since last scan
		song2 := new(data.Song)
		song2.FileName = song.FileName

		// Check for existing song
		if err := song2.Load(); err == sql.ErrNoRows {
			// Save song (don't log these because they really slow things down)
			if err2 := song.Save(); err2 != nil && err2 != sql.ErrNoRows {
				log.Println(err2)
			} else if err2 == nil {
				songCount++
			}
		} else {
			// Song already existed.  Check if it's been updated
			if song.LastModified > song2.LastModified {
				// Update existing song
				song.ID = song2.ID
				if err2 := song.Update(); err2 != nil {
					log.Println(err2)
				}

				songUpdateCount++
			}
		}

		// Successful media scan
		return nil
	})

	// Check for filesystem walk errors
	if err != nil {
		return err
	}

	// Iterate all new folder/art ID pairs
	for _, a := range artFiles {
		// Fetch all songs for the folder from the pair
		songs, err := data.DB.SongsForFolder(a.folderID)
		if err != nil {
			return err
		}

		// Iterate and update songs with their new art ID
		for _, s := range songs {
			s.ArtID = a.artID
			if err := s.Update(); err != nil {
				return err
			}
		}
	}

	// Print metrics
	if verbose {
		log.Printf("fs: media scan complete [time: %s]", time.Since(startTime).String())
		log.Printf("fs: added: [art: %d] [artists: %d] [albums: %d] [songs: %d] [folders: %d]",
			artCount, artistCount, albumCount, songCount, folderCount)
		log.Printf("fs: updated: [songs: %d]", songUpdateCount)
	}

	// No errors
	return nil
}

// OrphanScan scans for missing "orphaned" media files in the local filesystem
func (fsFileSource) OrphanScan(baseFolder string, subFolder string, verbose bool, orphanCancelChan chan struct{}) error {
	// Halt scan if needed
	var mutex sync.RWMutex
	haltOrphanScan := false
	go func() {
		// Wait for signal
		<-orphanCancelChan

		// Halt!
		mutex.Lock()
		haltOrphanScan = true
		mutex.Unlock()
	}()

	// Track metrics about the scan
	artCount := 0
	folderCount := 0
	songCount := 0
	startTime := time.Now()

	// Check if a baseFolder is set, meaning remove ANYTHING not under this base
	if baseFolder != "" {
		if verbose {
			log.Println("fs: orphan scanning base folder:", baseFolder)
		}

		// Scan for all art NOT under the base folder
		art, err := data.DB.ArtNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all art which is not in this path
		for _, a := range art {
			// Remove art from database
			if err := a.Delete(); err != nil {
				log.Println(err)
				return err
			}

			artCount++
		}

		// Scan for all songs NOT under the base folder
		songs, err := data.DB.SongsNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all songs which are not in this path
		for _, s := range songs {
			// Remove song from database
			if err := s.Delete(); err != nil {
				log.Println(err)
				return err
			}

			songCount++
		}

		// Scan for all folders NOT under the base folder
		folders, err := data.DB.FoldersNotInPath(baseFolder)
		if err != nil {
			log.Println(err)
			return err
		}

		// Remove all folders which are not in this path
		for _, f := range folders {
			// Remove folder from database
			if err := f.Delete(); err != nil {
				log.Println(err)
				return err
			}

			folderCount++
		}
	}

	// If no subfolder set, use the base folder to check file existence
	if subFolder == "" {
		subFolder = baseFolder
	}

	if verbose {
		log.Println("fs: orphan scanning subfolder:", subFolder)
	} else {
		log.Println("fs: removing:", subFolder)
	}

	// Scan for all art in subfolder
	art, err := data.DB.ArtInPath(subFolder)
	if err != nil {
		log.Println(err)
		return err
	}

	// Iterate all art in this path
	for _, a := range art {
		// Check that the art still exists in this place
		if _, err := os.Stat(a.FileName); os.IsNotExist(err) {
			// Remove art from database
			if err := a.Delete(); err != nil {
				log.Println(err)
				return err
			}

			artCount++
		}
	}

	// Scan for all songs in subfolder
	songs, err := data.DB.SongsInPath(subFolder)
	if err != nil {
		log.Println(err)
		return err
	}

	// Iterate all songs in this path
	for _, s := range songs {
		// Check that the song still exists in this place
		if _, err := os.Stat(s.FileName); os.IsNotExist(err) {
			// Remove song from database
			if err := s.Delete(); err != nil {
				log.Println(err)
				return err
			}

			songCount++
		}
	}

	// Scan for all folders in subfolder
	folders, err := data.DB.FoldersInPath(subFolder)
	if err != nil {
		return err
	}

	// Iterate all folders in this path
	for _, f := range folders {
		// Check that the folder still has items within it
		files, err := ioutil.ReadDir(f.Path)
		if err != nil && !os.IsNotExist(err) {
			log.Println(err)
			return err
		}

		// Delete any folders with 0 items
		if len(files) == 0 {
			if err := f.Delete(); err != nil {
				log.Println(err)
				return err
			}

			folderCount++
		}
	}

	// Now that songs have been purged, check for albums
	albumCount, err := data.DB.PurgeOrphanAlbums()
	if err != nil {
		log.Println(err)
		return err
	}

	// Check for artists
	artistCount, err := data.DB.PurgeOrphanArtists()
	if err != nil {
		log.Println(err)
		return err
	}

	// Print metrics
	if verbose {
		log.Printf("fs: orphan scan complete [time: %s]", time.Since(startTime).String())
		log.Printf("fs: removed: [art: %d] [artists: %d] [albums: %d] [songs: %d] [folders: %d]",
			artCount, artistCount, albumCount, songCount, folderCount)
	}
	return nil
}
<|endoftext|>


<commit_before>package logger

/**
 * Barista Paxos Logger
 *
 * @author: Manasi Vartak
 * @date: 04/25/2014
 */

import "io/ioutil"
 import "os"
//import "json"

type Logger struct {
  filename string
}

func Make(filename string) *Logger {
  logger := &Logger{}
  logger.filename = filename
  return logger
}

func (lg *Logger) WriteToLog(text string) error {
  //return ioutil.WriteFile(lg.filename, b, 0644)
  f, err := os.OpenFile(lg.filename, os.O_APPEND, 0644)
  n, err := f.WriteString(text)
  f.Close()
}

func (lg *Logger) ReadFromLog() ([]byte, error) {
  return ioutil.ReadFile(lg.filename)
}

<commit_msg>minor fix<commit_after>package logger

/**
 * Barista Paxos Logger
 *
 * @author: Manasi Vartak
 * @date: 04/25/2014
 */

import "io/ioutil"
 import "os"
//import "json"

type Logger struct {
  filename string
}

func Make(filename string) *Logger {
  logger := &Logger{}
  logger.filename = filename
  return logger
}

func (lg *Logger) WriteToLog(text string) error {
  //return ioutil.WriteFile(lg.filename, b, 0644)
  f, err := os.OpenFile(lg.filename, os.O_APPEND, 0644)
  n, err := f.WriteString(text)
  f.Close()
  return err
}

func (lg *Logger) ReadFromLog() ([]byte, error) {
  return ioutil.ReadFile(lg.filename)
}

<|endoftext|>