<commit_before>package detailed

import (
	"fmt"
	"strings"

	"github.com/weaveworks/scope/probe/awsecs"
	"github.com/weaveworks/scope/probe/docker"
	"github.com/weaveworks/scope/probe/host"
	"github.com/weaveworks/scope/probe/kubernetes"
	"github.com/weaveworks/scope/probe/overlay"
	"github.com/weaveworks/scope/probe/process"
	"github.com/weaveworks/scope/render"
	"github.com/weaveworks/scope/report"
)

// Shapes that are allowed
const (
	ImageNameNone = "<none>"

	// Keys we use to render container names
	AmazonECSContainerNameLabel  = "com.amazonaws.ecs.container-name"
	KubernetesContainerNameLabel = "io.kubernetes.container.name"
	MarathonAppIDEnv             = "MARATHON_APP_ID"
)

// NodeSummaryGroup is a topology-typed group of children for a Node.
type NodeSummaryGroup struct {
	ID         string        `json:"id"`
	Label      string        `json:"label"`
	Nodes      []NodeSummary `json:"nodes"`
	TopologyID string        `json:"topologyId"`
	Columns    []Column      `json:"columns"`
}

// Column provides special json serialization for column ids, so they include
// their label for the frontend.
type Column struct {
	ID          string `json:"id"`
	Label       string `json:"label"`
	DefaultSort bool   `json:"defaultSort"`
	Datatype    string `json:"dataType"`
}

// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
	ID         string               `json:"id"`
	Label      string               `json:"label"`
	LabelMinor string               `json:"labelMinor"`
	Rank       string               `json:"rank"`
	Shape      string               `json:"shape,omitempty"`
	Stack      bool                 `json:"stack,omitempty"`
	Linkable   bool                 `json:"linkable,omitempty"` // Whether this node can be linked-to
	Pseudo     bool                 `json:"pseudo,omitempty"`
	Metadata   []report.MetadataRow `json:"metadata,omitempty"`
	Parents    []Parent             `json:"parents,omitempty"`
	Metrics    []report.MetricRow   `json:"metrics,omitempty"`
	Tables     []report.Table       `json:"tables,omitempty"`
	Adjacency  report.IDList        `json:"adjacency,omitempty"`
}

var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
	render.Pseudo:         pseudoNodeSummary,
	report.Process:        processNodeSummary,
	report.Container:      containerNodeSummary,
	report.ContainerImage: containerImageNodeSummary,
	report.Pod:            podNodeSummary,
	report.Service:        podGroupNodeSummary,
	report.Deployment:     podGroupNodeSummary,
	report.DaemonSet:      podGroupNodeSummary,
	report.ECSTask:        ecsTaskNodeSummary,
	report.ECSService:     ecsServiceNodeSummary,
	report.SwarmService:   swarmServiceNodeSummary,
	report.Host:           hostNodeSummary,
	report.Overlay:        weaveNodeSummary,
	report.Endpoint:       nil, // Do not render
}

var templates = map[string]struct{ Label, LabelMinor string }{
	render.TheInternetID:      {render.InboundMajor, ""},
	render.IncomingInternetID: {render.InboundMajor, render.InboundMinor},
	render.OutgoingInternetID: {render.OutboundMajor, render.OutboundMinor},
}

// For each report.Topology, map to a 'primary' API topology. This can then be used in a variety of places.
var primaryAPITopology = map[string]string{
	report.Process:        "processes",
	report.Container:      "containers",
	report.ContainerImage: "containers-by-image",
	report.Pod:            "pods",
	report.Deployment:     "kube-controllers",
	report.DaemonSet:      "kube-controllers",
	report.Service:        "services",
	report.ECSTask:        "ecs-tasks",
	report.ECSService:     "ecs-services",
	report.SwarmService:   "swarm-services",
	report.Host:           "hosts",
}

// MakeNodeSummary summarizes a node, if possible.
func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) {
	if renderer, ok := renderers[n.Topology]; ok {
		// Skip (and don't fall through to fallback) if renderer maps to nil
		if renderer != nil {
			return renderer(baseNodeSummary(r, n), n)
		}
	} else if _, ok := r.Topology(n.Topology); ok {
		summary := baseNodeSummary(r, n)
		summary.Label = n.ID // This is unlikely to look very good, but is a reasonable fallback
		return summary, true
	}
	if strings.HasPrefix(n.Topology, "group:") {
		return groupNodeSummary(baseNodeSummary(r, n), r, n)
	}
	return NodeSummary{}, false
}

// SummarizeMetrics returns a copy of the NodeSummary where the metrics are
// replaced with their summaries
func (n NodeSummary) SummarizeMetrics() NodeSummary {
	summarizedMetrics := make([]report.MetricRow, len(n.Metrics))
	for i, m := range n.Metrics {
		summarizedMetrics[i] = m.Summary()
	}
	n.Metrics = summarizedMetrics
	return n
}

func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
	t, _ := r.Topology(n.Topology)
	return NodeSummary{
		ID:        n.ID,
		Shape:     t.GetShape(),
		Linkable:  true,
		Metadata:  NodeMetadata(r, n),
		Metrics:   NodeMetrics(r, n),
		Parents:   Parents(r, n),
		Tables:    NodeTables(r, n),
		Adjacency: n.Adjacency,
	}
}

func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Pseudo = true
	base.Rank = n.ID

	// try rendering as an internet node
	if template, ok := templates[n.ID]; ok {
		base.Label = template.Label
		base.LabelMinor = template.LabelMinor
		base.Shape = report.Cloud
		return base, true
	}

	// try rendering as a known service node
	if strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix) {
		base.Label = n.ID[len(render.ServiceNodeIDPrefix):]
		base.LabelMinor = ""
		base.Shape = report.Cloud
		return base, true
	}

	// try rendering it as an uncontained node
	if strings.HasPrefix(n.ID, render.UncontainedIDPrefix) {
		base.Label = render.UncontainedMajor
		base.LabelMinor = report.ExtractHostID(n)
		base.Shape = report.Square
		base.Stack = true
		return base, true
	}

	// try rendering it as an unmanaged node
	if strings.HasPrefix(n.ID, render.UnmanagedIDPrefix) {
		base.Label = render.UnmanagedMajor
		base.Shape = report.Square
		base.Stack = true
		base.LabelMinor = report.ExtractHostID(n)
		return base, true
	}

	// try rendering it as an endpoint
	if _, addr, _, ok := report.ParseEndpointNodeID(n.ID); ok {
		base.Label = addr
		base.Shape = report.Circle
		return base, true
	}

	return NodeSummary{}, false
}

func processNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(process.Name)
	base.Rank, _ = n.Latest.Lookup(process.Name)

	pid, ok := n.Latest.Lookup(process.PID)
	if !ok {
		return NodeSummary{}, false
	}
	if containerName, ok := n.Latest.Lookup(docker.ContainerName); ok {
		base.LabelMinor = fmt.Sprintf("%s (%s:%s)", report.ExtractHostID(n), containerName, pid)
	} else {
		base.LabelMinor = fmt.Sprintf("%s (%s)", report.ExtractHostID(n), pid)
	}

	_, isConnected := n.Latest.Lookup(render.IsConnected)
	base.Linkable = isConnected
	return base, true
}

func containerNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label = getRenderableContainerName(n)
	base.LabelMinor = report.ExtractHostID(n)

	if imageName, ok := n.Latest.Lookup(docker.ImageName); ok {
		base.Rank = docker.ImageNameWithoutVersion(imageName)
	}

	return base, true
}

func containerImageNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	imageName, ok := n.Latest.Lookup(docker.ImageName)
	if !ok {
		return NodeSummary{}, false
	}

	imageNameWithoutVersion := docker.ImageNameWithoutVersion(imageName)
	base.Label = imageNameWithoutVersion
	base.Rank = imageNameWithoutVersion
	base.Stack = true

	if base.Label == ImageNameNone {
		base.Label, _ = n.Latest.Lookup(docker.ImageID)
		if len(base.Label) > 12 {
			base.Label = base.Label[:12]
		}
	}

	base.LabelMinor = pluralize(n.Counters, report.Container, "container", "containers")

	return base, true
}

func addKubernetesLabelAndRank(base NodeSummary, n report.Node) NodeSummary {
	base.Label, _ = n.Latest.Lookup(kubernetes.Name)
	namespace, _ := n.Latest.Lookup(kubernetes.Namespace)
	base.Rank = namespace + "/" + base.Label
	return base
}

func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base = addKubernetesLabelAndRank(base, n)
	base.LabelMinor = pluralize(n.Counters, report.Container, "container", "containers")

	return base, true
}

var podGroupNodeTypeName = map[string]string{
	report.Deployment: "Deployment",
	report.DaemonSet:  "Daemon Set",
}

func podGroupNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base = addKubernetesLabelAndRank(base, n)
	base.Stack = true

	// NB: pods are the highest aggregation level for which we display
	// counts.
	count := pluralize(n.Counters, report.Pod, "pod", "pods")
	if typeName, ok := podGroupNodeTypeName[n.Topology]; ok {
		base.LabelMinor = fmt.Sprintf("%s of %s", typeName, count)
	} else {
		base.LabelMinor = count
	}

	return base, true
}

func ecsTaskNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(awsecs.TaskFamily)
	return base, true
}

func ecsServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	_, base.Label, _ = report.ParseECSServiceNodeID(n.ID)
	base.Stack = true
	return base, true
}

func swarmServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(docker.ServiceName)
	return base, true
}

func hostNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	var (
		hostname, _ = n.Latest.Lookup(host.HostName)
		parts       = strings.SplitN(hostname, ".", 2)
	)

	if len(parts) == 2 {
		base.Label, base.LabelMinor, base.Rank = parts[0], parts[1], parts[1]
	} else {
		base.Label = hostname
	}

	return base, true
}

func weaveNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	var (
		nickname, _ = n.Latest.Lookup(overlay.WeavePeerNickName)
	)

	_, peerName := report.ParseOverlayNodeID(n.ID)

	base.Label, base.LabelMinor = nickname, peerName

	return base, true
}

// groupNodeSummary renders the summary for a group node. n.Topology is
// expected to be of the form: group:container:hostname
func groupNodeSummary(base NodeSummary, r report.Report, n report.Node) (NodeSummary, bool) {
	parts := strings.Split(n.Topology, ":")
	if len(parts) != 3 {
		return NodeSummary{}, false
	}

	label, ok := n.Latest.Lookup(parts[2])
	if !ok {
		return NodeSummary{}, false
	}
	base.Label, base.Rank = label, label

	t, ok := r.Topology(parts[1])
	if ok && t.Label != "" {
		base.LabelMinor = pluralize(n.Counters, parts[1], t.Label, t.LabelPlural)
	}

	base.Shape = t.GetShape()
	base.Stack = true
	return base, true
}

func pluralize(counters report.Counters, key, singular, plural string) string {
	if c, ok := counters.Lookup(key); ok {
		if c == 1 {
			return fmt.Sprintf("%d %s", c, singular)
		}
		return fmt.Sprintf("%d %s", c, plural)
	}
	return ""
}

type nodeSummariesByID []NodeSummary

func (s nodeSummariesByID) Len() int           { return len(s) }
func (s nodeSummariesByID) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }

// NodeSummaries is a set of NodeSummaries indexed by ID.
type NodeSummaries map[string]NodeSummary

// Summaries converts RenderableNodes into a set of NodeSummaries
func Summaries(r report.Report, rns report.Nodes) NodeSummaries {

	result := NodeSummaries{}
	for id, node := range rns {
		if summary, ok := MakeNodeSummary(r, node); ok {
			for i, m := range summary.Metrics {
				summary.Metrics[i] = m.Summary()
			}
			result[id] = summary
		}
	}
	return result
}

// getRenderableContainerName obtains a user-friendly container name, to render in the UI
func getRenderableContainerName(nmd report.Node) string {
	for _, key := range []string{
		// Amazon's ecs-agent produces huge Docker container names, destructively
		// derived from mangling Container Definition names in Task
		// Definitions.
		//
		// However, the ecs-agent provides a label containing the original Container
		// Definition name.
		docker.LabelPrefix + AmazonECSContainerNameLabel,
		// Kubernetes also mangles its Docker container names and provides a
		// label with the original container name. However, note that this label
		// is only provided by Kubernetes versions >= 1.2 (see
		// https://github.com/kubernetes/kubernetes/pull/17234/ )
		docker.LabelPrefix + KubernetesContainerNameLabel,
		// Marathon doesn't set any Docker labels and this is the only meaningful
		// attribute we can find to make Scope useful without Mesos plugin
		docker.EnvPrefix + MarathonAppIDEnv,
		docker.ContainerName,
		docker.ContainerHostname,
	} {
		if label, ok := nmd.Latest.Lookup(key); ok {
			return label
		}
	}
	return ""
}
<commit_msg>render: In minor labels, display '0 things' instead of blank if zero things present<commit_after>package detailed

import (
	"fmt"
	"strings"

	"github.com/weaveworks/scope/probe/awsecs"
	"github.com/weaveworks/scope/probe/docker"
	"github.com/weaveworks/scope/probe/host"
	"github.com/weaveworks/scope/probe/kubernetes"
	"github.com/weaveworks/scope/probe/overlay"
	"github.com/weaveworks/scope/probe/process"
	"github.com/weaveworks/scope/render"
	"github.com/weaveworks/scope/report"
)

// Shapes that are allowed
const (
	ImageNameNone = "<none>"

	// Keys we use to render container names
	AmazonECSContainerNameLabel  = "com.amazonaws.ecs.container-name"
	KubernetesContainerNameLabel = "io.kubernetes.container.name"
	MarathonAppIDEnv             = "MARATHON_APP_ID"
)

// NodeSummaryGroup is a topology-typed group of children for a Node.
type NodeSummaryGroup struct {
	ID         string        `json:"id"`
	Label      string        `json:"label"`
	Nodes      []NodeSummary `json:"nodes"`
	TopologyID string        `json:"topologyId"`
	Columns    []Column      `json:"columns"`
}

// Column provides special json serialization for column ids, so they include
// their label for the frontend.
type Column struct {
	ID          string `json:"id"`
	Label       string `json:"label"`
	DefaultSort bool   `json:"defaultSort"`
	Datatype    string `json:"dataType"`
}

// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
	ID         string               `json:"id"`
	Label      string               `json:"label"`
	LabelMinor string               `json:"labelMinor"`
	Rank       string               `json:"rank"`
	Shape      string               `json:"shape,omitempty"`
	Stack      bool                 `json:"stack,omitempty"`
	Linkable   bool                 `json:"linkable,omitempty"` // Whether this node can be linked-to
	Pseudo     bool                 `json:"pseudo,omitempty"`
	Metadata   []report.MetadataRow `json:"metadata,omitempty"`
	Parents    []Parent             `json:"parents,omitempty"`
	Metrics    []report.MetricRow   `json:"metrics,omitempty"`
	Tables     []report.Table       `json:"tables,omitempty"`
	Adjacency  report.IDList        `json:"adjacency,omitempty"`
}

var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
	render.Pseudo:         pseudoNodeSummary,
	report.Process:        processNodeSummary,
	report.Container:      containerNodeSummary,
	report.ContainerImage: containerImageNodeSummary,
	report.Pod:            podNodeSummary,
	report.Service:        podGroupNodeSummary,
	report.Deployment:     podGroupNodeSummary,
	report.DaemonSet:      podGroupNodeSummary,
	report.ECSTask:        ecsTaskNodeSummary,
	report.ECSService:     ecsServiceNodeSummary,
	report.SwarmService:   swarmServiceNodeSummary,
	report.Host:           hostNodeSummary,
	report.Overlay:        weaveNodeSummary,
	report.Endpoint:       nil, // Do not render
}

var templates = map[string]struct{ Label, LabelMinor string }{
	render.TheInternetID:      {render.InboundMajor, ""},
	render.IncomingInternetID: {render.InboundMajor, render.InboundMinor},
	render.OutgoingInternetID: {render.OutboundMajor, render.OutboundMinor},
}

// For each report.Topology, map to a 'primary' API topology. This can then be used in a variety of places.
var primaryAPITopology = map[string]string{
	report.Process:        "processes",
	report.Container:      "containers",
	report.ContainerImage: "containers-by-image",
	report.Pod:            "pods",
	report.Deployment:     "kube-controllers",
	report.DaemonSet:      "kube-controllers",
	report.Service:        "services",
	report.ECSTask:        "ecs-tasks",
	report.ECSService:     "ecs-services",
	report.SwarmService:   "swarm-services",
	report.Host:           "hosts",
}

// MakeNodeSummary summarizes a node, if possible.
func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) {
	if renderer, ok := renderers[n.Topology]; ok {
		// Skip (and don't fall through to fallback) if renderer maps to nil
		if renderer != nil {
			return renderer(baseNodeSummary(r, n), n)
		}
	} else if _, ok := r.Topology(n.Topology); ok {
		summary := baseNodeSummary(r, n)
		summary.Label = n.ID // This is unlikely to look very good, but is a reasonable fallback
		return summary, true
	}
	if strings.HasPrefix(n.Topology, "group:") {
		return groupNodeSummary(baseNodeSummary(r, n), r, n)
	}
	return NodeSummary{}, false
}

// SummarizeMetrics returns a copy of the NodeSummary where the metrics are
// replaced with their summaries
func (n NodeSummary) SummarizeMetrics() NodeSummary {
	summarizedMetrics := make([]report.MetricRow, len(n.Metrics))
	for i, m := range n.Metrics {
		summarizedMetrics[i] = m.Summary()
	}
	n.Metrics = summarizedMetrics
	return n
}

func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
	t, _ := r.Topology(n.Topology)
	return NodeSummary{
		ID:        n.ID,
		Shape:     t.GetShape(),
		Linkable:  true,
		Metadata:  NodeMetadata(r, n),
		Metrics:   NodeMetrics(r, n),
		Parents:   Parents(r, n),
		Tables:    NodeTables(r, n),
		Adjacency: n.Adjacency,
	}
}

func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Pseudo = true
	base.Rank = n.ID

	// try rendering as an internet node
	if template, ok := templates[n.ID]; ok {
		base.Label = template.Label
		base.LabelMinor = template.LabelMinor
		base.Shape = report.Cloud
		return base, true
	}

	// try rendering as a known service node
	if strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix) {
		base.Label = n.ID[len(render.ServiceNodeIDPrefix):]
		base.LabelMinor = ""
		base.Shape = report.Cloud
		return base, true
	}

	// try rendering it as an uncontained node
	if strings.HasPrefix(n.ID, render.UncontainedIDPrefix) {
		base.Label = render.UncontainedMajor
		base.LabelMinor = report.ExtractHostID(n)
		base.Shape = report.Square
		base.Stack = true
		return base, true
	}

	// try rendering it as an unmanaged node
	if strings.HasPrefix(n.ID, render.UnmanagedIDPrefix) {
		base.Label = render.UnmanagedMajor
		base.Shape = report.Square
		base.Stack = true
		base.LabelMinor = report.ExtractHostID(n)
		return base, true
	}

	// try rendering it as an endpoint
	if _, addr, _, ok := report.ParseEndpointNodeID(n.ID); ok {
		base.Label = addr
		base.Shape = report.Circle
		return base, true
	}

	return NodeSummary{}, false
}

func processNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(process.Name)
	base.Rank, _ = n.Latest.Lookup(process.Name)

	pid, ok := n.Latest.Lookup(process.PID)
	if !ok {
		return NodeSummary{}, false
	}
	if containerName, ok := n.Latest.Lookup(docker.ContainerName); ok {
		base.LabelMinor = fmt.Sprintf("%s (%s:%s)", report.ExtractHostID(n), containerName, pid)
	} else {
		base.LabelMinor = fmt.Sprintf("%s (%s)", report.ExtractHostID(n), pid)
	}

	_, isConnected := n.Latest.Lookup(render.IsConnected)
	base.Linkable = isConnected
	return base, true
}

func containerNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label = getRenderableContainerName(n)
	base.LabelMinor = report.ExtractHostID(n)

	if imageName, ok := n.Latest.Lookup(docker.ImageName); ok {
		base.Rank = docker.ImageNameWithoutVersion(imageName)
	}

	return base, true
}

func containerImageNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	imageName, ok := n.Latest.Lookup(docker.ImageName)
	if !ok {
		return NodeSummary{}, false
	}

	imageNameWithoutVersion := docker.ImageNameWithoutVersion(imageName)
	base.Label = imageNameWithoutVersion
	base.Rank = imageNameWithoutVersion
	base.Stack = true

	if base.Label == ImageNameNone {
		base.Label, _ = n.Latest.Lookup(docker.ImageID)
		if len(base.Label) > 12 {
			base.Label = base.Label[:12]
		}
	}

	base.LabelMinor = pluralize(n.Counters, report.Container, "container", "containers")

	return base, true
}

func addKubernetesLabelAndRank(base NodeSummary, n report.Node) NodeSummary {
	base.Label, _ = n.Latest.Lookup(kubernetes.Name)
	namespace, _ := n.Latest.Lookup(kubernetes.Namespace)
	base.Rank = namespace + "/" + base.Label
	return base
}

func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base = addKubernetesLabelAndRank(base, n)
	base.LabelMinor = pluralize(n.Counters, report.Container, "container", "containers")

	return base, true
}

var podGroupNodeTypeName = map[string]string{
	report.Deployment: "Deployment",
	report.DaemonSet:  "Daemon Set",
}

func podGroupNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base = addKubernetesLabelAndRank(base, n)
	base.Stack = true

	// NB: pods are the highest aggregation level for which we display
	// counts.
	count := pluralize(n.Counters, report.Pod, "pod", "pods")
	if typeName, ok := podGroupNodeTypeName[n.Topology]; ok {
		base.LabelMinor = fmt.Sprintf("%s of %s", typeName, count)
	} else {
		base.LabelMinor = count
	}

	return base, true
}

func ecsTaskNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(awsecs.TaskFamily)
	return base, true
}

func ecsServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	_, base.Label, _ = report.ParseECSServiceNodeID(n.ID)
	base.Stack = true
	return base, true
}

func swarmServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	base.Label, _ = n.Latest.Lookup(docker.ServiceName)
	return base, true
}

func hostNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	var (
		hostname, _ = n.Latest.Lookup(host.HostName)
		parts       = strings.SplitN(hostname, ".", 2)
	)

	if len(parts) == 2 {
		base.Label, base.LabelMinor, base.Rank = parts[0], parts[1], parts[1]
	} else {
		base.Label = hostname
	}

	return base, true
}

func weaveNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
	var (
		nickname, _ = n.Latest.Lookup(overlay.WeavePeerNickName)
	)

	_, peerName := report.ParseOverlayNodeID(n.ID)

	base.Label, base.LabelMinor = nickname, peerName

	return base, true
}

// groupNodeSummary renders the summary for a group node. n.Topology is
// expected to be of the form: group:container:hostname
func groupNodeSummary(base NodeSummary, r report.Report, n report.Node) (NodeSummary, bool) {
	parts := strings.Split(n.Topology, ":")
	if len(parts) != 3 {
		return NodeSummary{}, false
	}

	label, ok := n.Latest.Lookup(parts[2])
	if !ok {
		return NodeSummary{}, false
	}
	base.Label, base.Rank = label, label

	t, ok := r.Topology(parts[1])
	if ok && t.Label != "" {
		base.LabelMinor = pluralize(n.Counters, parts[1], t.Label, t.LabelPlural)
	}

	base.Shape = t.GetShape()
	base.Stack = true
	return base, true
}

func pluralize(counters report.Counters, key, singular, plural string) string {
	c, ok := counters.Lookup(key)
	if !ok {
		c = 0
	}
	if c == 1 {
		return fmt.Sprintf("%d %s", c, singular)
	}
	return fmt.Sprintf("%d %s", c, plural)
}

type nodeSummariesByID []NodeSummary

func (s nodeSummariesByID) Len() int           { return len(s) }
func (s nodeSummariesByID) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }

// NodeSummaries is a set of NodeSummaries indexed by ID.
type NodeSummaries map[string]NodeSummary

// Summaries converts RenderableNodes into a set of NodeSummaries
func Summaries(r report.Report, rns report.Nodes) NodeSummaries {

	result := NodeSummaries{}
	for id, node := range rns {
		if summary, ok := MakeNodeSummary(r, node); ok {
			for i, m := range summary.Metrics {
				summary.Metrics[i] = m.Summary()
			}
			result[id] = summary
		}
	}
	return result
}

// getRenderableContainerName obtains a user-friendly container name, to render in the UI
func getRenderableContainerName(nmd report.Node) string {
	for _, key := range []string{
		// Amazon's ecs-agent produces huge Docker container names, destructively
		// derived from mangling Container Definition names in Task
		// Definitions.
		//
		// However, the ecs-agent provides a label containing the original Container
		// Definition name.
		docker.LabelPrefix + AmazonECSContainerNameLabel,
		// Kubernetes also mangles its Docker container names and provides a
		// label with the original container name. However, note that this label
		// is only provided by Kubernetes versions >= 1.2 (see
		// https://github.com/kubernetes/kubernetes/pull/17234/ )
		docker.LabelPrefix + KubernetesContainerNameLabel,
		// Marathon doesn't set any Docker labels and this is the only meaningful
		// attribute we can find to make Scope useful without Mesos plugin
		docker.EnvPrefix + MarathonAppIDEnv,
		docker.ContainerName,
		docker.ContainerHostname,
	} {
		if label, ok := nmd.Latest.Lookup(key); ok {
			return label
		}
	}
	return ""
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package discovery

import (
	"fmt"
	"sync"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/golang/glog"
)

// APIGroupResources is an API group with a mapping of versions to
// resources.
type APIGroupResources struct {
	Group metav1.APIGroup
	// A mapping of version string to a slice of APIResources for
	// that version.
	VersionedResources map[string][]metav1.APIResource
}

// NewRESTMapper returns a PriorityRESTMapper based on the discovered
// groups and resources passed in.
func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.VersionInterfacesFunc) meta.RESTMapper {
	unionMapper := meta.MultiRESTMapper{}

	var groupPriority []string
	// /v1 is special.  It should always come first
	resourcePriority := []schema.GroupVersionResource{{Group: "", Version: "v1", Resource: meta.AnyResource}}
	kindPriority := []schema.GroupVersionKind{{Group: "", Version: "v1", Kind: meta.AnyKind}}

	for _, group := range groupResources {
		groupPriority = append(groupPriority, group.Group.Name)

		// Make sure the preferred version comes first
		if len(group.Group.PreferredVersion.Version) != 0 {
			preferred := group.Group.PreferredVersion.Version
			if _, ok := group.VersionedResources[preferred]; ok {
				resourcePriority = append(resourcePriority, schema.GroupVersionResource{
					Group:    group.Group.Name,
					Version:  group.Group.PreferredVersion.Version,
					Resource: meta.AnyResource,
				})

				kindPriority = append(kindPriority, schema.GroupVersionKind{
					Group:   group.Group.Name,
					Version: group.Group.PreferredVersion.Version,
					Kind:    meta.AnyKind,
				})
			}
		}

		for _, discoveryVersion := range group.Group.Versions {
			resources, ok := group.VersionedResources[discoveryVersion.Version]
			if !ok {
				continue
			}

			// Add non-preferred versions after the preferred version, in case there are resources that only exist in those versions
			if discoveryVersion.Version != group.Group.PreferredVersion.Version {
				resourcePriority = append(resourcePriority, schema.GroupVersionResource{
					Group:    group.Group.Name,
					Version:  discoveryVersion.Version,
					Resource: meta.AnyResource,
				})

				kindPriority = append(kindPriority, schema.GroupVersionKind{
					Group:   group.Group.Name,
					Version: discoveryVersion.Version,
					Kind:    meta.AnyKind,
				})
			}

			gv := schema.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version}
			versionMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gv}, versionInterfaces)

			for _, resource := range resources {
				scope := meta.RESTScopeNamespace
				if !resource.Namespaced {
					scope = meta.RESTScopeRoot
				}
				versionMapper.Add(gv.WithKind(resource.Kind), scope)
				// TODO only do this if it supports listing
				versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope)
			}
			// TODO why is this type not in discovery (at least for "v1")
			versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot)
			unionMapper = append(unionMapper, versionMapper)
		}
	}

	for _, group := range groupPriority {
		resourcePriority = append(resourcePriority, schema.GroupVersionResource{
			Group:    group,
			Version:  meta.AnyVersion,
			Resource: meta.AnyResource,
		})
		kindPriority = append(kindPriority, schema.GroupVersionKind{
			Group:   group,
			Version: meta.AnyVersion,
			Kind:    meta.AnyKind,
		})
	}

	return meta.PriorityRESTMapper{
		Delegate:         unionMapper,
		ResourcePriority: resourcePriority,
		KindPriority:     kindPriority,
	}
}

// GetAPIGroupResources uses the provided discovery client to gather
// discovery information and populate a slice of APIGroupResources.
func GetAPIGroupResources(cl DiscoveryInterface) ([]*APIGroupResources, error) {
	apiGroups, err := cl.ServerGroups()
	if err != nil {
		return nil, err
	}
	var result []*APIGroupResources
	for _, group := range apiGroups.Groups {
		groupResources := &APIGroupResources{
			Group:              group,
			VersionedResources: make(map[string][]metav1.APIResource),
		}
		for _, version := range group.Versions {
			resources, err := cl.ServerResourcesForGroupVersion(version.GroupVersion)
			if err != nil {
				// continue as best we can
				// TODO track the errors and update callers to handle partial errors.
				continue
			}
			groupResources.VersionedResources[version.Version] = resources.APIResources
		}
		result = append(result, groupResources)
	}
	return result, nil
}

// DeferredDiscoveryRESTMapper is a RESTMapper that will defer
// initialization of the RESTMapper until the first mapping is
// requested.
type DeferredDiscoveryRESTMapper struct {
	initMu           sync.Mutex
	delegate         meta.RESTMapper
	cl               CachedDiscoveryInterface
	versionInterface meta.VersionInterfacesFunc
}

// NewDeferredDiscoveryRESTMapper returns a
// DeferredDiscoveryRESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
func NewDeferredDiscoveryRESTMapper(cl CachedDiscoveryInterface, versionInterface meta.VersionInterfacesFunc) *DeferredDiscoveryRESTMapper {
	return &DeferredDiscoveryRESTMapper{
		cl:               cl,
		versionInterface: versionInterface,
	}
}

func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
	d.initMu.Lock()
	defer d.initMu.Unlock()

	if d.delegate != nil {
		return d.delegate, nil
	}

	groupResources, err := GetAPIGroupResources(d.cl)
	if err != nil {
		return nil, err
	}

	d.delegate = NewRESTMapper(groupResources, d.versionInterface)
	return d.delegate, err
}

// Reset resets the internally cached Discovery information and will
// cause the next mapping request to re-discover.
func (d *DeferredDiscoveryRESTMapper) Reset() {
	glog.V(5).Info("Invalidating discovery information")

	d.initMu.Lock()
	defer d.initMu.Unlock()

	d.cl.Invalidate()
	d.delegate = nil
}

// KindFor takes a partial resource and returns back the single match.
// It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return schema.GroupVersionKind{}, err
	}
	gvk, err = del.KindFor(resource)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvk, err = d.KindFor(resource)
	}
	return
}

// KindsFor takes a partial resource and returns back the list of
// potential kinds in priority order.
func (d *DeferredDiscoveryRESTMapper) KindsFor(resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	gvks, err = del.KindsFor(resource)
	if len(gvks) == 0 && !d.cl.Fresh() {
		d.Reset()
		gvks, err = d.KindsFor(resource)
	}
	return
}

// ResourceFor takes a partial resource and returns back the single
// match. It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) ResourceFor(input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return schema.GroupVersionResource{}, err
	}
	gvr, err = del.ResourceFor(input)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvr, err = d.ResourceFor(input)
	}
	return
}

// ResourcesFor takes a partial resource and returns back the list of
// potential resource in priority order.
func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	gvrs, err = del.ResourcesFor(input)
	if len(gvrs) == 0 && !d.cl.Fresh() {
		d.Reset()
		gvrs, err = d.ResourcesFor(input)
	}
	return
}

// RESTMapping identifies a preferred resource mapping for the
// provided group kind.
func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	m, err = del.RESTMapping(gk, versions...)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		m, err = d.RESTMapping(gk, versions...)
	}
	return
}

// RESTMappings returns the RESTMappings for the provided group kind
// in a rough internal preferred order. If no kind is found, it will
// return a NoResourceMatchError.
func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	ms, err = del.RESTMappings(gk, versions...)
	if len(ms) == 0 && !d.cl.Fresh() {
		d.Reset()
		ms, err = d.RESTMappings(gk, versions...)
	}
	return
}

// ResourceSingularizer converts a resource name from plural to
// singular (e.g., from pods to pod).
func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return resource, err
	}
	singular, err = del.ResourceSingularizer(resource)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		singular, err = d.ResourceSingularizer(resource)
	}
	return
}

func (d *DeferredDiscoveryRESTMapper) String() string {
	del, err := d.getDelegate()
	if err != nil {
		return fmt.Sprintf("DeferredDiscoveryRESTMapper{%v}", err)
	}
	return fmt.Sprintf("DeferredDiscoveryRESTMapper{\n\t%v\n}", del)
}

// Make sure it satisfies the interface
var _ meta.RESTMapper = &DeferredDiscoveryRESTMapper{}
<commit_msg>use specified discovery information if possible<commit_after>/*
Copyright 2016 The Kubernetes Authors.

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

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

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

package discovery

import (
	"fmt"
	"sync"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/golang/glog"
)

// APIGroupResources is an API group with a mapping of versions to
// resources.
type APIGroupResources struct {
	Group metav1.APIGroup
	// A mapping of version string to a slice of APIResources for
	// that version.
	VersionedResources map[string][]metav1.APIResource
}

// NewRESTMapper returns a PriorityRESTMapper based on the discovered
// groups and resources passed in.
func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.VersionInterfacesFunc) meta.RESTMapper {
	unionMapper := meta.MultiRESTMapper{}

	var groupPriority []string
	// /v1 is special.  It should always come first
	resourcePriority := []schema.GroupVersionResource{{Group: "", Version: "v1", Resource: meta.AnyResource}}
	kindPriority := []schema.GroupVersionKind{{Group: "", Version: "v1", Kind: meta.AnyKind}}

	for _, group := range groupResources {
		groupPriority = append(groupPriority, group.Group.Name)

		// Make sure the preferred version comes first
		if len(group.Group.PreferredVersion.Version) != 0 {
			preferred := group.Group.PreferredVersion.Version
			if _, ok := group.VersionedResources[preferred]; ok {
				resourcePriority = append(resourcePriority, schema.GroupVersionResource{
					Group:    group.Group.Name,
					Version:  group.Group.PreferredVersion.Version,
					Resource: meta.AnyResource,
				})

				kindPriority = append(kindPriority, schema.GroupVersionKind{
					Group:   group.Group.Name,
					Version: group.Group.PreferredVersion.Version,
					Kind:    meta.AnyKind,
				})
			}
		}

		for _, discoveryVersion := range group.Group.Versions {
			resources, ok := group.VersionedResources[discoveryVersion.Version]
			if !ok {
				continue
			}

			// Add non-preferred versions after the preferred version, in case there are resources that only exist in those versions
			if discoveryVersion.Version != group.Group.PreferredVersion.Version {
				resourcePriority = append(resourcePriority, schema.GroupVersionResource{
					Group:    group.Group.Name,
					Version:  discoveryVersion.Version,
					Resource: meta.AnyResource,
				})

				kindPriority = append(kindPriority, schema.GroupVersionKind{
					Group:   group.Group.Name,
					Version: discoveryVersion.Version,
					Kind:    meta.AnyKind,
				})
			}

			gv := schema.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version}
			versionMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gv}, versionInterfaces)

			for _, resource := range resources {
				scope := meta.RESTScopeNamespace
				if !resource.Namespaced {
					scope = meta.RESTScopeRoot
				}

				// this is for legacy resources and servers which don't list singular forms.  For those we must still guess.
				if len(resource.SingularName) == 0 {
					versionMapper.Add(gv.WithKind(resource.Kind), scope)
					// TODO this is producing unsafe guesses that don't actually work, but it matches previous behavior
					versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope)
					continue
				}

				plural := gv.WithResource(resource.Name)
				singular := gv.WithResource(resource.SingularName)
				versionMapper.AddSpecific(gv.WithKind(resource.Kind), plural, singular, scope)
				// TODO this is producing unsafe guesses that don't actually work, but it matches previous behavior
				versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope)
			}
			// TODO why is this type not in discovery (at least for "v1")
			versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot)
			unionMapper = append(unionMapper, versionMapper)
		}
	}

	for _, group := range groupPriority {
		resourcePriority = append(resourcePriority, schema.GroupVersionResource{
			Group:    group,
			Version:  meta.AnyVersion,
			Resource: meta.AnyResource,
		})
		kindPriority = append(kindPriority, schema.GroupVersionKind{
			Group:   group,
			Version: meta.AnyVersion,
			Kind:    meta.AnyKind,
		})
	}

	return meta.PriorityRESTMapper{
		Delegate:         unionMapper,
		ResourcePriority: resourcePriority,
		KindPriority:     kindPriority,
	}
}

// GetAPIGroupResources uses the provided discovery client to gather
// discovery information and populate a slice of APIGroupResources.
func GetAPIGroupResources(cl DiscoveryInterface) ([]*APIGroupResources, error) {
	apiGroups, err := cl.ServerGroups()
	if err != nil {
		return nil, err
	}
	var result []*APIGroupResources
	for _, group := range apiGroups.Groups {
		groupResources := &APIGroupResources{
			Group:              group,
			VersionedResources: make(map[string][]metav1.APIResource),
		}
		for _, version := range group.Versions {
			resources, err := cl.ServerResourcesForGroupVersion(version.GroupVersion)
			if err != nil {
				// continue as best we can
				// TODO track the errors and update callers to handle partial errors.
				continue
			}
			groupResources.VersionedResources[version.Version] = resources.APIResources
		}
		result = append(result, groupResources)
	}
	return result, nil
}

// DeferredDiscoveryRESTMapper is a RESTMapper that will defer
// initialization of the RESTMapper until the first mapping is
// requested.
type DeferredDiscoveryRESTMapper struct {
	initMu           sync.Mutex
	delegate         meta.RESTMapper
	cl               CachedDiscoveryInterface
	versionInterface meta.VersionInterfacesFunc
}

// NewDeferredDiscoveryRESTMapper returns a
// DeferredDiscoveryRESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
func NewDeferredDiscoveryRESTMapper(cl CachedDiscoveryInterface, versionInterface meta.VersionInterfacesFunc) *DeferredDiscoveryRESTMapper {
	return &DeferredDiscoveryRESTMapper{
		cl:               cl,
		versionInterface: versionInterface,
	}
}

func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
	d.initMu.Lock()
	defer d.initMu.Unlock()

	if d.delegate != nil {
		return d.delegate, nil
	}

	groupResources, err := GetAPIGroupResources(d.cl)
	if err != nil {
		return nil, err
	}

	d.delegate = NewRESTMapper(groupResources, d.versionInterface)
	return d.delegate, err
}

// Reset resets the internally cached Discovery information and will
// cause the next mapping request to re-discover.
func (d *DeferredDiscoveryRESTMapper) Reset() {
	glog.V(5).Info("Invalidating discovery information")

	d.initMu.Lock()
	defer d.initMu.Unlock()

	d.cl.Invalidate()
	d.delegate = nil
}

// KindFor takes a partial resource and returns back the single match.
// It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return schema.GroupVersionKind{}, err
	}
	gvk, err = del.KindFor(resource)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvk, err = d.KindFor(resource)
	}
	return
}

// KindsFor takes a partial resource and returns back the list of
// potential kinds in priority order.
func (d *DeferredDiscoveryRESTMapper) KindsFor(resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	gvks, err = del.KindsFor(resource)
	if len(gvks) == 0 && !d.cl.Fresh() {
		d.Reset()
		gvks, err = d.KindsFor(resource)
	}
	return
}

// ResourceFor takes a partial resource and returns back the single
// match. It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) ResourceFor(input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return schema.GroupVersionResource{}, err
	}
	gvr, err = del.ResourceFor(input)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvr, err = d.ResourceFor(input)
	}
	return
}

// ResourcesFor takes a partial resource and returns back the list of
// potential resource in priority order.
func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	gvrs, err = del.ResourcesFor(input)
	if len(gvrs) == 0 && !d.cl.Fresh() {
		d.Reset()
		gvrs, err = d.ResourcesFor(input)
	}
	return
}

// RESTMapping identifies a preferred resource mapping for the
// provided group kind.
func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	m, err = del.RESTMapping(gk, versions...)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		m, err = d.RESTMapping(gk, versions...)
	}
	return
}

// RESTMappings returns the RESTMappings for the provided group kind
// in a rough internal preferred order. If no kind is found, it will
// return a NoResourceMatchError.
func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	ms, err = del.RESTMappings(gk, versions...)
	if len(ms) == 0 && !d.cl.Fresh() {
		d.Reset()
		ms, err = d.RESTMappings(gk, versions...)
	}
	return
}

// ResourceSingularizer converts a resource name from plural to
// singular (e.g., from pods to pod).
func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return resource, err
	}
	singular, err = del.ResourceSingularizer(resource)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		singular, err = d.ResourceSingularizer(resource)
	}
	return
}

func (d *DeferredDiscoveryRESTMapper) String() string {
	del, err := d.getDelegate()
	if err != nil {
		return fmt.Sprintf("DeferredDiscoveryRESTMapper{%v}", err)
	}
	return fmt.Sprintf("DeferredDiscoveryRESTMapper{\n\t%v\n}", del)
}

// Make sure it satisfies the interface
var _ meta.RESTMapper = &DeferredDiscoveryRESTMapper{}
<|endoftext|>


<commit_before>package fly

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"github.com/EngineerBetter/concourse-up/config"
	"github.com/EngineerBetter/concourse-up/util"
)

// DarwinBinaryURL is a compile-time variable set with -ldflags
var DarwinBinaryURL = "COMPILE_TIME_VARIABLE_fly_darwin_binary_url"

// LinuxBinaryURL is a compile-time variable set with -ldflags
var LinuxBinaryURL = "COMPILE_TIME_VARIABLE_fly_linux_binary_url"

// WindowsBinaryURL is a compile-time variable set with -ldflags
var WindowsBinaryURL = "COMPILE_TIME_VARIABLE_fly_windows_binary_url"

// IClient represents an interface for a client
type IClient interface {
	CanConnect() (bool, error)
	SetDefaultPipeline(deployArgs *config.DeployArgs, config *config.Config) error
	Cleanup() error
}

// Client represents a low-level wrapper for fly
type Client struct {
	tempDir *util.TempDir
	creds   Credentials
	stdout  io.Writer
	stderr  io.Writer
}

// Credentials represents credentials needed to connect to concourse
type Credentials struct {
	Target   string
	API      string
	Username string
	Password string
	CACert   string
}

// New returns a new fly client
func New(creds Credentials, stdout, stderr io.Writer) (IClient, error) {
	tempDir, err := util.NewTempDir()
	if err != nil {
		return nil, err
	}

	fileHandler, err := os.Create(tempDir.Path("fly"))
	if err != nil {
		return nil, err
	}
	defer fileHandler.Close()

	url, err := getFlyURL()
	if err != nil {
		return nil, err
	}

	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if _, err := io.Copy(fileHandler, resp.Body); err != nil {
		return nil, err
	}

	if err := fileHandler.Sync(); err != nil {
		return nil, err
	}

	if err := os.Chmod(fileHandler.Name(), 0700); err != nil {
		return nil, err
	}

	return &Client{
		tempDir,
		creds,
		stdout,
		stderr,
	}, nil
}

// CanConnect returns true if it can connect to the concourse
func (client *Client) CanConnect() (bool, error) {
	cmd := exec.Command(
		client.tempDir.Path("fly"),
		"--target",
		client.creds.Target,
		"login",
		"--insecure",
		"--concourse-url",
		client.creds.API,
		"--username",
		client.creds.Username,
		"--password",
		client.creds.Password,
	)

	stderr := bytes.NewBuffer(nil)
	cmd.Stdout = client.stdout
	cmd.Stderr = stderr

	runErr := cmd.Run()
	if runErr == nil {
		return true, nil
	}

	stderrBytes, err := ioutil.ReadAll(stderr)
	if err != nil {
		return false, err
	}

	if strings.Contains(string(stderrBytes), "could not reach the Concourse server") {
		return false, nil
	}

	// if there is a legitimate error, copy it to stderr for debugging
	if _, err := client.stderr.Write(stderrBytes); err != nil {
		return false, err
	}

	return false, runErr
}

// SetDefaultPipeline sets the default pipeline against a given concourse
func (client *Client) SetDefaultPipeline(deployArgs *config.DeployArgs, config *config.Config) error {
	if err := client.login(); err != nil {
		return err
	}

	pipelinePath := client.tempDir.Path("default-pipeline.yml")
	pipelineName := "concourse-up-self-update"

	if err := client.writePipelineConfig(pipelinePath, deployArgs, config); err != nil {
		return err
	}

	if err := client.run("set-pipeline", "--pipeline", pipelineName, "--config", pipelinePath, "--non-interactive"); err != nil {
		return err
	}

	if err := os.Remove(pipelinePath); err != nil {
		return err
	}

	if !deployArgs.PauseSelfUpdate {
		if err := client.run("unpause-pipeline", "--pipeline", pipelineName); err != nil {
			return err
		}
	}

	return nil
}

func (client *Client) writePipelineConfig(pipelinePath string, deployArgs *config.DeployArgs, config *config.Config) error {
	fileHandler, err := os.Create(pipelinePath)
	if err != nil {
		return err
	}
	defer fileHandler.Close()

	params, err := client.buildDefaultPipelineParams(deployArgs, config)
	if err != nil {
		return err
	}

	pipelineConfig, err := util.RenderTemplate(defaultPipelineTemplate, params)
	if err != nil {
		return err
	}

	if _, err := fileHandler.Write(pipelineConfig); err != nil {
		return err
	}

	if err := fileHandler.Sync(); err != nil {
		return err
	}

	return nil
}

// Cleanup removes tempfiles
func (client *Client) Cleanup() error {
	return client.tempDir.Cleanup()
}

func (client *Client) login() error {
	attempts := 100

	client.stdout.Write([]byte("Waiting for Concourse ATC to start... \n"))

	for i := 0; i < attempts; i++ {
		canConnect, err := client.CanConnect()
		if err != nil {
			return err
		}
		if canConnect {
			return nil
		}

		time.Sleep(time.Second)
	}

	return fmt.Errorf("failed to log in to %s after %d attempts", client.creds.API, attempts)
}

func (client *Client) run(args ...string) error {
	args = append([]string{"--target", client.creds.Target}, args...)
	cmd := exec.Command(client.tempDir.Path("fly"), args...)
	cmd.Stdout = client.stdout
	cmd.Stderr = client.stderr
	return cmd.Run()
}

func getFlyURL() (string, error) {
	os := runtime.GOOS
	if os == "darwin" {
		return DarwinBinaryURL, nil
	} else if os == "linux" {
		return LinuxBinaryURL, nil
	} else if os == "windows" {
		return WindowsBinaryURL, nil
	}
	return "", fmt.Errorf("unknown os: `%s`", os)
}

func (client *Client) buildDefaultPipelineParams(deployArgs *config.DeployArgs, config *config.Config) (*defaultPipelineParams, error) {
	awsAccessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
	if awsAccessKeyID == "" {
		return nil, errors.New("env var AWS_ACCESS_KEY_ID not found")
	}

	awsSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
	if awsSecretAccessKey == "" {
		return nil, errors.New("env var AWS_SECRET_ACCESS_KEY not found")
	}

	return &defaultPipelineParams{
		FlagAWSRegion:      deployArgs.AWSRegion,
		FlagDomain:         deployArgs.Domain,
		FlagTLSCert:        deployArgs.TLSCert,
		FlagTLSKey:         deployArgs.TLSKey,
		FlagWorkers:        deployArgs.WorkerCount,
		FlagWorkerSize:     deployArgs.WorkerSize,
		Deployment:         strings.TrimPrefix(config.Deployment, "concourse-up-"),
		AWSAccessKeyID:     awsAccessKeyID,
		AWSSecretAccessKey: awsSecretAccessKey,
	}, nil
}

type defaultPipelineParams struct {
	FlagAWSRegion      string
	FlagDomain         string
	FlagTLSCert        string
	FlagTLSKey         string
	FlagWorkers        int
	FlagWorkerSize     string
	Deployment         string
	AWSAccessKeyID     string
	AWSSecretAccessKey string
	AWSDefaultRegion   string
}

// Indent is a helper function to indent the field a given number of spaces
func (params defaultPipelineParams) Indent(countStr, field string) string {
	return util.Indent(countStr, field)
}

const defaultPipelineTemplate = `
---
resources:
- name: concourse-up-release
  type: github-release
  source:
    user: engineerbetter
    repository: concourse-up
    pre_release: true

jobs:
- name: self-update
  plan:
  - get: concourse-up-release
    trigger: true
  - task: update
    params:
      AWS_REGION: "<% .FlagAWSRegion %>"
      DOMAIN: "<% .FlagDomain %>"
      TLS_CERT: |-
        <% .Indent "8" .FlagTLSCert %>
      TLS_KEY: |-
        <% .Indent "8" .FlagTLSKey %>
      WORKERS: "<% .FlagWorkers %>"
      WORKER_SIZE: "<% .FlagWorkerSize %>"
      DEPLOYMENT: "<% .Deployment %>"
      AWS_ACCESS_KEY_ID: "<% .AWSAccessKeyID %>"
      AWS_SECRET_ACCESS_KEY: "<% .AWSSecretAccessKey %>"
      SELF_UPDATE: true
    config:
      platform: linux
      image_resource:
        type: docker-image
        source:
          repository: engineerbetter/cup-image
      inputs:
      - name: concourse-up-release
      run:
        path: bash
        args:
        - -c
        - |
          set -eux

          cd concourse-up-release
          chmod +x concourse-up-linux-amd64
          ./concourse-up-linux-amd64 deploy $DEPLOYMENT
`
<commit_msg>ensure that self update pipeline cant happen in parallel<commit_after>package fly

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"github.com/EngineerBetter/concourse-up/config"
	"github.com/EngineerBetter/concourse-up/util"
)

// DarwinBinaryURL is a compile-time variable set with -ldflags
var DarwinBinaryURL = "COMPILE_TIME_VARIABLE_fly_darwin_binary_url"

// LinuxBinaryURL is a compile-time variable set with -ldflags
var LinuxBinaryURL = "COMPILE_TIME_VARIABLE_fly_linux_binary_url"

// WindowsBinaryURL is a compile-time variable set with -ldflags
var WindowsBinaryURL = "COMPILE_TIME_VARIABLE_fly_windows_binary_url"

// IClient represents an interface for a client
type IClient interface {
	CanConnect() (bool, error)
	SetDefaultPipeline(deployArgs *config.DeployArgs, config *config.Config) error
	Cleanup() error
}

// Client represents a low-level wrapper for fly
type Client struct {
	tempDir *util.TempDir
	creds   Credentials
	stdout  io.Writer
	stderr  io.Writer
}

// Credentials represents credentials needed to connect to concourse
type Credentials struct {
	Target   string
	API      string
	Username string
	Password string
	CACert   string
}

// New returns a new fly client
func New(creds Credentials, stdout, stderr io.Writer) (IClient, error) {
	tempDir, err := util.NewTempDir()
	if err != nil {
		return nil, err
	}

	fileHandler, err := os.Create(tempDir.Path("fly"))
	if err != nil {
		return nil, err
	}
	defer fileHandler.Close()

	url, err := getFlyURL()
	if err != nil {
		return nil, err
	}

	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if _, err := io.Copy(fileHandler, resp.Body); err != nil {
		return nil, err
	}

	if err := fileHandler.Sync(); err != nil {
		return nil, err
	}

	if err := os.Chmod(fileHandler.Name(), 0700); err != nil {
		return nil, err
	}

	return &Client{
		tempDir,
		creds,
		stdout,
		stderr,
	}, nil
}

// CanConnect returns true if it can connect to the concourse
func (client *Client) CanConnect() (bool, error) {
	cmd := exec.Command(
		client.tempDir.Path("fly"),
		"--target",
		client.creds.Target,
		"login",
		"--insecure",
		"--concourse-url",
		client.creds.API,
		"--username",
		client.creds.Username,
		"--password",
		client.creds.Password,
	)

	stderr := bytes.NewBuffer(nil)
	cmd.Stdout = client.stdout
	cmd.Stderr = stderr

	runErr := cmd.Run()
	if runErr == nil {
		return true, nil
	}

	stderrBytes, err := ioutil.ReadAll(stderr)
	if err != nil {
		return false, err
	}

	if strings.Contains(string(stderrBytes), "could not reach the Concourse server") {
		return false, nil
	}

	// if there is a legitimate error, copy it to stderr for debugging
	if _, err := client.stderr.Write(stderrBytes); err != nil {
		return false, err
	}

	return false, runErr
}

// SetDefaultPipeline sets the default pipeline against a given concourse
func (client *Client) SetDefaultPipeline(deployArgs *config.DeployArgs, config *config.Config) error {
	if err := client.login(); err != nil {
		return err
	}

	pipelinePath := client.tempDir.Path("default-pipeline.yml")
	pipelineName := "concourse-up-self-update"

	if err := client.writePipelineConfig(pipelinePath, deployArgs, config); err != nil {
		return err
	}

	if err := client.run("set-pipeline", "--pipeline", pipelineName, "--config", pipelinePath, "--non-interactive"); err != nil {
		return err
	}

	if err := os.Remove(pipelinePath); err != nil {
		return err
	}

	if !deployArgs.PauseSelfUpdate {
		if err := client.run("unpause-pipeline", "--pipeline", pipelineName); err != nil {
			return err
		}
	}

	return nil
}

func (client *Client) writePipelineConfig(pipelinePath string, deployArgs *config.DeployArgs, config *config.Config) error {
	fileHandler, err := os.Create(pipelinePath)
	if err != nil {
		return err
	}
	defer fileHandler.Close()

	params, err := client.buildDefaultPipelineParams(deployArgs, config)
	if err != nil {
		return err
	}

	pipelineConfig, err := util.RenderTemplate(defaultPipelineTemplate, params)
	if err != nil {
		return err
	}

	if _, err := fileHandler.Write(pipelineConfig); err != nil {
		return err
	}

	if err := fileHandler.Sync(); err != nil {
		return err
	}

	return nil
}

// Cleanup removes tempfiles
func (client *Client) Cleanup() error {
	return client.tempDir.Cleanup()
}

func (client *Client) login() error {
	attempts := 100

	client.stdout.Write([]byte("Waiting for Concourse ATC to start... \n"))

	for i := 0; i < attempts; i++ {
		canConnect, err := client.CanConnect()
		if err != nil {
			return err
		}
		if canConnect {
			return nil
		}

		time.Sleep(time.Second)
	}

	return fmt.Errorf("failed to log in to %s after %d attempts", client.creds.API, attempts)
}

func (client *Client) run(args ...string) error {
	args = append([]string{"--target", client.creds.Target}, args...)
	cmd := exec.Command(client.tempDir.Path("fly"), args...)
	cmd.Stdout = client.stdout
	cmd.Stderr = client.stderr
	return cmd.Run()
}

func getFlyURL() (string, error) {
	os := runtime.GOOS
	if os == "darwin" {
		return DarwinBinaryURL, nil
	} else if os == "linux" {
		return LinuxBinaryURL, nil
	} else if os == "windows" {
		return WindowsBinaryURL, nil
	}
	return "", fmt.Errorf("unknown os: `%s`", os)
}

func (client *Client) buildDefaultPipelineParams(deployArgs *config.DeployArgs, config *config.Config) (*defaultPipelineParams, error) {
	awsAccessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
	if awsAccessKeyID == "" {
		return nil, errors.New("env var AWS_ACCESS_KEY_ID not found")
	}

	awsSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
	if awsSecretAccessKey == "" {
		return nil, errors.New("env var AWS_SECRET_ACCESS_KEY not found")
	}

	return &defaultPipelineParams{
		FlagAWSRegion:      deployArgs.AWSRegion,
		FlagDomain:         deployArgs.Domain,
		FlagTLSCert:        deployArgs.TLSCert,
		FlagTLSKey:         deployArgs.TLSKey,
		FlagWorkers:        deployArgs.WorkerCount,
		FlagWorkerSize:     deployArgs.WorkerSize,
		Deployment:         strings.TrimPrefix(config.Deployment, "concourse-up-"),
		AWSAccessKeyID:     awsAccessKeyID,
		AWSSecretAccessKey: awsSecretAccessKey,
	}, nil
}

type defaultPipelineParams struct {
	FlagAWSRegion      string
	FlagDomain         string
	FlagTLSCert        string
	FlagTLSKey         string
	FlagWorkers        int
	FlagWorkerSize     string
	Deployment         string
	AWSAccessKeyID     string
	AWSSecretAccessKey string
	AWSDefaultRegion   string
}

// Indent is a helper function to indent the field a given number of spaces
func (params defaultPipelineParams) Indent(countStr, field string) string {
	return util.Indent(countStr, field)
}

const defaultPipelineTemplate = `
---
resources:
- name: concourse-up-release
  type: github-release
  source:
    user: engineerbetter
    repository: concourse-up
    pre_release: true

jobs:
- name: self-update
  serial: true
  plan:
  - get: concourse-up-release
    trigger: true
  - task: update
    params:
      AWS_REGION: "<% .FlagAWSRegion %>"
      DOMAIN: "<% .FlagDomain %>"
      TLS_CERT: |-
        <% .Indent "8" .FlagTLSCert %>
      TLS_KEY: |-
        <% .Indent "8" .FlagTLSKey %>
      WORKERS: "<% .FlagWorkers %>"
      WORKER_SIZE: "<% .FlagWorkerSize %>"
      DEPLOYMENT: "<% .Deployment %>"
      AWS_ACCESS_KEY_ID: "<% .AWSAccessKeyID %>"
      AWS_SECRET_ACCESS_KEY: "<% .AWSSecretAccessKey %>"
      SELF_UPDATE: true
    config:
      platform: linux
      image_resource:
        type: docker-image
        source:
          repository: engineerbetter/cup-image
      inputs:
      - name: concourse-up-release
      run:
        path: bash
        args:
        - -c
        - |
          set -eux

          cd concourse-up-release
          chmod +x concourse-up-linux-amd64
          ./concourse-up-linux-amd64 deploy $DEPLOYMENT
`
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"

	"github.com/justsocialapps/holmes/analytics"
	"github.com/justsocialapps/holmes/assets"
	"github.com/justsocialapps/holmes/publisher"
	"github.com/justsocialapps/holmes/tracker"
	"gopkg.in/Shopify/sarama.v1"
)

const version string = "1.7.0"

//go:generate scripts/prepare_assets.sh
//go:generate go run scripts/include_assets.go

func provideTrackingChannel(trackingChannel chan<- *tracker.TrackingObject, handler func(trackingChannel chan<- *tracker.TrackingObject, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		handler(trackingChannel, w, r)
	}
}

func startServer(host string, port *string) {
	listener, err := net.Listen("tcp", host+":"+*port)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("Holmes running on " + host + ":" + *port)
	log.Println(assets.Bannertxt)

	err = http.Serve(listener, nil)
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	var protocol = flag.String("protocol", "https", "The protocol used to serve Holmes resources ('http' or 'https')")
	var host = flag.String("host", "localhost", "The host name used to reach Holmes")
	var proxyPort = flag.String("proxyPort", "3001", "The TCP port for reaching Holmes if Holmes is operated behind a reverse proxy.")
	var proxyPath = flag.String("proxyPath", "", "The base path for reaching Holmes if Holmes is operated behind a reverse proxy.")
	var listenPort = flag.String("listenPort", "3001", "The TCP port that Holmes listens on")
	var kafkaHost = flag.String("kafkaHost", "localhost:9092", "The Kafka host to consume messages from")
	var logfileName = flag.String("logfile", "", "The file to log messages to")
	var printVersion = flag.Bool("version", false, "Print Holmes version and exit")
	flag.Parse()

	if *printVersion {
		fmt.Println(version)
		return
	}

	var logFile *os.File
	if *logfileName == "" {
		logFile = os.Stdout
	} else {
		var err error
		logFile, err = os.OpenFile(*logfileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
		if err != nil {
			log.Fatal(err)
		}
	}
	log.SetOutput(logFile)
	sarama.Logger = log.New(logFile, "[Sarama] ", log.LstdFlags)

	baseURL := *protocol + "://" + *host
	if !(*protocol == "https" && *proxyPort == "443") && !(*protocol == "http" && *proxyPort == "80") {
		baseURL = baseURL + ":" + *proxyPort
	}
	baseURL = baseURL + *proxyPath

	trackingChannel := make(chan *tracker.TrackingObject, 10)
	http.HandleFunc("/track", provideTrackingChannel(trackingChannel, tracker.Track))
	http.HandleFunc("/analytics.js", analytics.Analytics(baseURL))
	go publisher.Publish(trackingChannel, kafkaHost, "tracking")

	startServer("localhost", listenPort)
}
<commit_msg>Bumped version number to 1.8.0-dev<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"

	"github.com/justsocialapps/holmes/analytics"
	"github.com/justsocialapps/holmes/assets"
	"github.com/justsocialapps/holmes/publisher"
	"github.com/justsocialapps/holmes/tracker"
	"gopkg.in/Shopify/sarama.v1"
)

const version string = "1.8.0-dev"

//go:generate scripts/prepare_assets.sh
//go:generate go run scripts/include_assets.go

func provideTrackingChannel(trackingChannel chan<- *tracker.TrackingObject, handler func(trackingChannel chan<- *tracker.TrackingObject, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		handler(trackingChannel, w, r)
	}
}

func startServer(host string, port *string) {
	listener, err := net.Listen("tcp", host+":"+*port)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("Holmes running on " + host + ":" + *port)
	log.Println(assets.Bannertxt)

	err = http.Serve(listener, nil)
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	var protocol = flag.String("protocol", "https", "The protocol used to serve Holmes resources ('http' or 'https')")
	var host = flag.String("host", "localhost", "The host name used to reach Holmes")
	var proxyPort = flag.String("proxyPort", "3001", "The TCP port for reaching Holmes if Holmes is operated behind a reverse proxy.")
	var proxyPath = flag.String("proxyPath", "", "The base path for reaching Holmes if Holmes is operated behind a reverse proxy.")
	var listenPort = flag.String("listenPort", "3001", "The TCP port that Holmes listens on")
	var kafkaHost = flag.String("kafkaHost", "localhost:9092", "The Kafka host to consume messages from")
	var logfileName = flag.String("logfile", "", "The file to log messages to")
	var printVersion = flag.Bool("version", false, "Print Holmes version and exit")
	flag.Parse()

	if *printVersion {
		fmt.Println(version)
		return
	}

	var logFile *os.File
	if *logfileName == "" {
		logFile = os.Stdout
	} else {
		var err error
		logFile, err = os.OpenFile(*logfileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
		if err != nil {
			log.Fatal(err)
		}
	}
	log.SetOutput(logFile)
	sarama.Logger = log.New(logFile, "[Sarama] ", log.LstdFlags)

	baseURL := *protocol + "://" + *host
	if !(*protocol == "https" && *proxyPort == "443") && !(*protocol == "http" && *proxyPort == "80") {
		baseURL = baseURL + ":" + *proxyPort
	}
	baseURL = baseURL + *proxyPath

	trackingChannel := make(chan *tracker.TrackingObject, 10)
	http.HandleFunc("/track", provideTrackingChannel(trackingChannel, tracker.Track))
	http.HandleFunc("/analytics.js", analytics.Analytics(baseURL))
	go publisher.Publish(trackingChannel, kafkaHost, "tracking")

	startServer("localhost", listenPort)
}
<|endoftext|>


<commit_before>package mantle

import (
	"github.com/garyburd/redigo/redis"
	"github.com/youtube/vitess/go/pools"
	"time"
)

const (
	PoolSize    = 10
	DefaultHost = "localhost"
	DefaultPort = "6379"
)

type Redis struct {
	Settings PoolSettings
	pool     *ResourcePool
}

func (r *Redis) SetDefaults() {
	if r.Settings.Host == "" {
		r.Settings.Host = DefaultHost
	}
	if r.Settings.Port == "" {
		r.Settings.Port = DefaultPort
	}
	if r.Settings.Capacity == 0 {
		r.Settings.Capacity = PoolSize
	}
	if r.Settings.MaxCapacity == 0 {
		r.Settings.MaxCapacity = PoolSize
	}
	r.Settings.Timeout = time.Minute
	r.pool = NewPool(Connect, r.Settings)
}

//Alias to SetDefaults
func (r *Redis) Configure() {
	r.SetDefaults()
}

//Wrapping redis connection
type RedisConn struct {
	redis.Conn
}

//Close a redis connection
func (r *RedisConn) Close() {
	_ = r.Conn.Close()
}

func (r *Redis) GetClient() (*RedisConn, error) {
	connection, err := r.pool.GetConn()
	if err != nil {
		return nil, err
	}
	return connection.(*RedisConn), nil
}

func (r *Redis) PutClient(c *RedisConn) {
	r.pool.PutConn(c)
}

//This method creates a redis connection
func Connect(host string, port string) (pools.Resource, error) {
	cli, err := redis.Dial("tcp", host+":"+port)
	if err != nil {
		panic(err)
	}
	return &RedisConn{cli}, nil
}

//Generic method to execute any redis call
func (r *Redis) Execute(cmd string, args ...interface{}) (interface{}, error) {
	client, err := r.GetClient()
	if err != nil {
		return nil, err
	}
	defer r.PutClient(client)
	return client.Do(cmd, args...)
}

func (r *Redis) Delete(keys ...interface{}) int {
	value, err := redis.Int(r.Execute("DEL", keys...))
	if err != nil {
		return -1
	}
	return value
}

func (r *Redis) Get(key string) string {
	value, err := redis.String(r.Execute("GET", key))
	if err != nil {
		return ""
	}
	return value
}

func (r *Redis) Set(key string, value interface{}) bool {
	_, err := r.Execute("SET", key, value)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) MGet(keys ...interface{}) []string {
	values, err := redis.Strings(r.Execute("MGET", keys...))
	if err != nil {
		return []string{}
	}
	return values
}

func (r *Redis) MSet(mapOfKeyVal map[string]interface{}) bool {
	_, err := r.Execute("MSET", redis.Args{}.AddFlat(mapOfKeyVal)...)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) Expire(key string, duration int) bool {
	_, err := r.Execute("EXPIRE", key, duration)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) Setex(key string, duration int, val interface{}) bool {
	_, err := r.Execute("SETEX", key, duration, val)
	if err != nil {
		return false
	}
	return true
}
<commit_msg>using the new pool<commit_after>package mantle

import (
	"github.com/garyburd/redigo/redis"
	"github.com/youtube/vitess/go/pools"
	"strings"
	"time"
)

//cant make these guys const as []string is not allowed in consts
var PoolSize = 10
var DefaultIpAndHost = []string{"localhost:6379"}

type Redis struct {
	Settings PoolSettings
	pool     *ResourcePool
}

func (r *Redis) SetDefaults() {
	if len(r.Settings.IpAndHosts) == 0 {
		r.Settings.IpAndHosts = DefaultIpAndHost
	}
	if r.Settings.Capacity == 0 {
		r.Settings.Capacity = PoolSize
	}
	if r.Settings.MaxCapacity == 0 {
		r.Settings.MaxCapacity = PoolSize
	}
	r.Settings.Timeout = time.Minute
	r.pool = NewPool(Connect, r.Settings)
}

//Alias to SetDefaults
func (r *Redis) Configure() {
	r.SetDefaults()
}

//Wrapping redis connection
type RedisConn struct {
	redis.Conn
}

//Close a redis connection
func (r *RedisConn) Close() {
	_ = r.Conn.Close()
}

func (r *Redis) GetClient() (*RedisConn, error) {
	connection, err := r.pool.GetConn()
	if err != nil {
		return nil, err
	}
	return connection.(*RedisConn), nil
}

func (r *Redis) PutClient(c *RedisConn) {
	r.pool.PutConn(c)
}

//This method creates a redis connection
func Connect(IpAndHost []string) (pools.Resource, error) {
	if len(IpAndHost) > 1 {
		panic("we can only connect to 1 server at the moment")
	}
	hostNPort := strings.Split(IpAndHost[0], ":")
	cli, err := redis.Dial("tcp", hostNPort[0]+":"+hostNPort[1])
	if err != nil {
		panic(err)
	}
	return &RedisConn{cli}, nil
}

//Generic method to execute any redis call
func (r *Redis) Execute(cmd string, args ...interface{}) (interface{}, error) {
	client, err := r.GetClient()
	if err != nil {
		return nil, err
	}
	defer r.PutClient(client)
	return client.Do(cmd, args...)
}

func (r *Redis) Delete(keys ...interface{}) int {
	value, err := redis.Int(r.Execute("DEL", keys...))
	if err != nil {
		return -1
	}
	return value
}

func (r *Redis) Get(key string) string {
	value, err := redis.String(r.Execute("GET", key))
	if err != nil {
		return ""
	}
	return value
}

func (r *Redis) Set(key string, value interface{}) bool {
	_, err := r.Execute("SET", key, value)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) MGet(keys ...interface{}) []string {
	values, err := redis.Strings(r.Execute("MGET", keys...))
	if err != nil {
		return []string{}
	}
	return values
}

func (r *Redis) MSet(mapOfKeyVal map[string]interface{}) bool {
	_, err := r.Execute("MSET", redis.Args{}.AddFlat(mapOfKeyVal)...)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) Expire(key string, duration int) bool {
	_, err := r.Execute("EXPIRE", key, duration)
	if err != nil {
		return false
	}
	return true
}

func (r *Redis) Setex(key string, duration int, val interface{}) bool {
	_, err := r.Execute("SETEX", key, duration, val)
	if err != nil {
		return false
	}
	return true
}
<|endoftext|>


<commit_before>package onedrive

import "time"

// The HashesFacet groups different types of hashes into a single structure, for
// an item on OneDrive.
// See: http://onedrive.github.io/facets/hashes_facet.htm
type HashesFacet struct {
	Sha1Hash  string `json:"sha1Hash"`
	Crc32Hash string `json:"crc32Hash"`
}

func newHashesFacet(sha1, crc string) *HashesFacet {
	return &HashesFacet{sha1, crc}
}

// The FileFacet groups file-related data on OneDrive into a single structure.
// It is available on the file property of Item resources that represent files.
// See: http://onedrive.github.io/facets/file_facet.htm
type FileFacet struct {
	MimeType string       `json:"mimeType"`
	Hashes   *HashesFacet `json:"hashes"`
}

func newFileFacet(mime string, hashes *HashesFacet) *FileFacet {
	return &FileFacet{mime, hashes}
}

// The FolderFacet groups folder-related data on OneDrive into a single
// structure. It is available on the folder property of Item resources that
// represent folders.
// See: http://onedrive.github.io/facets/folder_facet.htm
type FolderFacet struct {
	ChildCount int64 `json:"childCount"`
}

func newFolderFacet(children int64) *FolderFacet {
	return &FolderFacet{children}
}

// The ImageFacet groups image-related data on OneDrive into a single structure.
// See: http://onedrive.github.io/facets/image_facet.htm
type ImageFacet struct {
	Width  int `json:"width"`
	Height int `json:"height"`
}

func newImageFacet(height, width int) *ImageFacet {
	return &ImageFacet{width, height}
}

// The PhotoFacet groups photo-related data on OneDrive, for example, EXIF
// metadata, into a single structure.
// See: http://onedrive.github.io/facets/photo_facet.htm
type PhotoFacet struct {
	TakenDateTime       time.Time `json:"takenDateTime"`
	CameraMake          string    `json:"cameraMake"`
	CameraModel         string    `json:"cameraModel"`
	FNumber             float64   `json:"fNumber"`
	ExposureDenominator float64   `json:"exposureDenominator"`
	ExposureNumerator   float64   `json:"exposureNumerator"`
	FocalLength         float64   `json:"focalLength"`
	ISO                 int       `json:"iso"`
}

func newPhotoFacet(timeTaken time.Time, cameraMake, cameraModel string, fNumber, exposureDenominator, exposureNumerator, focalLength float64, iso int) *PhotoFacet {
	return &PhotoFacet{timeTaken, cameraMake, cameraModel, fNumber, exposureDenominator, exposureNumerator, focalLength, iso}
}

// The AudioFacet groups audio-related data on OneDrive into a single structure.
// It is available on the audio property of Item resources that have associated audio.
// See: http://onedrive.github.io/facets/audio_facet.htm
type AudioFacet struct {
	Album             string `json:"album"`
	AlbumArtist       string `json:"albumArtist"`
	Artist            string `json:"artist"`
	Bitrate           int64  `json:"bitrate"`
	Composers         string `json:"composers"`
	Copyright         string `json:"copyright"`
	Disc              int    `json:"disc"`
	DiscCount         int    `json:"discCount"`
	Duration          int64  `json:"duration"`
	Genre             string `json:"genre"`
	HasDRM            bool   `json:"hasDrm"`
	IsVariableBitrate bool   `json:"isVariableBitrate"`
	Title             string `json:"title"`
	Track             int    `json:"track"`
	TrackCount        int    `json:"trackCount"`
	Year              int    `json:"year"`
}

func newAudioFacet(album, albumArtist, artist string, bitrate int64, composers, copyright string, disc, discCount int, duration int64, genre string, drm, variableBitrate bool, title string, track, trackCount, year int) *AudioFacet {
	return &AudioFacet{album, albumArtist, artist, bitrate, composers, copyright, disc, discCount, duration, genre, drm, variableBitrate, title, track, trackCount, year}
}

// The VideoFacet groups video-related data on OneDrive into a single complex type.
// See: http://onedrive.github.io/facets/video_facet.htm
type VideoFacet struct {
	Bitrate  int64 `json:"bitrate"`
	Duration int64 `json:"duration"`
	Height   int64 `json:"height"`
	Width    int64 `json:"width"`
}

func newVideoFacet(bitrate, duration, height, width int64) *VideoFacet {
	return &VideoFacet{bitrate, duration, height, width}
}

// The LocationFacet groups geographic location-related data on OneDrive into a
// single structure.
// See: http://onedrive.github.io/facets/location_facet.htm
type LocationFacet struct {
	Altitude  float64 `json:"altitude"`
	Latitude  float64 `json:"latitude"`
	Longitude float64 `json:"longitude"`
}

func newLocationFacet(alt, lat, long float64) *LocationFacet {
	return &LocationFacet{alt, lat, long}
}

// The DeletedFacet indicates that the item on OneDrive has been deleted. In
// this version of the API, the presence (non-null) of the facet value indicates
// that the file was deleted. A null (or missing) value indicates that the file
//is not deleted.
// See: http://onedrive.github.io/facets/deleted_facet.htm
type DeletedFacet struct{}

// The SpecialFolder facet provides information about how a folder on OneDrive
// can be accessed via the special folders collection.
// See: http://onedrive.github.io/facets/jumpinfo_facet.htm
type SpecialFolder struct {
	Name string `json:"name"`
}

// The SharingLink type groups sharing link-related data on OneDrive into a
// single structure.
// See: http://onedrive.github.io/facets/sharinglink_facet.htm
type SharingLink struct {
	Token       string    `json:"token"`
	WebURL      string    `json:"webUrl"`
	Type        string    `json:"type"`
	Application *Identity `json:"application"`
}
<commit_msg>Omit FileFacet properties if they're empty<commit_after>package onedrive

import "time"

// The HashesFacet groups different types of hashes into a single structure, for
// an item on OneDrive.
// See: http://onedrive.github.io/facets/hashes_facet.htm
type HashesFacet struct {
	Sha1Hash  string `json:"sha1Hash"`
	Crc32Hash string `json:"crc32Hash"`
}

func newHashesFacet(sha1, crc string) *HashesFacet {
	return &HashesFacet{sha1, crc}
}

// The FileFacet groups file-related data on OneDrive into a single structure.
// It is available on the file property of Item resources that represent files.
// See: http://onedrive.github.io/facets/file_facet.htm
type FileFacet struct {
	MimeType *string      `json:"mimeType,omitempty"`
	Hashes   *HashesFacet `json:"hashes,omitempty"`
}

func newFileFacet(mime string, hashes *HashesFacet) *FileFacet {
	return &FileFacet{&mime, hashes}
}

// The FolderFacet groups folder-related data on OneDrive into a single
// structure. It is available on the folder property of Item resources that
// represent folders.
// See: http://onedrive.github.io/facets/folder_facet.htm
type FolderFacet struct {
	ChildCount int64 `json:"childCount"`
}

func newFolderFacet(children int64) *FolderFacet {
	return &FolderFacet{children}
}

// The ImageFacet groups image-related data on OneDrive into a single structure.
// See: http://onedrive.github.io/facets/image_facet.htm
type ImageFacet struct {
	Width  int `json:"width"`
	Height int `json:"height"`
}

func newImageFacet(height, width int) *ImageFacet {
	return &ImageFacet{width, height}
}

// The PhotoFacet groups photo-related data on OneDrive, for example, EXIF
// metadata, into a single structure.
// See: http://onedrive.github.io/facets/photo_facet.htm
type PhotoFacet struct {
	TakenDateTime       time.Time `json:"takenDateTime"`
	CameraMake          string    `json:"cameraMake"`
	CameraModel         string    `json:"cameraModel"`
	FNumber             float64   `json:"fNumber"`
	ExposureDenominator float64   `json:"exposureDenominator"`
	ExposureNumerator   float64   `json:"exposureNumerator"`
	FocalLength         float64   `json:"focalLength"`
	ISO                 int       `json:"iso"`
}

func newPhotoFacet(timeTaken time.Time, cameraMake, cameraModel string, fNumber, exposureDenominator, exposureNumerator, focalLength float64, iso int) *PhotoFacet {
	return &PhotoFacet{timeTaken, cameraMake, cameraModel, fNumber, exposureDenominator, exposureNumerator, focalLength, iso}
}

// The AudioFacet groups audio-related data on OneDrive into a single structure.
// It is available on the audio property of Item resources that have associated audio.
// See: http://onedrive.github.io/facets/audio_facet.htm
type AudioFacet struct {
	Album             string `json:"album"`
	AlbumArtist       string `json:"albumArtist"`
	Artist            string `json:"artist"`
	Bitrate           int64  `json:"bitrate"`
	Composers         string `json:"composers"`
	Copyright         string `json:"copyright"`
	Disc              int    `json:"disc"`
	DiscCount         int    `json:"discCount"`
	Duration          int64  `json:"duration"`
	Genre             string `json:"genre"`
	HasDRM            bool   `json:"hasDrm"`
	IsVariableBitrate bool   `json:"isVariableBitrate"`
	Title             string `json:"title"`
	Track             int    `json:"track"`
	TrackCount        int    `json:"trackCount"`
	Year              int    `json:"year"`
}

func newAudioFacet(album, albumArtist, artist string, bitrate int64, composers, copyright string, disc, discCount int, duration int64, genre string, drm, variableBitrate bool, title string, track, trackCount, year int) *AudioFacet {
	return &AudioFacet{album, albumArtist, artist, bitrate, composers, copyright, disc, discCount, duration, genre, drm, variableBitrate, title, track, trackCount, year}
}

// The VideoFacet groups video-related data on OneDrive into a single complex type.
// See: http://onedrive.github.io/facets/video_facet.htm
type VideoFacet struct {
	Bitrate  int64 `json:"bitrate"`
	Duration int64 `json:"duration"`
	Height   int64 `json:"height"`
	Width    int64 `json:"width"`
}

func newVideoFacet(bitrate, duration, height, width int64) *VideoFacet {
	return &VideoFacet{bitrate, duration, height, width}
}

// The LocationFacet groups geographic location-related data on OneDrive into a
// single structure.
// See: http://onedrive.github.io/facets/location_facet.htm
type LocationFacet struct {
	Altitude  float64 `json:"altitude"`
	Latitude  float64 `json:"latitude"`
	Longitude float64 `json:"longitude"`
}

func newLocationFacet(alt, lat, long float64) *LocationFacet {
	return &LocationFacet{alt, lat, long}
}

// The DeletedFacet indicates that the item on OneDrive has been deleted. In
// this version of the API, the presence (non-null) of the facet value indicates
// that the file was deleted. A null (or missing) value indicates that the file
//is not deleted.
// See: http://onedrive.github.io/facets/deleted_facet.htm
type DeletedFacet struct{}

// The SpecialFolder facet provides information about how a folder on OneDrive
// can be accessed via the special folders collection.
// See: http://onedrive.github.io/facets/jumpinfo_facet.htm
type SpecialFolder struct {
	Name string `json:"name"`
}

// The SharingLink type groups sharing link-related data on OneDrive into a
// single structure.
// See: http://onedrive.github.io/facets/sharinglink_facet.htm
type SharingLink struct {
	Token       string    `json:"token"`
	WebURL      string    `json:"webUrl"`
	Type        string    `json:"type"`
	Application *Identity `json:"application"`
}
<|endoftext|>


<commit_before>package graval

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"path/filepath"
	"strings"
	"time"
)

type ftpConn struct {
	conn             *net.TCPConn
	controlReader    *bufio.Reader
	controlWriter    *bufio.Writer
	dataConn         ftpDataSocket
	driver           FTPDriver
	logger           *ftpLogger
	serverName       string
	sessionId        string
	namePrefix       string
	reqUser          string
	user             string
	renameFrom       string
	minDataPort      int
	maxDataPort      int
	pasvAdvertisedIp string
}

// NewftpConn constructs a new object that will handle the FTP protocol over
// an active net.TCPConn. The TCP connection should already be open before
// it is handed to this functions. driver is an instance of FTPDriver that
// will handle all auth and persistence details.
func newftpConn(tcpConn *net.TCPConn, driver FTPDriver, serverName string, minPort int, maxPort int, pasvAdvertisedIp string) *ftpConn {
	c := new(ftpConn)
	c.namePrefix = "/"
	c.conn = tcpConn
	c.controlReader = bufio.NewReader(tcpConn)
	c.controlWriter = bufio.NewWriter(tcpConn)
	c.driver = driver
	c.sessionId = newSessionId()
	c.logger = newFtpLogger(c.sessionId)
	c.serverName = serverName
	c.minDataPort = minPort
	c.maxDataPort = maxPort
	c.pasvAdvertisedIp = pasvAdvertisedIp
	return c
}

// returns a random 20 char string that can be used as a unique session ID
func newSessionId() string {
	hash := sha256.New()
	_, err := io.CopyN(hash, rand.Reader, 50)
	if err != nil {
		return "????????????????????"
	}
	md := hash.Sum(nil)
	mdStr := hex.EncodeToString(md)
	return mdStr[0:20]
}

// Serve starts an endless loop that reads FTP commands from the client and
// responds appropriately. terminated is a channel that will receive a true
// message when the connection closes. This loop will be running inside a
// goroutine, so use this channel to be notified when the connection can be
// cleaned up.
func (ftpConn *ftpConn) Serve() {
	defer func() {
		if r := recover(); r != nil {
			ftpConn.logger.Printf("Recovered in ftpConn Serve: %s", r)
		}

		ftpConn.Close()
	}()

	ftpConn.logger.Printf("Connection Established (local: %s, remote: %s)", ftpConn.localIP(), ftpConn.remoteIP())
	// send welcome
	ftpConn.writeMessage(220, ftpConn.serverName)
	// read commands
	for {
		line, err := ftpConn.controlReader.ReadString('\n')
		if err != nil {
			break
		}
		ftpConn.receiveLine(line)
	}
	ftpConn.logger.Print("Connection Terminated")
}

// Close will manually close this connection, even if the client isn't ready.
func (ftpConn *ftpConn) Close() {
	ftpConn.conn.Close()
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
	}
}

// receiveLine accepts a single line FTP command and co-ordinates an
// appropriate response.
func (ftpConn *ftpConn) receiveLine(line string) {
	command, param := ftpConn.parseLine(line)
	ftpConn.logger.PrintCommand(command, param)
	cmdObj := commands[command]
	if cmdObj == nil {
		ftpConn.writeMessage(500, "Command not found")
		return
	}
	if cmdObj.RequireParam() && param == "" {
		ftpConn.writeMessage(553, "action aborted, required param missing")
	} else if cmdObj.RequireAuth() && ftpConn.user == "" {
		ftpConn.writeMessage(530, "not logged in")
	} else {
		cmdObj.Execute(ftpConn, param)
	}
}

func (ftpConn *ftpConn) parseLine(line string) (string, string) {
	params := strings.SplitN(strings.Trim(line, "\r\n"), " ", 2)
	if len(params) == 1 {
		return params[0], ""
	}
	return params[0], strings.TrimSpace(params[1])
}

// writeMessage will send a standard FTP response back to the client.
func (ftpConn *ftpConn) writeMessage(code int, message string) (wrote int, err error) {
	ftpConn.logger.PrintResponse(code, message)
	line := fmt.Sprintf("%d %s\r\n", code, message)
	wrote, err = ftpConn.controlWriter.WriteString(line)
	ftpConn.controlWriter.Flush()
	return
}

// writeLines will send a multiline FTP response back to the client.
func (ftpConn *ftpConn) writeLines(code int, lines ...string) (wrote int, err error) {
	message := strings.Join(lines, "\r\n") + "\r\n"
	ftpConn.logger.PrintResponse(code, message)
	wrote, err = ftpConn.controlWriter.WriteString(message)
	ftpConn.controlWriter.Flush()
	return
}

// buildPath takes a client supplied path or filename and generates a safe
// absolute path within their account sandbox.
//
//    buildpath("/")
//    => "/"
//    buildpath("one.txt")
//    => "/one.txt"
//    buildpath("/files/two.txt")
//    => "/files/two.txt"
//    buildpath("files/two.txt")
//    => "files/two.txt"
//    buildpath("/../../../../etc/passwd")
//    => "/etc/passwd"
//
// The driver implementation is responsible for deciding how to treat this path.
// Obviously they MUST NOT just read the path off disk. The probably want to
// prefix the path with something to scope the users access to a sandbox.
func (ftpConn *ftpConn) buildPath(filename string) (fullPath string) {
	if len(filename) > 0 && filename[0:1] == "/" {
		fullPath = filepath.Clean(filename)
	} else if len(filename) > 0 {
		fullPath = filepath.Clean(ftpConn.namePrefix + "/" + filename)
	} else {
		fullPath = filepath.Clean(ftpConn.namePrefix)
	}
	fullPath = strings.Replace(fullPath, "//", "/", -1)
	return
}

// the server IP that is being used for this connection. May be the same for all connections,
// or may vary if the server is listening on 0.0.0.0
func (ftpConn *ftpConn) localIP() string {
	lAddr := ftpConn.conn.LocalAddr().(*net.TCPAddr)
	return lAddr.IP.String()
}

// the client IP address
func (ftpConn *ftpConn) remoteIP() string {
	rAddr := ftpConn.conn.RemoteAddr().(*net.TCPAddr)
	return rAddr.IP.String()
}

// sendOutofbandData will copy data from reader to the client via the currently
// open data socket. Assumes the socket is open and ready to be used.
func (ftpConn *ftpConn) sendOutofbandReader(reader io.Reader) {
	defer ftpConn.dataConn.Close()

	_, err := io.Copy(ftpConn.dataConn, reader)

	if err != nil {
		ftpConn.logger.Printf("sendOutofbandReader copy error %s", err)
		ftpConn.writeMessage(550, "Action not taken")
		return
	}

	ftpConn.writeMessage(226, "Transfer complete.")

	// Chrome dies on localhost if we close connection to soon
	time.Sleep(10 * time.Millisecond)
}

// sendOutofbandData will send a string to the client via the currently open
// data socket. Assumes the socket is open and ready to be used.
func (ftpConn *ftpConn) sendOutofbandData(data string) {
	ftpConn.sendOutofbandReader(bytes.NewReader([]byte(data)))
}

func (ftpConn *ftpConn) newPassiveSocket() (socket *ftpPassiveSocket, err error) {
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
		ftpConn.dataConn = nil
	}

	socket, err = newPassiveSocket(ftpConn.localIP(), ftpConn.minDataPort, ftpConn.maxDataPort, ftpConn.logger)

	if err == nil {
		ftpConn.dataConn = socket
	}

	return
}

func (ftpConn *ftpConn) newActiveSocket(host string, port int) (socket *ftpActiveSocket, err error) {
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
		ftpConn.dataConn = nil
	}

	socket, err = newActiveSocket(host, port, ftpConn.logger)

	if err == nil {
		ftpConn.dataConn = socket
	}

	return
}
<commit_msg>Change TCPConn to Conn interface for easier unit testing.<commit_after>package graval

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"path/filepath"
	"strings"
	"time"
)

type ftpConn struct {
	conn          net.Conn
	controlReader    *bufio.Reader
	controlWriter    *bufio.Writer
	dataConn         ftpDataSocket
	driver           FTPDriver
	logger           *ftpLogger
	serverName       string
	sessionId        string
	namePrefix       string
	reqUser          string
	user             string
	renameFrom       string
	minDataPort      int
	maxDataPort      int
	pasvAdvertisedIp string
}

// NewftpConn constructs a new object that will handle the FTP protocol over
// an active net.TCPConn. The TCP connection should already be open before
// it is handed to this functions. driver is an instance of FTPDriver that
// will handle all auth and persistence details.
func newftpConn(tcpConn net.Conn, driver FTPDriver, serverName string, minPort int, maxPort int, pasvAdvertisedIp string) *ftpConn {
	c := new(ftpConn)
	c.namePrefix = "/"
	c.conn = tcpConn
	c.controlReader = bufio.NewReader(tcpConn)
	c.controlWriter = bufio.NewWriter(tcpConn)
	c.driver = driver
	c.sessionId = newSessionId()
	c.logger = newFtpLogger(c.sessionId)
	c.serverName = serverName
	c.minDataPort = minPort
	c.maxDataPort = maxPort
	c.pasvAdvertisedIp = pasvAdvertisedIp
	return c
}

// returns a random 20 char string that can be used as a unique session ID
func newSessionId() string {
	hash := sha256.New()
	_, err := io.CopyN(hash, rand.Reader, 50)
	if err != nil {
		return "????????????????????"
	}
	md := hash.Sum(nil)
	mdStr := hex.EncodeToString(md)
	return mdStr[0:20]
}

// Serve starts an endless loop that reads FTP commands from the client and
// responds appropriately. terminated is a channel that will receive a true
// message when the connection closes. This loop will be running inside a
// goroutine, so use this channel to be notified when the connection can be
// cleaned up.
func (ftpConn *ftpConn) Serve() {
	defer func() {
		if r := recover(); r != nil {
			ftpConn.logger.Printf("Recovered in ftpConn Serve: %s", r)
		}

		ftpConn.Close()
	}()

	ftpConn.logger.Printf("Connection Established (local: %s, remote: %s)", ftpConn.localIP(), ftpConn.remoteIP())
	// send welcome
	ftpConn.writeMessage(220, ftpConn.serverName)
	// read commands
	for {
		line, err := ftpConn.controlReader.ReadString('\n')
		if err != nil {
			break
		}
		ftpConn.receiveLine(line)
	}
	ftpConn.logger.Print("Connection Terminated")
}

// Close will manually close this connection, even if the client isn't ready.
func (ftpConn *ftpConn) Close() {
	ftpConn.conn.Close()
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
	}
}

// receiveLine accepts a single line FTP command and co-ordinates an
// appropriate response.
func (ftpConn *ftpConn) receiveLine(line string) {
	command, param := ftpConn.parseLine(line)
	ftpConn.logger.PrintCommand(command, param)
	cmdObj := commands[command]
	if cmdObj == nil {
		ftpConn.writeMessage(500, "Command not found")
		return
	}
	if cmdObj.RequireParam() && param == "" {
		ftpConn.writeMessage(553, "action aborted, required param missing")
	} else if cmdObj.RequireAuth() && ftpConn.user == "" {
		ftpConn.writeMessage(530, "not logged in")
	} else {
		cmdObj.Execute(ftpConn, param)
	}
}

func (ftpConn *ftpConn) parseLine(line string) (string, string) {
	params := strings.SplitN(strings.Trim(line, "\r\n"), " ", 2)
	if len(params) == 1 {
		return params[0], ""
	}
	return params[0], strings.TrimSpace(params[1])
}

// writeMessage will send a standard FTP response back to the client.
func (ftpConn *ftpConn) writeMessage(code int, message string) (wrote int, err error) {
	ftpConn.logger.PrintResponse(code, message)
	line := fmt.Sprintf("%d %s\r\n", code, message)
	wrote, err = ftpConn.controlWriter.WriteString(line)
	ftpConn.controlWriter.Flush()
	return
}

// writeLines will send a multiline FTP response back to the client.
func (ftpConn *ftpConn) writeLines(code int, lines ...string) (wrote int, err error) {
	message := strings.Join(lines, "\r\n") + "\r\n"
	ftpConn.logger.PrintResponse(code, message)
	wrote, err = ftpConn.controlWriter.WriteString(message)
	ftpConn.controlWriter.Flush()
	return
}

// buildPath takes a client supplied path or filename and generates a safe
// absolute path within their account sandbox.
//
//    buildpath("/")
//    => "/"
//    buildpath("one.txt")
//    => "/one.txt"
//    buildpath("/files/two.txt")
//    => "/files/two.txt"
//    buildpath("files/two.txt")
//    => "files/two.txt"
//    buildpath("/../../../../etc/passwd")
//    => "/etc/passwd"
//
// The driver implementation is responsible for deciding how to treat this path.
// Obviously they MUST NOT just read the path off disk. The probably want to
// prefix the path with something to scope the users access to a sandbox.
func (ftpConn *ftpConn) buildPath(filename string) (fullPath string) {
	if len(filename) > 0 && filename[0:1] == "/" {
		fullPath = filepath.Clean(filename)
	} else if len(filename) > 0 {
		fullPath = filepath.Clean(ftpConn.namePrefix + "/" + filename)
	} else {
		fullPath = filepath.Clean(ftpConn.namePrefix)
	}
	fullPath = strings.Replace(fullPath, "//", "/", -1)
	return
}

// the server IP that is being used for this connection. May be the same for all connections,
// or may vary if the server is listening on 0.0.0.0
func (ftpConn *ftpConn) localIP() string {
	lAddr := ftpConn.conn.LocalAddr().(*net.TCPAddr)
	return lAddr.IP.String()
}

// the client IP address
func (ftpConn *ftpConn) remoteIP() string {
	rAddr := ftpConn.conn.RemoteAddr().(*net.TCPAddr)
	return rAddr.IP.String()
}

// sendOutofbandData will copy data from reader to the client via the currently
// open data socket. Assumes the socket is open and ready to be used.
func (ftpConn *ftpConn) sendOutofbandReader(reader io.Reader) {
	defer ftpConn.dataConn.Close()

	_, err := io.Copy(ftpConn.dataConn, reader)

	if err != nil {
		ftpConn.logger.Printf("sendOutofbandReader copy error %s", err)
		ftpConn.writeMessage(550, "Action not taken")
		return
	}

	ftpConn.writeMessage(226, "Transfer complete.")

	// Chrome dies on localhost if we close connection to soon
	time.Sleep(10 * time.Millisecond)
}

// sendOutofbandData will send a string to the client via the currently open
// data socket. Assumes the socket is open and ready to be used.
func (ftpConn *ftpConn) sendOutofbandData(data string) {
	ftpConn.sendOutofbandReader(bytes.NewReader([]byte(data)))
}

func (ftpConn *ftpConn) newPassiveSocket() (socket *ftpPassiveSocket, err error) {
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
		ftpConn.dataConn = nil
	}

	socket, err = newPassiveSocket(ftpConn.localIP(), ftpConn.minDataPort, ftpConn.maxDataPort, ftpConn.logger)

	if err == nil {
		ftpConn.dataConn = socket
	}

	return
}

func (ftpConn *ftpConn) newActiveSocket(host string, port int) (socket *ftpActiveSocket, err error) {
	if ftpConn.dataConn != nil {
		ftpConn.dataConn.Close()
		ftpConn.dataConn = nil
	}

	socket, err = newActiveSocket(host, port, ftpConn.logger)

	if err == nil {
		ftpConn.dataConn = socket
	}

	return
}
<|endoftext|>