<commit_before>package envoy

import (
	"context"
	"fmt"
	envoyv2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
	accesslogv2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2"
	"github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
	"github.com/envoyproxy/go-control-plane/pkg/cache"
	xds "github.com/envoyproxy/go-control-plane/pkg/server"
	"github.com/envoyproxy/go-control-plane/pkg/util"
	"github.com/gogo/protobuf/types"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	kubev1 "k8s.io/api/core/v1"
	v1alpha12 "knative.dev/serving/pkg/apis/networking/v1alpha1"
	"kourier/pkg/kubernetes"
	"net"
	"net/http"
	"strconv"
	"time"
)

const (
	grpcMaxConcurrentStreams = 1000000
)

type EnvoyXdsServer struct {
	gatewayPort    uint
	managementPort uint
	kubeClient     kubernetes.KubernetesClient // TODO: let's try to remove this coupling later
	ctx            context.Context
	server         xds.Server
	snapshotCache  cache.SnapshotCache
}

// Hasher returns node ID as an ID
type Hasher struct {
}

func (h Hasher) ID(node *core.Node) string {
	if node == nil {
		return "unknown"
	}
	return node.Id
}

func NewEnvoyXdsServer(gatewayPort uint, managementPort uint, kubeClient kubernetes.KubernetesClient) EnvoyXdsServer {
	ctx := context.Background()
	snapshotCache := cache.NewSnapshotCache(true, Hasher{}, nil)
	srv := xds.NewServer(snapshotCache, nil)

	return EnvoyXdsServer{
		gatewayPort:    gatewayPort,
		managementPort: managementPort,
		kubeClient:     kubeClient,
		ctx:            ctx,
		server:         srv,
		snapshotCache:  snapshotCache,
	}
}

// RunManagementServer starts an xDS server at the given Port.
func (envoyXdsServer *EnvoyXdsServer) RunManagementServer() {
	port := envoyXdsServer.managementPort
	server := envoyXdsServer.server

	var grpcOptions []grpc.ServerOption
	grpcOptions = append(grpcOptions, grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams))
	grpcServer := grpc.NewServer(grpcOptions...)
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		log.Error("Failed to listen")
	}

	// register services
	discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterEndpointDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterClusterDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterRouteDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterListenerDiscoveryServiceServer(grpcServer, server)

	log.Printf("Starting Management Server on Port %d\n", port)
	go func() {
		if err = grpcServer.Serve(lis); err != nil {
			log.Errorf("%s", err)
		}
	}()
	<-envoyXdsServer.ctx.Done()
	grpcServer.GracefulStop()
}

// RunManagementGateway starts an HTTP gateway to an xDS server.
func (envoyXdsServer *EnvoyXdsServer) RunGateway() {
	port := envoyXdsServer.gatewayPort
	server := envoyXdsServer.server
	ctx := envoyXdsServer.ctx

	log.Printf("Starting HTTP/1.1 gateway on Port %d\n", port)
	httpServer := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: &xds.HTTPGateway{Server: server}}
	go func() {
		if err := httpServer.ListenAndServe(); err != nil {
			panic(err)
		}
	}()

	<-ctx.Done()
	if err := httpServer.Shutdown(ctx); err != nil {
		panic(err)
	}
}

func (envoyXdsServer *EnvoyXdsServer) SetSnapshotForClusterIngresses(nodeId string, Ingresses []v1alpha12.IngressAccessor) {
	var virtualHosts []*route.VirtualHost
	var routeCache []cache.Resource
	var clusterCache []cache.Resource

	for i, ingress := range Ingresses {

		routeName := getRouteName(ingress)
		routeNamespace := getRouteNamespace(ingress)

		log.WithFields(log.Fields{"name": routeName, "namespace": routeNamespace}).Info("Knative Ingress found")

		for _, rule := range ingress.GetSpec().Rules {

			var ruleRoute []*route.Route
			domains := rule.Hosts

			for _, httpPath := range rule.HTTP.Paths {

				path := "/"
				if httpPath.Path != "" {
					path = httpPath.Path
				}

				headersRev := httpPath.AppendHeaders

				var wrs []*route.WeightedCluster_ClusterWeight

				for _, split := range httpPath.Splits {

					headersSplit := split.AppendHeaders

					endpointList, err := envoyXdsServer.kubeClient.EndpointsForRevision(split.ServiceNamespace, split.ServiceName)

					if err != nil {
						log.Errorf("%s", err)
						break
					}

					service, err := envoyXdsServer.kubeClient.ServiceForRevision(split.ServiceNamespace, split.ServiceName)

					if err != nil {
						log.Errorf("%s", err)
						break
					}

					var targetPort int32
					http2 := false
					for _, port := range service.Spec.Ports {
						if port.Port == split.ServicePort.IntVal || port.Name == split.ServicePort.StrVal {
							targetPort = port.TargetPort.IntVal
							http2 = port.Name == "http2" || port.Name == "h2c"
						}
					}

					lbEndpoints := lbEndpointsForKubeEndpoints(endpointList, targetPort)

					connectTimeout := 5 * time.Second
					cluster := clusterForRevision(split.ServiceName, connectTimeout, lbEndpoints, http2, path)
					clusterCache = append(clusterCache, &cluster)

					weightedCluster := weightedCluster(split.ServiceName, uint32(split.Percent), path, headersSplit)

					wrs = append(wrs, &weightedCluster)

				}

				attempts := 0
				var perTryTimeout time.Duration
				if httpPath.Retries != nil {
					attempts = httpPath.Retries.Attempts

					if httpPath.Retries.PerTryTimeout != nil {
						perTryTimeout = httpPath.Retries.PerTryTimeout.Duration
					}
				}

				r := createRouteForRevision(routeName, i, path, wrs, attempts, perTryTimeout, headersRev)

				ruleRoute = append(ruleRoute, &r)
				routeCache = append(routeCache, &r)

			}

			virtualHost := route.VirtualHost{
				Name:    routeName,
				Domains: domains,
				Routes:  ruleRoute,
			}

			virtualHosts = append(virtualHosts, &virtualHost)
		}

	}

	manager := httpConnectionManager(virtualHosts)
	l := envoyListener(&manager)
	listenerCache := []cache.Resource{&l}

	snapshotVersion, errUUID := uuid.NewUUID()
	if errUUID != nil {
		log.Error(errUUID)
		return
	}

	snapshot := cache.NewSnapshot(snapshotVersion.String(), nil, clusterCache, routeCache, listenerCache)

	err := envoyXdsServer.snapshotCache.SetSnapshot(nodeId, snapshot)
	if err != nil {
		log.Error(err)
	}
}

func createRouteForRevision(routeName string, i int, path string, wrs []*route.WeightedCluster_ClusterWeight, attempts int, perTryTimeout time.Duration, headersToAdd map[string]string) route.Route {

	// TODO: extract header generation
	var headers []*core.HeaderValueOption

	for k, v := range headersToAdd {

		header := core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   k,
				Value: v,
			},
			Append: &types.BoolValue{
				Value: true,
			},
		}

		headers = append(headers, &header)

	}

	r := route.Route{
		Name: routeName + "_" + strconv.Itoa(i),
		Match: &route.RouteMatch{
			PathSpecifier: &route.RouteMatch_Prefix{
				Prefix: path,
			},
		},
		Action: &route.Route_Route{Route: &route.RouteAction{
			ClusterSpecifier: &route.RouteAction_WeightedClusters{
				WeightedClusters: &route.WeightedCluster{
					Clusters: wrs,
				},
			},
			RetryPolicy: createRetryPolicy(attempts, perTryTimeout),
		}},
		RequestHeadersToAdd:     headers,
	}

	return r
}

func createRetryPolicy(attempts int, perTryTimeout time.Duration) *route.RetryPolicy {
	if attempts > 0 {
		return &route.RetryPolicy{
			RetryOn: "5xx",
			NumRetries: &types.UInt32Value{
				Value: uint32(attempts),
			},
			PerTryTimeout: &perTryTimeout,
		}
	} else {
		return nil
	}
}

func getRouteNamespace(ingress v1alpha12.IngressAccessor) string {
	return ingress.GetLabels()["serving.knative.dev/routeNamespace"]
}

func getRouteName(ingress v1alpha12.IngressAccessor) string {
	return ingress.GetLabels()["serving.knative.dev/route"]
}

func lbEndpointsForKubeEndpoints(kubeEndpoints *kubev1.EndpointsList, targetPort int32) []*endpoint.LbEndpoint {
	var result []*endpoint.LbEndpoint

	for _, kubeEndpoint := range kubeEndpoints.Items {
		for _, subset := range kubeEndpoint.Subsets {

			for _, address := range subset.Addresses {

				serviceEndpoint := &core.Address{
					Address: &core.Address_SocketAddress{
						SocketAddress: &core.SocketAddress{
							Protocol: core.TCP,
							Address:  address.IP,
							PortSpecifier: &core.SocketAddress_PortValue{
								PortValue: uint32(targetPort),
							},
							Ipv4Compat: true,
						},
					},
				}

				lbEndpoint := endpoint.LbEndpoint{
					HostIdentifier: &endpoint.LbEndpoint_Endpoint{
						Endpoint: &endpoint.Endpoint{
							Address: serviceEndpoint,
						},
					},
				}
				result = append(result, &lbEndpoint)
			}
		}
	}

	return result
}

func clusterForRevision(revisionName string, connectTimeout time.Duration, lbEndpoints []*endpoint.LbEndpoint, http2 bool, path string) envoyv2.Cluster {

	cluster := envoyv2.Cluster{
		Name: revisionName + path,
		ClusterDiscoveryType: &envoyv2.Cluster_Type{
			Type: envoyv2.Cluster_STRICT_DNS,
		},
		ConnectTimeout: &connectTimeout,
		LoadAssignment: &envoyv2.ClusterLoadAssignment{
			ClusterName: revisionName + path,
			Endpoints: []*endpoint.LocalityLbEndpoints{
				{
					LbEndpoints: lbEndpoints,
				},
			},
		},
	}

	if http2 {
		cluster.Http2ProtocolOptions = &core.Http2ProtocolOptions{}
	}

	return cluster
}

func weightedCluster(revisionName string, trafficPerc uint32, path string, headersToAdd map[string]string) route.WeightedCluster_ClusterWeight {

	var headers []*core.HeaderValueOption

	for k, v := range headersToAdd {

		header := core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   k,
				Value: v,
			},
			Append: &types.BoolValue{
				Value: true,
			},
		}

		headers = append(headers, &header)

	}

	return route.WeightedCluster_ClusterWeight{
		Name: revisionName + path,
		Weight: &types.UInt32Value{
			Value: trafficPerc,
		},
		RequestHeadersToAdd: headers,
	}
}

func httpConnectionManager(virtualHosts []*route.VirtualHost) v2.HttpConnectionManager {
	return v2.HttpConnectionManager{
		CodecType:  v2.AUTO,
		StatPrefix: "ingress_http",
		RouteSpecifier: &v2.HttpConnectionManager_RouteConfig{
			RouteConfig: &envoyv2.RouteConfiguration{
				Name:         "local_route",
				VirtualHosts: virtualHosts,
			},
		},
		HttpFilters: []*v2.HttpFilter{
			{
				Name: util.Router,
			},
		},

		AccessLog: accessLogs(),
	}
}

// Outputs to /dev/stdout using the default format
func accessLogs() []*accesslogv2.AccessLog {
	accessLogConfigFields := make(map[string]*types.Value)
	accessLogConfigFields["path"] = &types.Value{
		Kind: &types.Value_StringValue{
			StringValue: "/dev/stdout",
		},
	}

	return []*accesslogv2.AccessLog{
		{
			Name: "envoy.file_access_log",
			ConfigType: &accesslogv2.AccessLog_Config{
				Config: &types.Struct{Fields: accessLogConfigFields},
			},
		},
	}
}

func envoyListener(httpConnectionManager *v2.HttpConnectionManager) envoyv2.Listener {
	pbst, err := util.MessageToStruct(httpConnectionManager)
	if err != nil {
		panic(err)
	}

	return envoyv2.Listener{
		Name: "listener_0",
		Address: &core.Address{
			Address: &core.Address_SocketAddress{
				SocketAddress: &core.SocketAddress{
					Protocol: core.TCP,
					Address:  "0.0.0.0",
					PortSpecifier: &core.SocketAddress_PortValue{
						PortValue: uint32(8080),
					},
				},
			},
		},
		FilterChains: []*listener.FilterChain{{
			Filters: []*listener.Filter{{
				Name:       util.HTTPConnectionManager,
				ConfigType: &listener.Filter_Config{Config: pbst},
			}},
		}},
	}
}
<commit_msg>envoy: add route timeouts<commit_after>package envoy

import (
	"context"
	"fmt"
	envoyv2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
	"github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
	accesslogv2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2"
	"github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
	"github.com/envoyproxy/go-control-plane/pkg/cache"
	xds "github.com/envoyproxy/go-control-plane/pkg/server"
	"github.com/envoyproxy/go-control-plane/pkg/util"
	"github.com/gogo/protobuf/types"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	kubev1 "k8s.io/api/core/v1"
	v1alpha12 "knative.dev/serving/pkg/apis/networking/v1alpha1"
	"kourier/pkg/kubernetes"
	"net"
	"net/http"
	"strconv"
	"time"
)

const (
	grpcMaxConcurrentStreams = 1000000
)

type EnvoyXdsServer struct {
	gatewayPort    uint
	managementPort uint
	kubeClient     kubernetes.KubernetesClient // TODO: let's try to remove this coupling later
	ctx            context.Context
	server         xds.Server
	snapshotCache  cache.SnapshotCache
}

// Hasher returns node ID as an ID
type Hasher struct {
}

func (h Hasher) ID(node *core.Node) string {
	if node == nil {
		return "unknown"
	}
	return node.Id
}

func NewEnvoyXdsServer(gatewayPort uint, managementPort uint, kubeClient kubernetes.KubernetesClient) EnvoyXdsServer {
	ctx := context.Background()
	snapshotCache := cache.NewSnapshotCache(true, Hasher{}, nil)
	srv := xds.NewServer(snapshotCache, nil)

	return EnvoyXdsServer{
		gatewayPort:    gatewayPort,
		managementPort: managementPort,
		kubeClient:     kubeClient,
		ctx:            ctx,
		server:         srv,
		snapshotCache:  snapshotCache,
	}
}

// RunManagementServer starts an xDS server at the given Port.
func (envoyXdsServer *EnvoyXdsServer) RunManagementServer() {
	port := envoyXdsServer.managementPort
	server := envoyXdsServer.server

	var grpcOptions []grpc.ServerOption
	grpcOptions = append(grpcOptions, grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams))
	grpcServer := grpc.NewServer(grpcOptions...)
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		log.Error("Failed to listen")
	}

	// register services
	discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterEndpointDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterClusterDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterRouteDiscoveryServiceServer(grpcServer, server)
	envoyv2.RegisterListenerDiscoveryServiceServer(grpcServer, server)

	log.Printf("Starting Management Server on Port %d\n", port)
	go func() {
		if err = grpcServer.Serve(lis); err != nil {
			log.Errorf("%s", err)
		}
	}()
	<-envoyXdsServer.ctx.Done()
	grpcServer.GracefulStop()
}

// RunManagementGateway starts an HTTP gateway to an xDS server.
func (envoyXdsServer *EnvoyXdsServer) RunGateway() {
	port := envoyXdsServer.gatewayPort
	server := envoyXdsServer.server
	ctx := envoyXdsServer.ctx

	log.Printf("Starting HTTP/1.1 gateway on Port %d\n", port)
	httpServer := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: &xds.HTTPGateway{Server: server}}
	go func() {
		if err := httpServer.ListenAndServe(); err != nil {
			panic(err)
		}
	}()

	<-ctx.Done()
	if err := httpServer.Shutdown(ctx); err != nil {
		panic(err)
	}
}

func (envoyXdsServer *EnvoyXdsServer) SetSnapshotForClusterIngresses(nodeId string, Ingresses []v1alpha12.IngressAccessor) {
	var virtualHosts []*route.VirtualHost
	var routeCache []cache.Resource
	var clusterCache []cache.Resource

	for i, ingress := range Ingresses {

		routeName := getRouteName(ingress)
		routeNamespace := getRouteNamespace(ingress)

		log.WithFields(log.Fields{"name": routeName, "namespace": routeNamespace}).Info("Knative Ingress found")

		for _, rule := range ingress.GetSpec().Rules {

			var ruleRoute []*route.Route
			domains := rule.Hosts

			for _, httpPath := range rule.HTTP.Paths {

				path := "/"
				if httpPath.Path != "" {
					path = httpPath.Path
				}

				headersRev := httpPath.AppendHeaders

				var wrs []*route.WeightedCluster_ClusterWeight

				for _, split := range httpPath.Splits {

					headersSplit := split.AppendHeaders

					endpointList, err := envoyXdsServer.kubeClient.EndpointsForRevision(split.ServiceNamespace, split.ServiceName)

					if err != nil {
						log.Errorf("%s", err)
						break
					}

					service, err := envoyXdsServer.kubeClient.ServiceForRevision(split.ServiceNamespace, split.ServiceName)

					if err != nil {
						log.Errorf("%s", err)
						break
					}

					var targetPort int32
					http2 := false
					for _, port := range service.Spec.Ports {
						if port.Port == split.ServicePort.IntVal || port.Name == split.ServicePort.StrVal {
							targetPort = port.TargetPort.IntVal
							http2 = port.Name == "http2" || port.Name == "h2c"
						}
					}

					lbEndpoints := lbEndpointsForKubeEndpoints(endpointList, targetPort)

					connectTimeout := 5 * time.Second
					cluster := clusterForRevision(split.ServiceName, connectTimeout, lbEndpoints, http2, path)
					clusterCache = append(clusterCache, &cluster)

					weightedCluster := weightedCluster(split.ServiceName, uint32(split.Percent), path, headersSplit)

					wrs = append(wrs, &weightedCluster)

				}

				attempts := 0
				var perTryTimeout time.Duration
				if httpPath.Retries != nil {
					attempts = httpPath.Retries.Attempts

					if httpPath.Retries.PerTryTimeout != nil {
						perTryTimeout = httpPath.Retries.PerTryTimeout.Duration
					}
				}

				var routeTimeout time.Duration
				if httpPath.Timeout != nil {
					routeTimeout = httpPath.Timeout.Duration
				}

				r := createRouteForRevision(routeName, i, path, wrs, attempts, perTryTimeout, routeTimeout, headersRev)

				ruleRoute = append(ruleRoute, &r)
				routeCache = append(routeCache, &r)

			}

			virtualHost := route.VirtualHost{
				Name:    routeName,
				Domains: domains,
				Routes:  ruleRoute,
			}

			virtualHosts = append(virtualHosts, &virtualHost)
		}

	}

	manager := httpConnectionManager(virtualHosts)
	l := envoyListener(&manager)
	listenerCache := []cache.Resource{&l}

	snapshotVersion, errUUID := uuid.NewUUID()
	if errUUID != nil {
		log.Error(errUUID)
		return
	}

	snapshot := cache.NewSnapshot(snapshotVersion.String(), nil, clusterCache, routeCache, listenerCache)

	err := envoyXdsServer.snapshotCache.SetSnapshot(nodeId, snapshot)
	if err != nil {
		log.Error(err)
	}
}

func createRouteForRevision(routeName string, i int, path string, wrs []*route.WeightedCluster_ClusterWeight, attempts int, perTryTimeout time.Duration, timeout time.Duration, headersToAdd map[string]string) route.Route {

	// TODO: extract header generation
	var headers []*core.HeaderValueOption

	for k, v := range headersToAdd {

		header := core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   k,
				Value: v,
			},
			Append: &types.BoolValue{
				Value: true,
			},
		}

		headers = append(headers, &header)

	}

	r := route.Route{
		Name: routeName + "_" + strconv.Itoa(i),
		Match: &route.RouteMatch{
			PathSpecifier: &route.RouteMatch_Prefix{
				Prefix: path,
			},
		},
		Action: &route.Route_Route{Route: &route.RouteAction{
			ClusterSpecifier: &route.RouteAction_WeightedClusters{
				WeightedClusters: &route.WeightedCluster{
					Clusters: wrs,
				},
			},
			Timeout:     &timeout,
			RetryPolicy: createRetryPolicy(attempts, perTryTimeout),
		}},
		RequestHeadersToAdd: headers,
	}

	return r
}

func createRetryPolicy(attempts int, perTryTimeout time.Duration) *route.RetryPolicy {
	if attempts > 0 {
		return &route.RetryPolicy{
			RetryOn: "5xx",
			NumRetries: &types.UInt32Value{
				Value: uint32(attempts),
			},
			PerTryTimeout: &perTryTimeout,
		}
	} else {
		return nil
	}
}

func getRouteNamespace(ingress v1alpha12.IngressAccessor) string {
	return ingress.GetLabels()["serving.knative.dev/routeNamespace"]
}

func getRouteName(ingress v1alpha12.IngressAccessor) string {
	return ingress.GetLabels()["serving.knative.dev/route"]
}

func lbEndpointsForKubeEndpoints(kubeEndpoints *kubev1.EndpointsList, targetPort int32) []*endpoint.LbEndpoint {
	var result []*endpoint.LbEndpoint

	for _, kubeEndpoint := range kubeEndpoints.Items {
		for _, subset := range kubeEndpoint.Subsets {

			for _, address := range subset.Addresses {

				serviceEndpoint := &core.Address{
					Address: &core.Address_SocketAddress{
						SocketAddress: &core.SocketAddress{
							Protocol: core.TCP,
							Address:  address.IP,
							PortSpecifier: &core.SocketAddress_PortValue{
								PortValue: uint32(targetPort),
							},
							Ipv4Compat: true,
						},
					},
				}

				lbEndpoint := endpoint.LbEndpoint{
					HostIdentifier: &endpoint.LbEndpoint_Endpoint{
						Endpoint: &endpoint.Endpoint{
							Address: serviceEndpoint,
						},
					},
				}
				result = append(result, &lbEndpoint)
			}
		}
	}

	return result
}

func clusterForRevision(revisionName string, connectTimeout time.Duration, lbEndpoints []*endpoint.LbEndpoint, http2 bool, path string) envoyv2.Cluster {

	cluster := envoyv2.Cluster{
		Name: revisionName + path,
		ClusterDiscoveryType: &envoyv2.Cluster_Type{
			Type: envoyv2.Cluster_STRICT_DNS,
		},
		ConnectTimeout: &connectTimeout,
		LoadAssignment: &envoyv2.ClusterLoadAssignment{
			ClusterName: revisionName + path,
			Endpoints: []*endpoint.LocalityLbEndpoints{
				{
					LbEndpoints: lbEndpoints,
				},
			},
		},
	}

	if http2 {
		cluster.Http2ProtocolOptions = &core.Http2ProtocolOptions{}
	}

	return cluster
}

func weightedCluster(revisionName string, trafficPerc uint32, path string, headersToAdd map[string]string) route.WeightedCluster_ClusterWeight {

	var headers []*core.HeaderValueOption

	for k, v := range headersToAdd {

		header := core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   k,
				Value: v,
			},
			Append: &types.BoolValue{
				Value: true,
			},
		}

		headers = append(headers, &header)

	}

	return route.WeightedCluster_ClusterWeight{
		Name: revisionName + path,
		Weight: &types.UInt32Value{
			Value: trafficPerc,
		},
		RequestHeadersToAdd: headers,
	}
}

func httpConnectionManager(virtualHosts []*route.VirtualHost) v2.HttpConnectionManager {
	return v2.HttpConnectionManager{
		CodecType:  v2.AUTO,
		StatPrefix: "ingress_http",
		RouteSpecifier: &v2.HttpConnectionManager_RouteConfig{
			RouteConfig: &envoyv2.RouteConfiguration{
				Name:         "local_route",
				VirtualHosts: virtualHosts,
			},
		},
		HttpFilters: []*v2.HttpFilter{
			{
				Name: util.Router,
			},
		},

		AccessLog: accessLogs(),
	}
}

// Outputs to /dev/stdout using the default format
func accessLogs() []*accesslogv2.AccessLog {
	accessLogConfigFields := make(map[string]*types.Value)
	accessLogConfigFields["path"] = &types.Value{
		Kind: &types.Value_StringValue{
			StringValue: "/dev/stdout",
		},
	}

	return []*accesslogv2.AccessLog{
		{
			Name: "envoy.file_access_log",
			ConfigType: &accesslogv2.AccessLog_Config{
				Config: &types.Struct{Fields: accessLogConfigFields},
			},
		},
	}
}

func envoyListener(httpConnectionManager *v2.HttpConnectionManager) envoyv2.Listener {
	pbst, err := util.MessageToStruct(httpConnectionManager)
	if err != nil {
		panic(err)
	}

	return envoyv2.Listener{
		Name: "listener_0",
		Address: &core.Address{
			Address: &core.Address_SocketAddress{
				SocketAddress: &core.SocketAddress{
					Protocol: core.TCP,
					Address:  "0.0.0.0",
					PortSpecifier: &core.SocketAddress_PortValue{
						PortValue: uint32(8080),
					},
				},
			},
		},
		FilterChains: []*listener.FilterChain{{
			Filters: []*listener.Filter{{
				Name:       util.HTTPConnectionManager,
				ConfigType: &listener.Filter_Config{Config: pbst},
			}},
		}},
	}
}
<|endoftext|>


<commit_before>// Copyright 2013 The Prometheus 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 event

import (
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/statsd_exporter/pkg/clock"
	"github.com/prometheus/statsd_exporter/pkg/mapper"
)

type Event interface {
	MetricName() string
	Value() float64
	Labels() map[string]string
	MetricType() mapper.MetricType
}

type CounterEvent struct {
	CMetricName string
	CValue      float64
	CLabels     map[string]string
}

func (c *CounterEvent) MetricName() string            { return c.CMetricName }
func (c *CounterEvent) Value() float64                { return c.CValue }
func (c *CounterEvent) Labels() map[string]string     { return c.CLabels }
func (c *CounterEvent) MetricType() mapper.MetricType { return mapper.MetricTypeCounter }

type GaugeEvent struct {
	GMetricName string
	GValue      float64
	GRelative   bool
	GLabels     map[string]string
}

func (g *GaugeEvent) MetricName() string            { return g.GMetricName }
func (g *GaugeEvent) Value() float64                { return g.GValue }
func (c *GaugeEvent) Labels() map[string]string     { return c.GLabels }
func (c *GaugeEvent) MetricType() mapper.MetricType { return mapper.MetricTypeGauge }

type ObserverEvent struct {
	OMetricName string
	OValue      float64
	OLabels     map[string]string
}

func (t *ObserverEvent) MetricName() string            { return t.OMetricName }
func (t *ObserverEvent) Value() float64                { return t.OValue }
func (c *ObserverEvent) Labels() map[string]string     { return c.OLabels }
func (c *ObserverEvent) MetricType() mapper.MetricType { return mapper.MetricTypeObserver }

type Events []Event

type EventQueue struct {
	C              chan Events
	q              Events
	m              sync.Mutex
	flushTicker    *time.Ticker
	flushThreshold int
	flushInterval  time.Duration
	eventsFlushed  prometheus.Counter
}

type EventHandler interface {
	Queue(event Events)
}

func NewEventQueue(c chan Events, flushThreshold int, flushInterval time.Duration, eventsFlushed prometheus.Counter) *EventQueue {
	ticker := clock.NewTicker(flushInterval)
	eq := &EventQueue{
		C:              c,
		flushThreshold: flushThreshold,
		flushInterval:  flushInterval,
		flushTicker:    ticker,
		q:              make([]Event, 0, flushThreshold),
		eventsFlushed:  eventsFlushed,
	}
	go func() {
		for {
			<-ticker.C
			eq.Flush()
		}
	}()
	return eq
}

func (eq *EventQueue) Queue(events Events) {
	eq.m.Lock()
	defer eq.m.Unlock()

	for _, e := range events {
		eq.q = append(eq.q, e)
		if len(eq.q) >= eq.flushThreshold {
			eq.FlushUnlocked()
		}
	}
}

func (eq *EventQueue) Flush() {
	eq.m.Lock()
	defer eq.m.Unlock()
	eq.FlushUnlocked()
}

func (eq *EventQueue) FlushUnlocked() {
	eq.C <- eq.q
	eq.q = make([]Event, 0, cap(eq.q))
	eq.eventsFlushed.Inc()
}

func (eq *EventQueue) Len() int {
	eq.m.Lock()
	defer eq.m.Unlock()

	return len(eq.q)
}

type UnbufferedEventHandler struct {
	C chan Events
}

func (ueh *UnbufferedEventHandler) Queue(events Events) {
	ueh.C <- events
}
<commit_msg>rename events for consistency<commit_after>// Copyright 2013 The Prometheus 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 event

import (
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/statsd_exporter/pkg/clock"
	"github.com/prometheus/statsd_exporter/pkg/mapper"
)

type Event interface {
	MetricName() string
	Value() float64
	Labels() map[string]string
	MetricType() mapper.MetricType
}

type CounterEvent struct {
	CMetricName string
	CValue      float64
	CLabels     map[string]string
}

func (c *CounterEvent) MetricName() string            { return c.CMetricName }
func (c *CounterEvent) Value() float64                { return c.CValue }
func (c *CounterEvent) Labels() map[string]string     { return c.CLabels }
func (c *CounterEvent) MetricType() mapper.MetricType { return mapper.MetricTypeCounter }

type GaugeEvent struct {
	GMetricName string
	GValue      float64
	GRelative   bool
	GLabels     map[string]string
}

func (g *GaugeEvent) MetricName() string            { return g.GMetricName }
func (g *GaugeEvent) Value() float64                { return g.GValue }
func (g *GaugeEvent) Labels() map[string]string     { return g.GLabels }
func (g *GaugeEvent) MetricType() mapper.MetricType { return mapper.MetricTypeGauge }

type ObserverEvent struct {
	OMetricName string
	OValue      float64
	OLabels     map[string]string
}

func (o *ObserverEvent) MetricName() string            { return o.OMetricName }
func (o *ObserverEvent) Value() float64                { return o.OValue }
func (o *ObserverEvent) Labels() map[string]string     { return o.OLabels }
func (o *ObserverEvent) MetricType() mapper.MetricType { return mapper.MetricTypeObserver }

type Events []Event

type EventQueue struct {
	C              chan Events
	q              Events
	m              sync.Mutex
	flushTicker    *time.Ticker
	flushThreshold int
	flushInterval  time.Duration
	eventsFlushed  prometheus.Counter
}

type EventHandler interface {
	Queue(event Events)
}

func NewEventQueue(c chan Events, flushThreshold int, flushInterval time.Duration, eventsFlushed prometheus.Counter) *EventQueue {
	ticker := clock.NewTicker(flushInterval)
	eq := &EventQueue{
		C:              c,
		flushThreshold: flushThreshold,
		flushInterval:  flushInterval,
		flushTicker:    ticker,
		q:              make([]Event, 0, flushThreshold),
		eventsFlushed:  eventsFlushed,
	}
	go func() {
		for {
			<-ticker.C
			eq.Flush()
		}
	}()
	return eq
}

func (eq *EventQueue) Queue(events Events) {
	eq.m.Lock()
	defer eq.m.Unlock()

	for _, e := range events {
		eq.q = append(eq.q, e)
		if len(eq.q) >= eq.flushThreshold {
			eq.FlushUnlocked()
		}
	}
}

func (eq *EventQueue) Flush() {
	eq.m.Lock()
	defer eq.m.Unlock()
	eq.FlushUnlocked()
}

func (eq *EventQueue) FlushUnlocked() {
	eq.C <- eq.q
	eq.q = make([]Event, 0, cap(eq.q))
	eq.eventsFlushed.Inc()
}

func (eq *EventQueue) Len() int {
	eq.m.Lock()
	defer eq.m.Unlock()

	return len(eq.q)
}

type UnbufferedEventHandler struct {
	C chan Events
}

func (ueh *UnbufferedEventHandler) Queue(events Events) {
	ueh.C <- events
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors All rights reserved.

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

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

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

package helm

import (
	"golang.org/x/net/context"
	cpb "k8s.io/helm/pkg/proto/hapi/chart"
	rls "k8s.io/helm/pkg/proto/hapi/services"
)

// Option allows specifying various settings configurable by
// the helm client user for overriding the defaults used when
// issuing rpc's to the Tiller release server.
type Option func(*options)

// options specify optional settings used by the helm client.
type options struct {
	// value of helm host override
	home string
	// value of helm home override
	host string
	// name of chart
	chart string
	// if set dry-run helm client calls
	dryRun bool
	// if set, re-use an existing name
	reuseName bool
	// if set, skip running hooks
	disableHooks bool
	// release list options are applied directly to the list releases request
	listReq rls.ListReleasesRequest
	// release install options are applied directly to the install release request
	instReq rls.InstallReleaseRequest
	// release update options are applied directly to the update release request
	updateReq rls.UpdateReleaseRequest
}

// Home specifies the location of helm home, (default = "$HOME/.helm").
func Home(home string) Option {
	return func(opts *options) {
		opts.home = home
	}
}

// Host specifies the host address of the Tiller release server, (default = ":44134").
func Host(host string) Option {
	return func(opts *options) {
		opts.host = host
	}
}

// ReleaseListOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm list` command.
type ReleaseListOption func(*options)

// ReleaseListOffset specifies the offset into a list of releases.
func ReleaseListOffset(offset string) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Offset = offset
	}
}

// ReleaseListFilter specifies a filter to apply a list of releases.
func ReleaseListFilter(filter string) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Filter = filter
	}
}

// ReleaseListLimit set an upper bound on the number of releases returned.
func ReleaseListLimit(limit int) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Limit = int64(limit)
	}
}

// ReleaseListOrder specifies how to order a list of releases.
func ReleaseListOrder(order int32) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.SortOrder = rls.ListSort_SortOrder(order)
	}
}

// ReleaseListSort specifies how to sort a release list.
func ReleaseListSort(sort int32) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.SortBy = rls.ListSort_SortBy(sort)
	}
}

// InstallOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm install` command.
type InstallOption func(*options)

// ValueOverrides specifies a list of values to include when installing.
func ValueOverrides(raw []byte) InstallOption {
	return func(opts *options) {
		opts.instReq.Values = &cpb.Config{Raw: string(raw)}
	}
}

// UpdateValueOverrides specifies a list of values to include when upgrading
func UpdateValueOverrides(raw []byte) UpdateOption {
	return func(opts *options) {
		opts.updateReq.Values = &cpb.Config{Raw: string(raw)}
	}
}

// ReleaseName specifies the name of the release when installing.
func ReleaseName(name string) InstallOption {
	return func(opts *options) {
		opts.instReq.Name = name
	}
}

// DeleteDisableHooks will disable hooks for a deletion operation.
func DeleteDisableHooks(disable bool) DeleteOption {
	return func(opts *options) {
		opts.disableHooks = disable
	}
}

// DeleteDryRun will (if true) execute a deletion as a dry run.
func DeleteDryRun(dry bool) DeleteOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// UpgradeDryRun will (if true) execute an upgrade as a dry run.
func UpgradeDryRun(dry bool) UpdateOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// InstallDisableHooks disables hooks during installation.
func InstallDisableHooks(disable bool) InstallOption {
	return func(opts *options) {
		opts.disableHooks = disable
	}
}

// InstallDryRun will (if true) execute an installation as a dry run.
func InstallDryRun(dry bool) InstallOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// InstallReuseName will (if true) instruct Tiller to re-use an existing name.
func InstallReuseName(reuse bool) InstallOption {
	return func(opts *options) {
		opts.reuseName = reuse
	}
}

// ContentOption -- TODO
type ContentOption func(*options)

// StatusOption -- TODO
type StatusOption func(*options)

// DeleteOption -- TODO
type DeleteOption func(*options)

// UpdateOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm upgrade` command.
type UpdateOption func(*options)

// RPC helpers defined on `options` type. Note: These actually execute the
// the corresponding tiller RPC. There is no particular reason why these
// are APIs are hung off `options`, they are internal to pkg/helm to remain
// malleable.

// Executes tiller.ListReleases RPC.
func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) {
	// apply release list options
	for _, opt := range opts {
		opt(o)
	}
	s, err := rlc.ListReleases(context.TODO(), &o.listReq)
	if err != nil {
		return nil, err
	}

	return s.Recv()
}

// Executes tiller.InstallRelease RPC.
func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
	// apply the install options
	for _, opt := range opts {
		opt(o)
	}
	o.instReq.Chart = chr
	o.instReq.Namespace = ns
	o.instReq.DryRun = o.dryRun
	o.instReq.DisableHooks = o.disableHooks
	o.instReq.ReuseName = o.reuseName

	return rlc.InstallRelease(context.TODO(), &o.instReq)
}

// Executes tiller.UninstallRelease RPC.
func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) {
	for _, opt := range opts {
		opt(o)
	}
	if o.dryRun {
		// In the dry run case, just see if the release exists
		r, err := o.rpcGetReleaseContent(rlsName, rlc)
		if err != nil {
			return &rls.UninstallReleaseResponse{}, err
		}
		return &rls.UninstallReleaseResponse{Release: r.Release}, nil
	}
	return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks})
}

// Executes tiller.UpdateRelease RPC.
func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
	//TODO: handle dryRun

	return rlc.UpdateRelease(context.TODO(), &rls.UpdateReleaseRequest{Name: rlsName, Chart: chr})
}

// Executes tiller.GetReleaseStatus RPC.
func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClient, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) {
	req := &rls.GetReleaseStatusRequest{Name: rlsName}
	return rlc.GetReleaseStatus(context.TODO(), req)
}

// Executes tiller.GetReleaseContent.
func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceClient, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) {
	req := &rls.GetReleaseContentRequest{Name: rlsName}
	return rlc.GetReleaseContent(context.TODO(), req)
}
<commit_msg>chore(helm): add options to update request<commit_after>/*
Copyright 2016 The Kubernetes Authors All rights reserved.

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

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

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

package helm

import (
	"golang.org/x/net/context"
	cpb "k8s.io/helm/pkg/proto/hapi/chart"
	rls "k8s.io/helm/pkg/proto/hapi/services"
)

// Option allows specifying various settings configurable by
// the helm client user for overriding the defaults used when
// issuing rpc's to the Tiller release server.
type Option func(*options)

// options specify optional settings used by the helm client.
type options struct {
	// value of helm host override
	home string
	// value of helm home override
	host string
	// name of chart
	chart string
	// if set dry-run helm client calls
	dryRun bool
	// if set, re-use an existing name
	reuseName bool
	// if set, skip running hooks
	disableHooks bool
	// release list options are applied directly to the list releases request
	listReq rls.ListReleasesRequest
	// release install options are applied directly to the install release request
	instReq rls.InstallReleaseRequest
	// release update options are applied directly to the update release request
	updateReq rls.UpdateReleaseRequest
}

// Home specifies the location of helm home, (default = "$HOME/.helm").
func Home(home string) Option {
	return func(opts *options) {
		opts.home = home
	}
}

// Host specifies the host address of the Tiller release server, (default = ":44134").
func Host(host string) Option {
	return func(opts *options) {
		opts.host = host
	}
}

// ReleaseListOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm list` command.
type ReleaseListOption func(*options)

// ReleaseListOffset specifies the offset into a list of releases.
func ReleaseListOffset(offset string) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Offset = offset
	}
}

// ReleaseListFilter specifies a filter to apply a list of releases.
func ReleaseListFilter(filter string) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Filter = filter
	}
}

// ReleaseListLimit set an upper bound on the number of releases returned.
func ReleaseListLimit(limit int) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.Limit = int64(limit)
	}
}

// ReleaseListOrder specifies how to order a list of releases.
func ReleaseListOrder(order int32) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.SortOrder = rls.ListSort_SortOrder(order)
	}
}

// ReleaseListSort specifies how to sort a release list.
func ReleaseListSort(sort int32) ReleaseListOption {
	return func(opts *options) {
		opts.listReq.SortBy = rls.ListSort_SortBy(sort)
	}
}

// InstallOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm install` command.
type InstallOption func(*options)

// ValueOverrides specifies a list of values to include when installing.
func ValueOverrides(raw []byte) InstallOption {
	return func(opts *options) {
		opts.instReq.Values = &cpb.Config{Raw: string(raw)}
	}
}

// UpdateValueOverrides specifies a list of values to include when upgrading
func UpdateValueOverrides(raw []byte) UpdateOption {
	return func(opts *options) {
		opts.updateReq.Values = &cpb.Config{Raw: string(raw)}
	}
}

// ReleaseName specifies the name of the release when installing.
func ReleaseName(name string) InstallOption {
	return func(opts *options) {
		opts.instReq.Name = name
	}
}

// DeleteDisableHooks will disable hooks for a deletion operation.
func DeleteDisableHooks(disable bool) DeleteOption {
	return func(opts *options) {
		opts.disableHooks = disable
	}
}

// DeleteDryRun will (if true) execute a deletion as a dry run.
func DeleteDryRun(dry bool) DeleteOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// UpgradeDryRun will (if true) execute an upgrade as a dry run.
func UpgradeDryRun(dry bool) UpdateOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// InstallDisableHooks disables hooks during installation.
func InstallDisableHooks(disable bool) InstallOption {
	return func(opts *options) {
		opts.disableHooks = disable
	}
}

// InstallDryRun will (if true) execute an installation as a dry run.
func InstallDryRun(dry bool) InstallOption {
	return func(opts *options) {
		opts.dryRun = dry
	}
}

// InstallReuseName will (if true) instruct Tiller to re-use an existing name.
func InstallReuseName(reuse bool) InstallOption {
	return func(opts *options) {
		opts.reuseName = reuse
	}
}

// ContentOption -- TODO
type ContentOption func(*options)

// StatusOption -- TODO
type StatusOption func(*options)

// DeleteOption -- TODO
type DeleteOption func(*options)

// UpdateOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm upgrade` command.
type UpdateOption func(*options)

// RPC helpers defined on `options` type. Note: These actually execute the
// the corresponding tiller RPC. There is no particular reason why these
// are APIs are hung off `options`, they are internal to pkg/helm to remain
// malleable.

// Executes tiller.ListReleases RPC.
func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) {
	// apply release list options
	for _, opt := range opts {
		opt(o)
	}
	s, err := rlc.ListReleases(context.TODO(), &o.listReq)
	if err != nil {
		return nil, err
	}

	return s.Recv()
}

// Executes tiller.InstallRelease RPC.
func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
	// apply the install options
	for _, opt := range opts {
		opt(o)
	}
	o.instReq.Chart = chr
	o.instReq.Namespace = ns
	o.instReq.DryRun = o.dryRun
	o.instReq.DisableHooks = o.disableHooks
	o.instReq.ReuseName = o.reuseName

	return rlc.InstallRelease(context.TODO(), &o.instReq)
}

// Executes tiller.UninstallRelease RPC.
func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) {
	for _, opt := range opts {
		opt(o)
	}
	if o.dryRun {
		// In the dry run case, just see if the release exists
		r, err := o.rpcGetReleaseContent(rlsName, rlc)
		if err != nil {
			return &rls.UninstallReleaseResponse{}, err
		}
		return &rls.UninstallReleaseResponse{Release: r.Release}, nil
	}
	return rlc.UninstallRelease(context.TODO(), &rls.UninstallReleaseRequest{Name: rlsName, DisableHooks: o.disableHooks})
}

// Executes tiller.UpdateRelease RPC.
func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
	//TODO: handle dryRun
	for _, opt := range opts {
		opt(o)
	}

	o.updateReq.Chart = chr
	o.updateReq.DryRun = o.dryRun
	o.updateReq.Name = rlsName

	return rlc.UpdateRelease(context.TODO(), &o.updateReq)
}

// Executes tiller.GetReleaseStatus RPC.
func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClient, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) {
	req := &rls.GetReleaseStatusRequest{Name: rlsName}
	return rlc.GetReleaseStatus(context.TODO(), req)
}

// Executes tiller.GetReleaseContent.
func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceClient, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) {
	req := &rls.GetReleaseContentRequest{Name: rlsName}
	return rlc.GetReleaseContent(context.TODO(), req)
}
<|endoftext|>


<commit_before>// +build linux

package mount

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"syscall"

	"go.pedge.io/dlog"
)

// Manager defines the interface for keep track of volume driver mounts.
type Manager interface {
	// String representation of the mount table
	String() string
	// Reload mount table for specified device.
	Reload(source string) error
	// Load mount table for all devices that match the list of identifiers
	Load(source []string) error
	// Inspect mount table for specified source. ErrEnoent may be returned.
	Inspect(source string) []*PathInfo
	// Mounts returns paths for specified source.
	Mounts(source string) []string
	// HasMounts determines returns the number of mounts for the source.
	HasMounts(source string) int
	// HasTarget determines returns the number of mounts for the target.
	HasTarget(target string) (string, bool)
	// Exists returns true if the device is mounted at specified path.
	// returned if the device does not exists.
	Exists(source, path string) (bool, error)
	// GetSourcePath scans mount for a specified mountPath and returns the
	// sourcePath if found or returnes an ErrEnoent
	GetSourcePath(mountPath string) (string, error)
	// GetSourcePaths returns all source paths from the mount table
	GetSourcePaths() []string
	// Mount device at mountpoint
	Mount(minor int, device, path, fs string, flags uintptr, data string, timeout int) error
	// Unmount device at mountpoint and remove from the matrix.
	// ErrEnoent is returned if the device or mountpoint for the device
	// is not found.
	Unmount(source, path string, flags int, timeout int) error
}

// MountImpl backend implementation for Mount/Unmount calls
type MountImpl interface {
	Mount(source, target, fstype string, flags uintptr, data string, timeout int) error
	Unmount(target string, flags int, timeout int) error
}

// MountType indicates different mount types supported
type MountType int

const (
	// DeviceMount indicates a device mount type
	DeviceMount MountType = 1 << iota
	// NFSMount indicates a NFS mount point
	NFSMount
	// CustomMount indicates a custom mount type with its
	// own defined way of handling mount table
	CustomMount
)

var (
	// ErrExist is returned if path is already mounted to a different device.
	ErrExist = errors.New("Mountpath already exists")
	// ErrEnoent is returned for a non existent mount point
	ErrEnoent = errors.New("Mountpath is not mounted")
	// ErrEinval is returned is fields for an entry do no match
	// existing fields
	ErrEinval = errors.New("Invalid arguments for mount entry")
	// ErrUnsupported is returned for an unsupported operation or a mount type.
	ErrUnsupported = errors.New("Not supported")
	// ErrMountpathNotAllowed is returned when the requested mountpath is not
	// a part of the provided allowed mount paths
	ErrMountpathNotAllowed = errors.New("Mountpath is not allowed")
)

// DeviceMap map device name to Info
type DeviceMap map[string]*Info

// PathMap map path name to device
type PathMap map[string]string

// PathInfo is a reference counted path
type PathInfo struct {
	Path string
}

// Info per device
type Info struct {
	sync.Mutex
	Device     string
	Minor      int
	Mountpoint []*PathInfo
	Fs         string
}

// Mounter implements Ops and keeps track of active mounts for volume drivers.
type Mounter struct {
	sync.Mutex
	mountImpl   MountImpl
	mounts      DeviceMap
	paths       PathMap
	allowedDirs []string
}

// DefaultMounter defaults to syscall implementation.
type DefaultMounter struct {
}

// Mount default mount implementation is syscall.
func (m *DefaultMounter) Mount(
	source string,
	target string,
	fstype string,
	flags uintptr,
	data string,
	timeout int,
) error {
	return syscall.Mount(source, target, fstype, flags, data)
}

// Unmount default unmount implementation is syscall.
func (m *DefaultMounter) Unmount(target string, flags int, timeout int) error {
	return syscall.Unmount(target, flags)
}

// String representation of Mounter
func (m *Mounter) String() string {
	return fmt.Sprintf("%#v", *m)
}

// Inspect mount table for device
func (m *Mounter) Inspect(sourcePath string) []*PathInfo {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return nil
	}
	return v.Mountpoint
}

// Mounts returns  mount table for device
func (m *Mounter) Mounts(sourcePath string) []string {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return nil
	}

	mounts := make([]string, len(v.Mountpoint))
	for i, v := range v.Mountpoint {
		mounts[i] = v.Path
	}

	return mounts
}

// GetSourcePaths returns all source paths from the mount table
func (m *Mounter) GetSourcePaths() []string {
	m.Lock()
	defer m.Unlock()

	sourcePaths := make([]string, len(m.mounts))
	i := 0
	for path := range m.mounts {
		sourcePaths[i] = path
		i++
	}
	return sourcePaths
}

// HasMounts determines returns the number of mounts for the device.
func (m *Mounter) HasMounts(sourcePath string) int {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return 0
	}
	return len(v.Mountpoint)
}

// HasTarget returns true/false based on the target provided
func (m *Mounter) HasTarget(targetPath string) (string, bool) {
	m.Lock()
	defer m.Unlock()

	for k, v := range m.mounts {
		for _, p := range v.Mountpoint {
			if p.Path == targetPath {
				return k, true
			}
		}
	}
	return "", false
}

// Exists scans mountpaths for specified device and returns true if path is one of the
// mountpaths. ErrEnoent may be retuned if the device is not found
func (m *Mounter) Exists(sourcePath string, path string) (bool, error) {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return false, ErrEnoent
	}
	for _, p := range v.Mountpoint {
		if p.Path == path {
			return true, nil
		}
	}
	return false, nil
}

// GetSourcePath scans mount for a specified mountPath and returns the sourcePath
// if found or returnes an ErrEnoent
func (m *Mounter) GetSourcePath(mountPath string) (string, error) {
	m.Lock()
	defer m.Unlock()

	for k, v := range m.mounts {
		for _, p := range v.Mountpoint {
			if p.Path == mountPath {
				return k, nil
			}
		}
	}
	return "", ErrEnoent
}

func normalizeMountPath(mountPath string) string {
	if len(mountPath) > 1 && strings.HasSuffix(mountPath, "/") {
		return mountPath[:len(mountPath)-1]
	}
	return mountPath
}

func (m *Mounter) maybeRemoveDevice(device string) {
	m.Lock()
	defer m.Unlock()
	if info, ok := m.mounts[device]; ok {
		// If the device has no more mountpoints, remove it from the map
		if len(info.Mountpoint) == 0 {
			delete(m.mounts, device)
		}
	}
}

func (m *Mounter) hasPath(path string) (string, bool) {
	m.Lock()
	defer m.Unlock()
	p, ok := m.paths[path]
	return p, ok
}

func (m *Mounter) addPath(path, device string) {
	m.Lock()
	defer m.Unlock()
	m.paths[path] = device
}

func (m *Mounter) deletePath(path string) bool {
	m.Lock()
	defer m.Unlock()
	if _, pathExists := m.paths[path]; pathExists {
		delete(m.paths, path)
		return true
	}
	return false
}

// Mount new mountpoint for specified device.
func (m *Mounter) Mount(
	minor int,
	device, path, fs string,
	flags uintptr,
	data string,
	timeout int,
) error {
	path = normalizeMountPath(path)
	if len(m.allowedDirs) > 0 {
		foundPrefix := false
		for _, allowedDir := range m.allowedDirs {
			if strings.Contains(path, allowedDir) {
				foundPrefix = true
				break
			}
		}
		if !foundPrefix {
			return ErrMountpathNotAllowed
		}
	}
	dev, ok := m.hasPath(path)
	if ok && dev != device {
		dlog.Warnf("cannot mount %q,  device %q is mounted at %q", device, dev, path)
		return ErrExist
	}
	m.Lock()
	info, ok := m.mounts[device]
	if !ok {
		info = &Info{
			Device:     device,
			Mountpoint: make([]*PathInfo, 0),
			Minor:      minor,
			Fs:         fs,
		}
	}
	m.mounts[device] = info
	m.Unlock()
	info.Lock()
	defer info.Unlock()

	// Validate input params
	if fs != info.Fs {
		dlog.Warnf("%s Existing mountpoint has fs %q cannot change to %q",
			device, info.Fs, fs)
		return ErrEinval
	}

	// Try to find the mountpoint. If it already exists, do nothing
	for _, p := range info.Mountpoint {
		if p.Path == path {
			return nil
		}
	}
	// The device is not mounted at path, mount it and add to its mountpoints.
	err := m.mountImpl.Mount(device, path, fs, flags, data, timeout)
	if err != nil {
		return err
	}
	info.Mountpoint = append(info.Mountpoint, &PathInfo{Path: path})
	m.addPath(path, device)
	return nil
}

// Unmount device at mountpoint and from the matrix.
// ErrEnoent is returned if the device or mountpoint for the device is not found.
func (m *Mounter) Unmount(device, path string, flags int, timeout int) error {
	m.Lock()

	path = normalizeMountPath(path)
	info, ok := m.mounts[device]
	if !ok {
		m.Unlock()
		return ErrEnoent
	}
	m.Unlock()
	info.Lock()
	defer info.Unlock()
	for i, p := range info.Mountpoint {
		if p.Path != path {
			continue
		}
		err := m.mountImpl.Unmount(path, flags, timeout)
		if err != nil {
			return err
		}
		if pathExists := m.deletePath(path); !pathExists {
			dlog.Warnf("Path %q for device %q does not exist in pathMap",
				path, device)
		}
		// Blow away this mountpoint.
		info.Mountpoint[i] = info.Mountpoint[len(info.Mountpoint)-1]
		info.Mountpoint = info.Mountpoint[0 : len(info.Mountpoint)-1]
		m.maybeRemoveDevice(device)
		return nil
	}
	dlog.Warnf("Device %q is not mounted at path %q", path, device)
	return nil
}

// New returns a new Mount Manager
func New(
	mounterType MountType,
	mountImpl MountImpl,
	identifiers []string,
	customMounter CustomMounter,
	allowedDirs []string,
) (Manager, error) {

	if mountImpl == nil {
		mountImpl = &DefaultMounter{}
	}

	switch mounterType {
	case DeviceMount:
		return NewDeviceMounter(identifiers, mountImpl, allowedDirs)
	case NFSMount:
		if len(identifiers) > 1 {
			return nil, fmt.Errorf("Multiple server addresses provided.")
		}
		return NewNFSMounter(identifiers[0], mountImpl, allowedDirs)
	case CustomMount:
		return NewCustomMounter(identifiers, mountImpl, customMounter, allowedDirs)
	}
	return nil, ErrUnsupported
}
<commit_msg>Add APIs to mounter to make mounts shared and unbindable<commit_after>// +build linux

package mount

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"syscall"

	"go.pedge.io/dlog"
)

// Manager defines the interface for keep track of volume driver mounts.
type Manager interface {
	// String representation of the mount table
	String() string
	// Reload mount table for specified device.
	Reload(source string) error
	// Load mount table for all devices that match the list of identifiers
	Load(source []string) error
	// Inspect mount table for specified source. ErrEnoent may be returned.
	Inspect(source string) []*PathInfo
	// Mounts returns paths for specified source.
	Mounts(source string) []string
	// HasMounts determines returns the number of mounts for the source.
	HasMounts(source string) int
	// HasTarget determines returns the number of mounts for the target.
	HasTarget(target string) (string, bool)
	// Exists returns true if the device is mounted at specified path.
	// returned if the device does not exists.
	Exists(source, path string) (bool, error)
	// GetSourcePath scans mount for a specified mountPath and returns the
	// sourcePath if found or returnes an ErrEnoent
	GetSourcePath(mountPath string) (string, error)
	// GetSourcePaths returns all source paths from the mount table
	GetSourcePaths() []string
	// Mount device at mountpoint
	Mount(minor int, device, path, fs string, flags uintptr, data string, timeout int) error
	// Unmount device at mountpoint and remove from the matrix.
	// ErrEnoent is returned if the device or mountpoint for the device
	// is not found.
	Unmount(source, path string, flags int, timeout int) error
	// MakeMountUnbindable marks a mount path as unbindable
	MakeMountUnbindable(device string, path string) error
	// MakeMountShared marks a mount path as shared
	MakeMountShared(device string, path string) error
}

// MountImpl backend implementation for Mount/Unmount calls
type MountImpl interface {
	Mount(source, target, fstype string, flags uintptr, data string, timeout int) error
	Unmount(target string, flags int, timeout int) error
}

// MountType indicates different mount types supported
type MountType int

const (
	// DeviceMount indicates a device mount type
	DeviceMount MountType = 1 << iota
	// NFSMount indicates a NFS mount point
	NFSMount
	// CustomMount indicates a custom mount type with its
	// own defined way of handling mount table
	CustomMount
)

var (
	// ErrExist is returned if path is already mounted to a different device.
	ErrExist = errors.New("Mountpath already exists")
	// ErrEnoent is returned for a non existent mount point
	ErrEnoent = errors.New("Mountpath is not mounted")
	// ErrEinval is returned is fields for an entry do no match
	// existing fields
	ErrEinval = errors.New("Invalid arguments for mount entry")
	// ErrUnsupported is returned for an unsupported operation or a mount type.
	ErrUnsupported = errors.New("Not supported")
	// ErrMountpathNotAllowed is returned when the requested mountpath is not
	// a part of the provided allowed mount paths
	ErrMountpathNotAllowed = errors.New("Mountpath is not allowed")
)

// DeviceMap map device name to Info
type DeviceMap map[string]*Info

// PathMap map path name to device
type PathMap map[string]string

// PathInfo is a reference counted path
type PathInfo struct {
	Path string
}

// Info per device
type Info struct {
	sync.Mutex
	Device     string
	Minor      int
	Mountpoint []*PathInfo
	Fs         string
}

// Mounter implements Ops and keeps track of active mounts for volume drivers.
type Mounter struct {
	sync.Mutex
	mountImpl   MountImpl
	mounts      DeviceMap
	paths       PathMap
	allowedDirs []string
}

// DefaultMounter defaults to syscall implementation.
type DefaultMounter struct {
}

// Mount default mount implementation is syscall.
func (m *DefaultMounter) Mount(
	source string,
	target string,
	fstype string,
	flags uintptr,
	data string,
	timeout int,
) error {
	return syscall.Mount(source, target, fstype, flags, data)
}

// Unmount default unmount implementation is syscall.
func (m *DefaultMounter) Unmount(target string, flags int, timeout int) error {
	return syscall.Unmount(target, flags)
}

// String representation of Mounter
func (m *Mounter) String() string {
	return fmt.Sprintf("%#v", *m)
}

// Inspect mount table for device
func (m *Mounter) Inspect(sourcePath string) []*PathInfo {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return nil
	}
	return v.Mountpoint
}

// Mounts returns  mount table for device
func (m *Mounter) Mounts(sourcePath string) []string {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return nil
	}

	mounts := make([]string, len(v.Mountpoint))
	for i, v := range v.Mountpoint {
		mounts[i] = v.Path
	}

	return mounts
}

// GetSourcePaths returns all source paths from the mount table
func (m *Mounter) GetSourcePaths() []string {
	m.Lock()
	defer m.Unlock()

	sourcePaths := make([]string, len(m.mounts))
	i := 0
	for path := range m.mounts {
		sourcePaths[i] = path
		i++
	}
	return sourcePaths
}

// HasMounts determines returns the number of mounts for the device.
func (m *Mounter) HasMounts(sourcePath string) int {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return 0
	}
	return len(v.Mountpoint)
}

// HasTarget returns true/false based on the target provided
func (m *Mounter) HasTarget(targetPath string) (string, bool) {
	m.Lock()
	defer m.Unlock()

	for k, v := range m.mounts {
		for _, p := range v.Mountpoint {
			if p.Path == targetPath {
				return k, true
			}
		}
	}
	return "", false
}

// Exists scans mountpaths for specified device and returns true if path is one of the
// mountpaths. ErrEnoent may be retuned if the device is not found
func (m *Mounter) Exists(sourcePath string, path string) (bool, error) {
	m.Lock()
	defer m.Unlock()

	v, ok := m.mounts[sourcePath]
	if !ok {
		return false, ErrEnoent
	}
	for _, p := range v.Mountpoint {
		if p.Path == path {
			return true, nil
		}
	}
	return false, nil
}

// GetSourcePath scans mount for a specified mountPath and returns the sourcePath
// if found or returnes an ErrEnoent
func (m *Mounter) GetSourcePath(mountPath string) (string, error) {
	m.Lock()
	defer m.Unlock()

	for k, v := range m.mounts {
		for _, p := range v.Mountpoint {
			if p.Path == mountPath {
				return k, nil
			}
		}
	}
	return "", ErrEnoent
}

func normalizeMountPath(mountPath string) string {
	if len(mountPath) > 1 && strings.HasSuffix(mountPath, "/") {
		return mountPath[:len(mountPath)-1]
	}
	return mountPath
}

func (m *Mounter) maybeRemoveDevice(device string) {
	m.Lock()
	defer m.Unlock()
	if info, ok := m.mounts[device]; ok {
		// If the device has no more mountpoints, remove it from the map
		if len(info.Mountpoint) == 0 {
			delete(m.mounts, device)
		}
	}
}

func (m *Mounter) hasPath(path string) (string, bool) {
	m.Lock()
	defer m.Unlock()
	p, ok := m.paths[path]
	return p, ok
}

func (m *Mounter) addPath(path, device string) {
	m.Lock()
	defer m.Unlock()
	m.paths[path] = device
}

func (m *Mounter) deletePath(path string) bool {
	m.Lock()
	defer m.Unlock()
	if _, pathExists := m.paths[path]; pathExists {
		delete(m.paths, path)
		return true
	}
	return false
}

// Mount new mountpoint for specified device.
func (m *Mounter) Mount(
	minor int,
	device, path, fs string,
	flags uintptr,
	data string,
	timeout int,
) error {
	path = normalizeMountPath(path)
	if len(m.allowedDirs) > 0 {
		foundPrefix := false
		for _, allowedDir := range m.allowedDirs {
			if strings.Contains(path, allowedDir) {
				foundPrefix = true
				break
			}
		}
		if !foundPrefix {
			return ErrMountpathNotAllowed
		}
	}
	dev, ok := m.hasPath(path)
	if ok && dev != device {
		dlog.Warnf("cannot mount %q,  device %q is mounted at %q", device, dev, path)
		return ErrExist
	}
	m.Lock()
	info, ok := m.mounts[device]
	if !ok {
		info = &Info{
			Device:     device,
			Mountpoint: make([]*PathInfo, 0),
			Minor:      minor,
			Fs:         fs,
		}
	}
	m.mounts[device] = info
	m.Unlock()
	info.Lock()
	defer info.Unlock()

	// Validate input params
	if fs != info.Fs {
		dlog.Warnf("%s Existing mountpoint has fs %q cannot change to %q",
			device, info.Fs, fs)
		return ErrEinval
	}

	// Try to find the mountpoint. If it already exists, mark it shared
	for _, p := range info.Mountpoint {
		if p.Path == path {
			m.makeMountShared(device, path)
			return nil
		}
	}
	// The device is not mounted at path, mount it and add to its mountpoints.
	err := m.mountImpl.Mount(device, path, fs, flags, data, timeout)
	if err != nil {
		return err
	}
	err = m.makeMountShared(device, path)
	if err != nil {
		return err
	}

	info.Mountpoint = append(info.Mountpoint, &PathInfo{Path: path})
	m.addPath(path, device)
	return nil
}

// Unmount device at mountpoint and from the matrix.
// ErrEnoent is returned if the device or mountpoint for the device is not found.
func (m *Mounter) Unmount(device, path string, flags int, timeout int) error {
	m.Lock()

	path = normalizeMountPath(path)
	info, ok := m.mounts[device]
	if !ok {
		m.Unlock()
		return ErrEnoent
	}
	m.Unlock()
	info.Lock()
	defer info.Unlock()
	for i, p := range info.Mountpoint {
		if p.Path != path {
			continue
		}
		err := m.makeMountUnbindable(device, path)
		if err != nil {
			return err
		}

		err = m.mountImpl.Unmount(path, flags, timeout)
		if err != nil {
			return err
		}
		if pathExists := m.deletePath(path); !pathExists {
			dlog.Warnf("Path %q for device %q does not exist in pathMap",
				path, device)
		}
		// Blow away this mountpoint.
		info.Mountpoint[i] = info.Mountpoint[len(info.Mountpoint)-1]
		info.Mountpoint = info.Mountpoint[0 : len(info.Mountpoint)-1]
		m.maybeRemoveDevice(device)
		return nil
	}
	dlog.Warnf("Device %q is not mounted at path %q", path, device)
	return nil
}

func (m *Mounter) makeMountUnbindable(
	device string,
	path string,
) error {
	return m.mountImpl.Mount(device, path, "", syscall.MS_UNBINDABLE, "", 0)
}

func (m *Mounter) MakeMountUnbindable(
	device string,
	path string,
) error {
	m.Lock()

	path = normalizeMountPath(path)
	info, ok := m.mounts[device]
	if !ok {
		m.Unlock()
		return ErrEnoent
	}
	m.Unlock()
	info.Lock()
	defer info.Unlock()
	for _, p := range info.Mountpoint {
		if p.Path != path {
			continue
		}
		return m.makeMountUnbindable(device, path)
	}

	dlog.Warnf("Device %q is not mounted at path %q", path, device)
	return nil
}

func (m *Mounter) makeMountShared(
	device string,
	path string,
) error {
	return m.mountImpl.Mount(device, path, "", syscall.MS_SHARED, "", 0)
}

func (m *Mounter) MakeMountShared(
	device string,
	path string,
) error {
	m.Lock()

	path = normalizeMountPath(path)
	info, ok := m.mounts[device]
	if !ok {
		m.Unlock()
		return ErrEnoent
	}
	m.Unlock()
	info.Lock()
	defer info.Unlock()
	for _, p := range info.Mountpoint {
		if p.Path != path {
			continue
		}
		return m.makeMountShared(device, path)
	}

	dlog.Warnf("Device %q is not mounted at path %q", path, device)
	return nil
}

// New returns a new Mount Manager
func New(
	mounterType MountType,
	mountImpl MountImpl,
	identifiers []string,
	customMounter CustomMounter,
	allowedDirs []string,
) (Manager, error) {

	if mountImpl == nil {
		mountImpl = &DefaultMounter{}
	}

	switch mounterType {
	case DeviceMount:
		return NewDeviceMounter(identifiers, mountImpl, allowedDirs)
	case NFSMount:
		if len(identifiers) > 1 {
			return nil, fmt.Errorf("Multiple server addresses provided.")
		}
		return NewNFSMounter(identifiers[0], mountImpl, allowedDirs)
	case CustomMount:
		return NewCustomMounter(identifiers, mountImpl, customMounter, allowedDirs)
	}
	return nil, ErrUnsupported
}
<|endoftext|>


<commit_before>package proxy

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	kube_transport "k8s.io/client-go/transport"

	"github.com/matt-deboer/kuill/pkg/clients"
	"k8s.io/api/authentication/v1"

	"net/http/httputil"

	"github.com/gorilla/websocket"
	"github.com/matt-deboer/kuill/pkg/auth"
	log "github.com/sirupsen/logrus"
)

var whitelistedHeaders = []string{"Content-Type"}

const proxyBasePath = "/proxy"

type KubeAPIProxy struct {
	kubeClients         *clients.KubeClients
	websocketScheme     string
	reverseProxy        *httputil.ReverseProxy
	websocketProxy      *WebsocketProxy
	multiKindWatchProxy *KubeKindAggregatingWatchProxy
	traceRequests       bool
	traceWebsockets     bool
}

func NewKubeAPIProxy(kubeClients *clients.KubeClients,
	traceRequests, traceWebsockets bool, kindLister *KindsProxy,
	namespaceLister *NamespaceProxy, accessAggregator *AccessAggregator) (*KubeAPIProxy, error) {

	transportConfig, err := kubeClients.Config.TransportConfig()
	if err != nil {
		return nil, fmt.Errorf("Failed to resolve transport config; %v", err)
	}

	tlsConfig, err := kube_transport.TLSConfigFor(transportConfig)
	if err != nil {
		return nil, fmt.Errorf("Failed to resolve TLS config; %v", err)
	}

	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		Dial: (&net.Dialer{
			Timeout:   5 * time.Second,
			KeepAlive: 30 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
		TLSClientConfig:     tlsConfig,
	}

	wsURL, _ := url.Parse(kubeClients.BaseURL.String())
	if wsURL.Scheme == "https" {
		wsURL.Scheme = "wss"
	} else {
		wsURL.Scheme = "ws"
	}

	wsp := NewWebsocketProxy(wsURL, traceWebsockets)
	wsp.Upgrader = &websocket.Upgrader{
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
	}
	wsp.Director = func(incoming *http.Request, out http.Header) {
		for k, v := range incoming.Header {
			if k == v1.ImpersonateUserHeader || k == v1.ImpersonateGroupHeader || strings.HasPrefix(k, v1.ImpersonateUserExtraHeaderPrefix) {
				out[k] = v
			}
		}
		out.Set("Origin", kubeClients.BaseURL.String())
		if len(kubeClients.BearerToken) > 0 {
			out.Set("Authorization", fmt.Sprintf("Bearer %s", string(kubeClients.BearerToken)))
		}
	}
	wsp.Dialer = &websocket.Dialer{
		HandshakeTimeout: 5 * time.Second,
		TLSClientConfig:  tlsConfig,
		ReadBufferSize:   1024,
		WriteBufferSize:  1024,
	}

	mwp := NewKubeKindAggregatingWatchProxy(wsURL, traceWebsockets, kindLister, namespaceLister, accessAggregator)
	mwp.Dialer = wsp.Dialer
	mwp.Upgrader = wsp.Upgrader
	mwp.Director = wsp.Director

	log.Infof("Enabled kubernetes api proxy for %v", kubeClients.BaseURL)

	kp := &KubeAPIProxy{
		kubeClients:         kubeClients,
		reverseProxy:        httputil.NewSingleHostReverseProxy(kubeClients.BaseURL),
		websocketProxy:      wsp,
		multiKindWatchProxy: mwp,
		traceRequests:       traceRequests,
		traceWebsockets:     traceWebsockets,
	}

	if traceRequests {
		log.Infof("KubeAPIProxy: tracing requests...")
	}
	if traceWebsockets {
		log.Infof("KubeAPIProxy: tracing websockets...")
	}

	kp.reverseProxy.Director = kp.filterRequest
	kp.reverseProxy.Transport = transport

	return kp, nil
}

// ProxyRequest proxies the request
func (p *KubeAPIProxy) ProxyRequest(w http.ResponseWriter, r *http.Request, authContext auth.Context) {

	if strings.HasPrefix(r.URL.Scheme, "ws") || strings.ToLower(r.Header.Get("Connection")) == "upgrade" {
		r.Header.Set("Origin", p.kubeClients.BaseURL.String())
		if len(r.Header.Get("User-Agent")) == 0 {
			r.Header.Set("User-Agent", "kuill")
		}
		p.traceRequest(r, authContext, p.traceWebsockets)
		authContext.Impersonate(r.Header)

		if r.URL.Path == "/proxy/_/multiwatch" {
			p.multiKindWatchProxy.AggregateWatches(w, r, authContext)
		} else {
			r.URL.Path = strings.Replace(r.URL.Path, proxyBasePath, "", 1)
			r.URL.RawPath = strings.Replace(r.URL.RawPath, proxyBasePath, "", 1)
			p.websocketProxy.ServeHTTP(w, r)
		}
	} else {
		p.reverseProxy.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), auth.ContextKey, authContext)))
	}
}

func (p *KubeAPIProxy) filterRequest(r *http.Request) {
	r.URL.Path = strings.Replace(r.URL.Path, proxyBasePath, "", 1)
	r.URL.RawPath = strings.Replace(r.URL.RawPath, proxyBasePath, "", 1)
	r.URL.Scheme = p.kubeClients.BaseURL.Scheme
	r.URL.Host = p.kubeClients.BaseURL.Host
	if _, ok := r.Header["User-Agent"]; !ok {
		// explicitly disable User-Agent so it's not set to default value
		r.Header.Set("User-Agent", "")
	}
	r.Header.Set("Origin", p.kubeClients.BaseURL.String())
	authContext := r.Context().Value(auth.ContextKey).(auth.Context)
	p.traceRequest(r, authContext, p.traceWebsockets)
	authContext.Impersonate(r.Header)
}

func (p *KubeAPIProxy) traceRequest(r *http.Request, authContext auth.Context, trace bool) {

	if trace {
		lctx := log.WithFields(log.Fields{
			"method": "ProxyRequest",
			"req":    r.URL.String(),
		})

		data, err := httputil.DumpRequest(r, r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH")
		if err != nil {
			lctx.Warnf("Error dumping request : %v", err)
		} else {
			lctx.Infof("Proxying request for %s: %s\n%s", authContext.User(), r.URL, string(data))
		}
	}
}
<commit_msg>also set bearer token on reverse proxy<commit_after>package proxy

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	kube_transport "k8s.io/client-go/transport"

	"github.com/matt-deboer/kuill/pkg/clients"
	"k8s.io/api/authentication/v1"

	"net/http/httputil"

	"github.com/gorilla/websocket"
	"github.com/matt-deboer/kuill/pkg/auth"
	log "github.com/sirupsen/logrus"
)

var whitelistedHeaders = []string{"Content-Type"}

const proxyBasePath = "/proxy"

type KubeAPIProxy struct {
	kubeClients         *clients.KubeClients
	websocketScheme     string
	reverseProxy        *httputil.ReverseProxy
	websocketProxy      *WebsocketProxy
	multiKindWatchProxy *KubeKindAggregatingWatchProxy
	traceRequests       bool
	traceWebsockets     bool
}

func NewKubeAPIProxy(kubeClients *clients.KubeClients,
	traceRequests, traceWebsockets bool, kindLister *KindsProxy,
	namespaceLister *NamespaceProxy, accessAggregator *AccessAggregator) (*KubeAPIProxy, error) {

	transportConfig, err := kubeClients.Config.TransportConfig()
	if err != nil {
		return nil, fmt.Errorf("Failed to resolve transport config; %v", err)
	}

	tlsConfig, err := kube_transport.TLSConfigFor(transportConfig)
	if err != nil {
		return nil, fmt.Errorf("Failed to resolve TLS config; %v", err)
	}

	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		Dial: (&net.Dialer{
			Timeout:   5 * time.Second,
			KeepAlive: 30 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
		TLSClientConfig:     tlsConfig,
	}

	wsURL, _ := url.Parse(kubeClients.BaseURL.String())
	if wsURL.Scheme == "https" {
		wsURL.Scheme = "wss"
	} else {
		wsURL.Scheme = "ws"
	}

	wsp := NewWebsocketProxy(wsURL, traceWebsockets)
	wsp.Upgrader = &websocket.Upgrader{
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
	}
	wsp.Director = func(incoming *http.Request, out http.Header) {
		for k, v := range incoming.Header {
			if k == v1.ImpersonateUserHeader || k == v1.ImpersonateGroupHeader || strings.HasPrefix(k, v1.ImpersonateUserExtraHeaderPrefix) {
				out[k] = v
			}
		}
		out.Set("Origin", kubeClients.BaseURL.String())
		if len(kubeClients.BearerToken) > 0 {
			out.Set("Authorization", fmt.Sprintf("Bearer %s", string(kubeClients.BearerToken)))
		}
	}
	wsp.Dialer = &websocket.Dialer{
		HandshakeTimeout: 5 * time.Second,
		TLSClientConfig:  tlsConfig,
		ReadBufferSize:   1024,
		WriteBufferSize:  1024,
	}

	mwp := NewKubeKindAggregatingWatchProxy(wsURL, traceWebsockets, kindLister, namespaceLister, accessAggregator)
	mwp.Dialer = wsp.Dialer
	mwp.Upgrader = wsp.Upgrader
	mwp.Director = wsp.Director

	log.Infof("Enabled kubernetes api proxy for %v", kubeClients.BaseURL)

	kp := &KubeAPIProxy{
		kubeClients:         kubeClients,
		reverseProxy:        httputil.NewSingleHostReverseProxy(kubeClients.BaseURL),
		websocketProxy:      wsp,
		multiKindWatchProxy: mwp,
		traceRequests:       traceRequests,
		traceWebsockets:     traceWebsockets,
	}

	if traceRequests {
		log.Infof("KubeAPIProxy: tracing requests...")
	}
	if traceWebsockets {
		log.Infof("KubeAPIProxy: tracing websockets...")
	}

	kp.reverseProxy.Director = kp.filterRequest
	kp.reverseProxy.Transport = transport

	return kp, nil
}

// ProxyRequest proxies the request
func (p *KubeAPIProxy) ProxyRequest(w http.ResponseWriter, r *http.Request, authContext auth.Context) {

	if strings.HasPrefix(r.URL.Scheme, "ws") || strings.ToLower(r.Header.Get("Connection")) == "upgrade" {
		r.Header.Set("Origin", p.kubeClients.BaseURL.String())
		if len(r.Header.Get("User-Agent")) == 0 {
			r.Header.Set("User-Agent", "kuill")
		}
		p.traceRequest(r, authContext, p.traceWebsockets)
		authContext.Impersonate(r.Header)

		if r.URL.Path == "/proxy/_/multiwatch" {
			p.multiKindWatchProxy.AggregateWatches(w, r, authContext)
		} else {
			r.URL.Path = strings.Replace(r.URL.Path, proxyBasePath, "", 1)
			r.URL.RawPath = strings.Replace(r.URL.RawPath, proxyBasePath, "", 1)
			p.websocketProxy.ServeHTTP(w, r)
		}
	} else {
		p.reverseProxy.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), auth.ContextKey, authContext)))
	}
}

func (p *KubeAPIProxy) filterRequest(r *http.Request) {
	r.URL.Path = strings.Replace(r.URL.Path, proxyBasePath, "", 1)
	r.URL.RawPath = strings.Replace(r.URL.RawPath, proxyBasePath, "", 1)
	r.URL.Scheme = p.kubeClients.BaseURL.Scheme
	r.URL.Host = p.kubeClients.BaseURL.Host
	if _, ok := r.Header["User-Agent"]; !ok {
		// explicitly disable User-Agent so it's not set to default value
		r.Header.Set("User-Agent", "")
	}
	r.Header.Set("Origin", p.kubeClients.BaseURL.String())
	if len(p.kubeClients.BearerToken) > 0 {
		r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(p.kubeClients.BearerToken)))
	}
	authContext := r.Context().Value(auth.ContextKey).(auth.Context)
	p.traceRequest(r, authContext, p.traceWebsockets)
	authContext.Impersonate(r.Header)
}

func (p *KubeAPIProxy) traceRequest(r *http.Request, authContext auth.Context, trace bool) {

	if trace {
		lctx := log.WithFields(log.Fields{
			"method": "ProxyRequest",
			"req":    r.URL.String(),
		})

		data, err := httputil.DumpRequest(r, r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH")
		if err != nil {
			lctx.Warnf("Error dumping request : %v", err)
		} else {
			lctx.Infof("Proxying request for %s: %s\n%s", authContext.User(), r.URL, string(data))
		}
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2015 The Camlistore 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 server

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
	"sync"

	"camlistore.org/pkg/blobserver"
	"camlistore.org/pkg/httputil"
	"camlistore.org/pkg/jsonconfig"
	"camlistore.org/pkg/types/clientconfig"
)

const helpHTML string = `

			<title>Help</title>

Help

See the Camlistore online documentation and community contacts.

Client Configuration

You will need to use the following client configuration in order to access this server using the Camlistore command line tools.

{{ . }}

	`

// HelpHandler publishes information related to accessing the server
type HelpHandler struct {
	clientConfig *clientconfig.Config // generated from serverConfig
	serverConfig jsonconfig.Obj       // low-level config
	goTemplate   *template.Template   // for rendering
}

// setServerConfigOnce guards operation within SetServerConfig
var setServerConfigOnce sync.Once

// SetServerConfig enables the handler to receive the server config
// before InitHandler, which generates a client config from the server config, is called.
func (hh *HelpHandler) SetServerConfig(config jsonconfig.Obj) {
	setServerConfigOnce.Do(func() { hh.serverConfig = config })
}

func init() {
	blobserver.RegisterHandlerConstructor("help", newHelpFromConfig)
}

func (hh *HelpHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
	if hh.serverConfig == nil {
		return fmt.Errorf("HelpHandler's serverConfig must be set before calling its InitHandler")
	}

	clientConfig, err := clientconfig.GenerateClientConfig(hh.serverConfig)
	if err != nil {
		return fmt.Errorf("error generating client config: %v", err)
	}
	hh.clientConfig = clientConfig

	tmpl, err := template.New("help").Parse(helpHTML)
	if err != nil {
		return fmt.Errorf("error creating template: %v", err)
	}
	hh.goTemplate = tmpl

	return nil
}

func newHelpFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) {
	return &HelpHandler{}, nil
}

func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	suffix := httputil.PathSuffix(req)
	if !httputil.IsGet(req) {
		http.Error(rw, "Illegal help method.", http.StatusMethodNotAllowed)
		return
	}
	switch suffix {
	case "":
		hh.serveHelpHTML(rw, req)
	default:
		http.Error(rw, "Illegal help path.", http.StatusNotFound)
	}
}

func (hh *HelpHandler) serveHelpHTML(rw http.ResponseWriter, req *http.Request) {
	jsonBytes, err := json.MarshalIndent(hh.clientConfig, "", "  ")
	if err != nil {
		httputil.ServeError(rw, req, fmt.Errorf("could not serialize client config JSON: %v", err))
		return
	}

	hh.goTemplate.Execute(rw, string(jsonBytes))
}
<commit_msg>server/help: Add search bar predicates. reorganize.<commit_after>/*
Copyright 2015 The Camlistore 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 server

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
	"sync"

	"camlistore.org/pkg/blobserver"
	"camlistore.org/pkg/httputil"
	"camlistore.org/pkg/jsonconfig"
	"camlistore.org/pkg/types/clientconfig"
)

const helpHTML string = `

			<title>Help</title>

Help

Web User Interface

Search bar predicates.

Client Configuration

You will need to use the following client configuration in order to access this server using the Camlistore command line tools.

{{ . }}

Anything Else?

See the Camlistore online documentation and community contacts.

	`

// HelpHandler publishes information related to accessing the server
type HelpHandler struct {
	clientConfig *clientconfig.Config // generated from serverConfig
	serverConfig jsonconfig.Obj       // low-level config
	goTemplate   *template.Template   // for rendering
}

// setServerConfigOnce guards operation within SetServerConfig
var setServerConfigOnce sync.Once

// SetServerConfig enables the handler to receive the server config
// before InitHandler, which generates a client config from the server config, is called.
func (hh *HelpHandler) SetServerConfig(config jsonconfig.Obj) {
	setServerConfigOnce.Do(func() { hh.serverConfig = config })
}

func init() {
	blobserver.RegisterHandlerConstructor("help", newHelpFromConfig)
}

func (hh *HelpHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
	if hh.serverConfig == nil {
		return fmt.Errorf("HelpHandler's serverConfig must be set before calling its InitHandler")
	}

	clientConfig, err := clientconfig.GenerateClientConfig(hh.serverConfig)
	if err != nil {
		return fmt.Errorf("error generating client config: %v", err)
	}
	hh.clientConfig = clientConfig

	tmpl, err := template.New("help").Parse(helpHTML)
	if err != nil {
		return fmt.Errorf("error creating template: %v", err)
	}
	hh.goTemplate = tmpl

	return nil
}

func newHelpFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) {
	return &HelpHandler{}, nil
}

func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	suffix := httputil.PathSuffix(req)
	if !httputil.IsGet(req) {
		http.Error(rw, "Illegal help method.", http.StatusMethodNotAllowed)
		return
	}
	switch suffix {
	case "":
		hh.serveHelpHTML(rw, req)
	default:
		http.Error(rw, "Illegal help path.", http.StatusNotFound)
	}
}

func (hh *HelpHandler) serveHelpHTML(rw http.ResponseWriter, req *http.Request) {
	jsonBytes, err := json.MarshalIndent(hh.clientConfig, "", "  ")
	if err != nil {
		httputil.ServeError(rw, req, fmt.Errorf("could not serialize client config JSON: %v", err))
		return
	}

	hh.goTemplate.Execute(rw, string(jsonBytes))
}
<|endoftext|>


<commit_before>package plugins

import (
	"../answers"
	"../ircclient"
	"bufio"
	"fmt"
	"log"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	default_time_format = "2006-01-02T15:04"
)

type QuoteDBPlugin struct {
	sync.RWMutex
	ic *ircclient.IRCClient
}

func (q *QuoteDBPlugin) String() string {
	return "quotedb"
}

func (q *QuoteDBPlugin) Info() string {
	return "collects and displays quotes from users"
}

func (q *QuoteDBPlugin) Usage(cmd string) string {
	switch cmd {
	case "quote":
		return "quote <arg>: if arg is an integer, return quote number <arg>, else if <arg> is empty return a random quote, else return a random quote from user <arg>"
	case "add":
		return "add <quote>: adds the string <quote> appended to the current time to the database"
	}
	return ""
}

func (q *QuoteDBPlugin) Register(cl *ircclient.IRCClient) {
	q.ic = cl

	if q.ic.GetStringOption("QuoteDB", "timeformat") == "" {
		log.Println("added default timeformat value of \"" + default_time_format + "\" to config file")
		q.ic.SetStringOption("QuoteDB", "timeformat", default_time_format)
	}

	q.ic.RegisterCommandHandler("quote", 0, 0, q)
	q.ic.RegisterCommandHandler("add", 1, 0, q)
}

func (q *QuoteDBPlugin) Unregister() {
	return
}

func (q *QuoteDBPlugin) ProcessLine(msg *ircclient.IRCMessage) {
	return
}

func (q *QuoteDBPlugin) ProcessCommand(cmd *ircclient.IRCCommand) {
	switch cmd.Command {
	case "quote":
		var out string
		if len(cmd.Args) == 0 {
			// no argument, get random quote
			out = q.getRandomQuote("")
			if out == "" {
				out = "No quotes in database"
			}
		} else {
			num64, err := strconv.ParseUint(cmd.Args[0], 10, 0)
			if err != nil {
				// argument is no integer, so it must be a string specifing a user
				out = q.getRandomQuote(cmd.Args[0])
				if out == "" {
					out = "No quotes from user " + cmd.Args[0]
				}
			} else {
				// argument is integer
				out = q.getQuote(uint(num64))
				if out == "" {
					out = "Quote not found"
				}
			}
		}
		q.ic.Reply(cmd, out)
	case "add":
		num := q.writeQuote(strings.Join(cmd.Args, " "), time.Now())
		out := fmt.Sprintf(answers.RandStr("addedQuote"), num)
		q.ic.Reply(cmd, out)
	}
}

// return a channel on which all lines of file <file> are send
func (q *QuoteDBPlugin) lines(file string) <-chan string {
	strChan := make(chan string)
	go func(sc chan<- string) {
		// close chan
		defer close(sc)

		q.RLock()
		defer q.RUnlock()

		f, err := os.Open(file)
		if err != nil {
			log.Println(err)
			return
		}
		defer f.Close()

		// new scanner that splits at line endings
		fScanner := bufio.NewScanner(f)
		for fScanner.Scan() {
			sc <- fScanner.Text()
		}

		err = fScanner.Err()
		if err != nil {
			log.Println(err)
			return
		}
	}(strChan)
	return strChan
}

// get the line <lineno> from file <file>
// if the line dosn't exist or a error occurs an empty string is returned
func (q *QuoteDBPlugin) getLine(file string, lineno uint) string {
	strChan := q.lines(file)
	var i uint = 0
	var out string
	for line := range strChan {
		if i == lineno {
			out = line
		}
		i++
	}
	return out
}

// returns the quote number <num>
// if the quote doesn't exist, returns an empty string
func (q *QuoteDBPlugin) getQuote(num uint) string {
	return q.getLine(q.ic.GetStringOption("QuoteDB", "file"), num)
}

// returns a random quote from user <user>
// if <user> is an empty string, returns a random quote from every user
// if there are no quotes from this user, returns an empty string
func (q *QuoteDBPlugin) getRandomQuote(user string) string {
	strChan := q.lines(q.ic.GetStringOption("QuoteDB", "file"))
	var i uint = 0
	quotes := make([]uint, 0)

	for line := range strChan {
		subs := strings.SplitN(line, " ", 3)
		if len(subs) >= 2 {
			// second substring is <user>
			if strings.Contains(subs[1], user) {
				quotes = append(quotes, i)
			}
		}
		i++
	}

	if len(quotes) == 0 {
		// no quotes found
		return ""
	}

	randQuote := quotes[rand.Intn(len(quotes))]
	out := q.getQuote(randQuote)
	return out
}

// writes the line <line> to the file <file>
// returns an error if any occurs
func (q *QuoteDBPlugin) writeLine(file, line string) (err error) {
	q.Lock()
	defer q.Unlock()

	f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
	if err != nil {
		return
	}
	defer f.Close()

	_, err = f.WriteString(line + "\n")
	if err != nil {
		return
	}

	return nil
}

// adds a line to the database consisting of the time <t> and the string <quote>
// returns which number the new quote is
func (q *QuoteDBPlugin) writeQuote(quote string, t time.Time) uint {
	line := fmt.Sprintf("%v %v", t.Format(q.ic.GetStringOption("QuoteDB", "timeformat")), quote)
	err := q.writeLine(q.ic.GetStringOption("QuoteDB", "file"), line)
	if err != nil {
		log.Println(err)
	}

	strChan := q.lines(q.ic.GetStringOption("QuoteDB", "file"))
	var i uint = 0
	for _ = range strChan {
		i++
	}
	return i - 1 // quotes numbering starts at 0
}
<commit_msg>the quotes db could now be searched<commit_after>package plugins

import (
	"../answers"
	"../ircclient"
	"bufio"
	"fmt"
	"log"
	"math/rand"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	default_time_format = "2006-01-02T15:04"
)

type QuoteDBPlugin struct {
	sync.RWMutex
	ic *ircclient.IRCClient
}

func (q *QuoteDBPlugin) String() string {
	return "quotedb"
}

func (q *QuoteDBPlugin) Info() string {
	return "collects and displays quotes from users"
}

func (q *QuoteDBPlugin) Usage(cmd string) string {
	switch cmd {
	case "quote":
		return "quote <arg>: if arg is an integer, return quote number <arg>, else if <arg> is empty return a random quote, else return a random quote from user <arg>"
	case "search":
		return "search <pattern>: search for <pattern> in quote database, interpret <pattern> as regex if it starts and ends with an '/'"
	case "add":
		return "add <quote>: adds the string <quote> appended to the current time to the database"
	}
	return ""
}

func (q *QuoteDBPlugin) Register(cl *ircclient.IRCClient) {
	q.ic = cl

	if q.ic.GetStringOption("QuoteDB", "timeformat") == "" {
		log.Println("added default timeformat value of \"" + default_time_format + "\" to config file")
		q.ic.SetStringOption("QuoteDB", "timeformat", default_time_format)
	}

	q.ic.RegisterCommandHandler("quote", 0, 0, q)
	q.ic.RegisterCommandHandler("search", 1, 0, q)
	q.ic.RegisterCommandHandler("add", 1, 0, q)
}

func (q *QuoteDBPlugin) Unregister() {
	return
}

func (q *QuoteDBPlugin) ProcessLine(msg *ircclient.IRCMessage) {
	return
}

func (q *QuoteDBPlugin) ProcessCommand(cmd *ircclient.IRCCommand) {
	switch cmd.Command {
	case "quote":
		var out string
		if len(cmd.Args) == 0 {
			// no argument, get random quote
			out = q.getRandomQuote("")
			if out == "" {
				out = "No quotes in database"
			}
		} else {
			num64, err := strconv.ParseUint(cmd.Args[0], 10, 0)
			if err != nil {
				// argument is no integer, so it must be a string specifing a user
				out = q.getRandomQuote(cmd.Args[0])
				if out == "" {
					out = "No quotes from user " + cmd.Args[0]
				}
			} else {
				// argument is integer
				out = q.getQuote(uint(num64))
				if out == "" {
					out = "Quote not found"
				}
			}
		}
		q.ic.Reply(cmd, out)
	case "search":
		results := q.searchQuotes(strings.Join(cmd.Args, " "))
		if len(results) == 0 {
			q.ic.Reply(cmd, "Didn't find any matching quotes")
		}

		for _, quote := range results {
			q.ic.Reply(cmd, quote)
		}
	case "add":
		num := q.writeQuote(strings.Join(cmd.Args, " "), time.Now())
		out := fmt.Sprintf(answers.RandStr("addedQuote"), num)
		q.ic.Reply(cmd, out)
	}
}

// return a channel on which all lines of file <file> are send
func (q *QuoteDBPlugin) lines(file string) <-chan string {
	strChan := make(chan string)
	go func(sc chan<- string) {
		// close chan
		defer close(sc)

		q.RLock()
		defer q.RUnlock()

		f, err := os.Open(file)
		if err != nil {
			log.Println(err)
			return
		}
		defer f.Close()

		// new scanner that splits at line endings
		fScanner := bufio.NewScanner(f)
		for fScanner.Scan() {
			sc <- fScanner.Text()
		}

		err = fScanner.Err()
		if err != nil {
			log.Println(err)
			return
		}
	}(strChan)
	return strChan
}

// get the line <lineno> from file <file>
// if the line dosn't exist or a error occurs an empty string is returned
func (q *QuoteDBPlugin) getLine(file string, lineno uint) string {
	strChan := q.lines(file)
	var i uint = 0
	var out string
	for line := range strChan {
		if i == lineno {
			out = line
		}
		i++
	}
	return out
}

// returns the quote number <num>
// if the quote doesn't exist, returns an empty string
func (q *QuoteDBPlugin) getQuote(num uint) string {
	return q.getLine(q.ic.GetStringOption("QuoteDB", "file"), num)
}

// returns a random quote from user <user>
// if <user> is an empty string, returns a random quote from every user
// if there are no quotes from this user, returns an empty string
func (q *QuoteDBPlugin) getRandomQuote(user string) string {
	strChan := q.lines(q.ic.GetStringOption("QuoteDB", "file"))
	var i uint = 0
	quotes := make([]uint, 0)

	for line := range strChan {
		subs := strings.SplitN(line, " ", 3)
		if len(subs) >= 2 {
			// second substring is <user>
			if strings.Contains(subs[1], user) {
				quotes = append(quotes, i)
			}
		}
		i++
	}

	if len(quotes) == 0 {
		// no quotes found
		return ""
	}

	randQuote := quotes[rand.Intn(len(quotes))]
	out := q.getQuote(randQuote)
	return out
}

// searches for quotes and returns them as a string slice
// if <pattern> begins and ends with an '/', interprets it as a regular expression
func (q *QuoteDBPlugin) searchQuotes(pattern string) (results []string) {
	results = make([]string, 0)
	var regex *regexp.Regexp
	if len(pattern) > 1 && strings.HasPrefix(pattern, "/") && strings.HasSuffix(pattern, "/") {
		var err error
		regex, err = regexp.Compile(pattern[1 : len(pattern)-1]) // trim '/'s
		if err != nil {
			results = append(results, "Couldn't search: "+err.Error())
			return
		}
	}

	strChan := q.lines(q.ic.GetStringOption("QuoteDB", "file"))
	var i int = 0
	for line := range strChan {
		// if specified, match the regular expression or just check if <line> contains <pattern>
		if (regex != nil && regex.MatchString(line)) || strings.Contains(line, pattern) {
			results = append(results, strconv.Itoa(i)+" "+line)
		}
		i++
	}
	return results
}

// writes the line <line> to the file <file>
// returns an error if any occurs
func (q *QuoteDBPlugin) writeLine(file, line string) (err error) {
	q.Lock()
	defer q.Unlock()

	f, err := os.OpenFile(file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
	if err != nil {
		return
	}
	defer f.Close()

	_, err = f.WriteString(line + "\n")
	if err != nil {
		return
	}

	return nil
}

// adds a line to the database consisting of the time <t> and the string <quote>
// returns which number the new quote is
func (q *QuoteDBPlugin) writeQuote(quote string, t time.Time) uint {
	line := fmt.Sprintf("%v %v", t.Format(q.ic.GetStringOption("QuoteDB", "timeformat")), quote)
	err := q.writeLine(q.ic.GetStringOption("QuoteDB", "file"), line)
	if err != nil {
		log.Println(err)
	}

	strChan := q.lines(q.ic.GetStringOption("QuoteDB", "file"))
	var i uint = 0
	for _ = range strChan {
		i++
	}
	return i - 1 // quotes numbering starts at 0
}
<|endoftext|>