package describe

import (
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"
	"text/tabwriter"

	kapi "k8s.io/kubernetes/pkg/api"
	kapierrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/apis/autoscaling"
	kclient "k8s.io/kubernetes/pkg/client/unversioned"
	utilerrors "k8s.io/kubernetes/pkg/util/errors"
	"k8s.io/kubernetes/pkg/util/sets"

	osgraph "github.com/openshift/origin/pkg/api/graph"
	"github.com/openshift/origin/pkg/api/graph/graphview"
	kubeedges "github.com/openshift/origin/pkg/api/kubegraph"
	kubeanalysis "github.com/openshift/origin/pkg/api/kubegraph/analysis"
	kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
	buildapi "github.com/openshift/origin/pkg/build/api"
	buildedges "github.com/openshift/origin/pkg/build/graph"
	buildanalysis "github.com/openshift/origin/pkg/build/graph/analysis"
	buildgraph "github.com/openshift/origin/pkg/build/graph/nodes"
	"github.com/openshift/origin/pkg/client"
	deployapi "github.com/openshift/origin/pkg/deploy/api"
	deployedges "github.com/openshift/origin/pkg/deploy/graph"
	deployanalysis "github.com/openshift/origin/pkg/deploy/graph/analysis"
	deploygraph "github.com/openshift/origin/pkg/deploy/graph/nodes"
	deployutil "github.com/openshift/origin/pkg/deploy/util"
	imageapi "github.com/openshift/origin/pkg/image/api"
	imageedges "github.com/openshift/origin/pkg/image/graph"
	imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
	projectapi "github.com/openshift/origin/pkg/project/api"
	routeapi "github.com/openshift/origin/pkg/route/api"
	routeedges "github.com/openshift/origin/pkg/route/graph"
	routeanalysis "github.com/openshift/origin/pkg/route/graph/analysis"
	routegraph "github.com/openshift/origin/pkg/route/graph/nodes"
	"github.com/openshift/origin/pkg/util/errors"
	"github.com/openshift/origin/pkg/util/parallel"
)

const ForbiddenListWarning = "Forbidden"

// ProjectStatusDescriber generates extended information about a Project
type ProjectStatusDescriber struct {
	K       kclient.Interface
	C       client.Interface
	Server  string
	Suggest bool

	LogsCommandName             string
	SecurityPolicyCommandFormat string
	SetProbeCommandName         string
}

func (d *ProjectStatusDescriber) MakeGraph(namespace string) (osgraph.Graph, sets.String, error) {
	g := osgraph.New()

	loaders := []GraphLoader{
		&serviceLoader{namespace: namespace, lister: d.K},
		&serviceAccountLoader{namespace: namespace, lister: d.K},
		&secretLoader{namespace: namespace, lister: d.K},
		&rcLoader{namespace: namespace, lister: d.K},
		&podLoader{namespace: namespace, lister: d.K},
		&horizontalPodAutoscalerLoader{namespace: namespace, lister: d.K.Autoscaling()},
		// TODO check swagger for feature enablement and selectively add bcLoader and buildLoader
		// then remove errors.TolerateNotFoundError method.
		&bcLoader{namespace: namespace, lister: d.C},
		&buildLoader{namespace: namespace, lister: d.C},
		&isLoader{namespace: namespace, lister: d.C},
		&dcLoader{namespace: namespace, lister: d.C},
		&routeLoader{namespace: namespace, lister: d.C},
	}
	loadingFuncs := []func() error{}
	for _, loader := range loaders {
		loadingFuncs = append(loadingFuncs, loader.Load)
	}

	forbiddenResources := sets.String{}
	if errs := parallel.Run(loadingFuncs...); len(errs) > 0 {
		actualErrors := []error{}
		for _, err := range errs {
			if kapierrors.IsForbidden(err) {
				forbiddenErr := err.(*kapierrors.StatusError)
				if (forbiddenErr.Status().Details != nil) && (len(forbiddenErr.Status().Details.Kind) > 0) {
					forbiddenResources.Insert(forbiddenErr.Status().Details.Kind)
				}
				continue
			}
			actualErrors = append(actualErrors, err)
		}

		if len(actualErrors) > 0 {
			return g, forbiddenResources, utilerrors.NewAggregate(actualErrors)
		}
	}

	for _, loader := range loaders {
		loader.AddToGraph(g)
	}

	kubeedges.AddAllExposedPodTemplateSpecEdges(g)
	kubeedges.AddAllExposedPodEdges(g)
	kubeedges.AddAllManagedByRCPodEdges(g)
	kubeedges.AddAllRequestedServiceAccountEdges(g)
	kubeedges.AddAllMountableSecretEdges(g)
	kubeedges.AddAllMountedSecretEdges(g)
	kubeedges.AddHPAScaleRefEdges(g)
	buildedges.AddAllInputOutputEdges(g)
	buildedges.AddAllBuildEdges(g)
	deployedges.AddAllTriggerEdges(g)
	deployedges.AddAllDeploymentEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)
	imageedges.AddAllImageStreamImageRefEdges(g)
	routeedges.AddAllRouteEdges(g)

	return g, forbiddenResources, nil
}

// Describe returns the description of a project
func (d *ProjectStatusDescriber) Describe(namespace, name string) (string, error) {
	var f formatter = namespacedFormatter{}

	g, forbiddenResources, err := d.MakeGraph(namespace)
	if err != nil {
		return "", err
	}

	allNamespaces := namespace == kapi.NamespaceAll
	var project *projectapi.Project
	if !allNamespaces {
		p, err := d.C.Projects().Get(namespace)
		if err != nil {
			return "", err
		}
		project = p
		f = namespacedFormatter{currentNamespace: namespace}
	}

	coveredNodes := graphview.IntSet{}

	services, coveredByServices := graphview.AllServiceGroups(g, coveredNodes)
	coveredNodes.Insert(coveredByServices.List()...)

	standaloneDCs, coveredByDCs := graphview.AllDeploymentConfigPipelines(g, coveredNodes)
	coveredNodes.Insert(coveredByDCs.List()...)

	standaloneRCs, coveredByRCs := graphview.AllReplicationControllers(g, coveredNodes)
	coveredNodes.Insert(coveredByRCs.List()...)

	standaloneImages, coveredByImages := graphview.AllImagePipelinesFromBuildConfig(g, coveredNodes)
	coveredNodes.Insert(coveredByImages.List()...)

	standalonePods, coveredByPods := graphview.AllPods(g, coveredNodes)
	coveredNodes.Insert(coveredByPods.List()...)

	return tabbedString(func(out *tabwriter.Writer) error {
		indent := "  "
		if allNamespaces {
			fmt.Fprintf(out, describeAllProjectsOnServer(f, d.Server))
		} else {
			fmt.Fprintf(out, describeProjectAndServer(f, project, d.Server))
		}

		for _, service := range services {
			if !service.Service.Found() {
				continue
			}
			local := namespacedFormatter{currentNamespace: service.Service.Namespace}

			var exposes []string
			for _, routeNode := range service.ExposingRoutes {
				exposes = append(exposes, describeRouteInServiceGroup(local, routeNode)...)
			}
			sort.Sort(exposedRoutes(exposes))

			fmt.Fprintln(out)
			printLines(out, "", 0, describeServiceInServiceGroup(f, service, exposes...)...)

			for _, dcPipeline := range service.DeploymentConfigPipelines {
				printLines(out, indent, 1, describeDeploymentInServiceGroup(local, dcPipeline)...)
			}

		rcNode:
			for _, rcNode := range service.FulfillingRCs {
				for _, coveredDC := range service.FulfillingDCs {
					if deployedges.BelongsToDeploymentConfig(coveredDC.DeploymentConfig, rcNode.ReplicationController) {
						continue rcNode
					}
				}
				printLines(out, indent, 1, describeRCInServiceGroup(local, rcNode)...)
			}

		pod:
			for _, podNode := range service.FulfillingPods {
				// skip pods that have been displayed in a roll-up of RCs and DCs (by implicit usage of RCs)
				for _, coveredRC := range service.FulfillingRCs {
					if g.Edge(podNode, coveredRC) != nil {
						continue pod
					}
				}
				printLines(out, indent, 1, describePodInServiceGroup(local, podNode)...)
			}
		}

		for _, standaloneDC := range standaloneDCs {
			fmt.Fprintln(out)
			printLines(out, indent, 0, describeDeploymentInServiceGroup(f, standaloneDC)...)
		}

		for _, standaloneImage := range standaloneImages {
			fmt.Fprintln(out)
			lines := describeStandaloneBuildGroup(f, standaloneImage, namespace)
			lines = append(lines, describeAdditionalBuildDetail(standaloneImage.Build, standaloneImage.LastSuccessfulBuild, standaloneImage.LastUnsuccessfulBuild, standaloneImage.ActiveBuilds, standaloneImage.DestinationResolved, true)...)
			printLines(out, indent, 0, lines...)
		}

		for _, standaloneRC := range standaloneRCs {
			fmt.Fprintln(out)
			printLines(out, indent, 0, describeRCInServiceGroup(f, standaloneRC.RC)...)
		}

		monopods, err := filterBoringPods(standalonePods)
		if err != nil {
			return err
		}
		for _, monopod := range monopods {
			fmt.Fprintln(out)
			printLines(out, indent, 0, describeMonopod(f, monopod.Pod)...)
		}

		allMarkers := osgraph.Markers{}
		allMarkers = append(allMarkers, createForbiddenMarkers(forbiddenResources)...)
		for _, scanner := range getMarkerScanners(d.LogsCommandName, d.SecurityPolicyCommandFormat, d.SetProbeCommandName) {
			allMarkers = append(allMarkers, scanner(g, f)...)
		}

		// TODO: Provide an option to chase these hidden markers.
		allMarkers = allMarkers.FilterByNamespace(namespace)

		fmt.Fprintln(out)

		sort.Stable(osgraph.ByKey(allMarkers))
		sort.Stable(osgraph.ByNodeID(allMarkers))

		errorMarkers := allMarkers.BySeverity(osgraph.ErrorSeverity)
		errorSuggestions := 0
		if len(errorMarkers) > 0 {
			fmt.Fprintln(out, "Errors:")
			for _, marker := range errorMarkers {
				fmt.Fprintln(out, indent+"* "+marker.Message)
				if len(marker.Suggestion) > 0 {
					errorSuggestions++
					if d.Suggest {
						switch s := marker.Suggestion.String(); {
						case strings.Contains(s, "\n"):
							fmt.Fprintln(out)
							for _, line := range strings.Split(s, "\n") {
								fmt.Fprintln(out, indent+"  "+line)
							}
						case len(s) > 0:
							fmt.Fprintln(out, indent+"  try: "+s)
						}
					}
				}
			}
		}

		warningMarkers := allMarkers.BySeverity(osgraph.WarningSeverity)
		if len(warningMarkers) > 0 {
			if d.Suggest {
				fmt.Fprintln(out, "Warnings:")
			}
			for _, marker := range warningMarkers {
				if d.Suggest {
					fmt.Fprintln(out, indent+"* "+marker.Message)
					switch s := marker.Suggestion.String(); {
					case strings.Contains(s, "\n"):
						fmt.Fprintln(out)
						for _, line := range strings.Split(s, "\n") {
							fmt.Fprintln(out, indent+"  "+line)
						}
					case len(s) > 0:
						fmt.Fprintln(out, indent+"  try: "+s)
					}
				}
			}
		}

		// We print errors by default and warnings if -v is used. If we get none,
		// this would be an extra new line.
		if len(errorMarkers) != 0 || (d.Suggest && len(warningMarkers) != 0) {
			fmt.Fprintln(out)
		}

		errors, warnings := "", ""
		if len(errorMarkers) == 1 {
			errors = "1 error"
		} else if len(errorMarkers) > 1 {
			errors = fmt.Sprintf("%d errors", len(errorMarkers))
		}
		if len(warningMarkers) == 1 {
			warnings = "1 warning"
		} else if len(warningMarkers) > 1 {
			warnings = fmt.Sprintf("%d warnings", len(warningMarkers))
		}

		switch {
		case !d.Suggest && len(errorMarkers) > 0 && len(warningMarkers) > 0:
			fmt.Fprintf(out, "%s and %s identified, use 'oc status -v' to see details.\n", errors, warnings)

		case !d.Suggest && len(errorMarkers) > 0 && errorSuggestions > 0:
			fmt.Fprintf(out, "%s identified, use 'oc status -v' to see details.\n", errors)

		case !d.Suggest && len(warningMarkers) > 0:
			fmt.Fprintf(out, "%s identified, use 'oc status -v' to see details.\n", warnings)

		case (len(services) == 0) && (len(standaloneDCs) == 0) && (len(standaloneImages) == 0):
			fmt.Fprintln(out, "You have no services, deployment configs, or build configs.")
			fmt.Fprintln(out, "Run 'oc new-app' to create an application.")

		default:
			fmt.Fprintln(out, "View details with 'oc describe <resource>/<name>' or list everything with 'oc get all'.")
		}

		return nil
	})
}

func createForbiddenMarkers(forbiddenResources sets.String) []osgraph.Marker {
	markers := []osgraph.Marker{}
	for forbiddenResource := range forbiddenResources {
		markers = append(markers, osgraph.Marker{
			Severity: osgraph.WarningSeverity,
			Key:      ForbiddenListWarning,
			Message:  fmt.Sprintf("Unable to list %s resources.  Not all status relationships can be established.", forbiddenResource),
		})
	}
	return markers
}

func getMarkerScanners(logsCommandName, securityPolicyCommandFormat, setProbeCommandName string) []osgraph.MarkerScanner {
	return []osgraph.MarkerScanner{
		func(g osgraph.Graph, f osgraph.Namer) []osgraph.Marker {
			return kubeanalysis.FindRestartingPods(g, f, logsCommandName, securityPolicyCommandFormat)
		},
		kubeanalysis.FindDuelingReplicationControllers,
		kubeanalysis.FindMissingSecrets,
		kubeanalysis.FindHPASpecsMissingCPUTargets,
		kubeanalysis.FindHPASpecsMissingScaleRefs,
		kubeanalysis.FindOverlappingHPAs,
		buildanalysis.FindUnpushableBuildConfigs,
		buildanalysis.FindCircularBuilds,
		buildanalysis.FindPendingTags,
		deployanalysis.FindDeploymentConfigTriggerErrors,
		buildanalysis.FindMissingInputImageStreams,
		func(g osgraph.Graph, f osgraph.Namer) []osgraph.Marker {
			return deployanalysis.FindDeploymentConfigReadinessWarnings(g, f, setProbeCommandName)
		},
		routeanalysis.FindPortMappingIssues,
		routeanalysis.FindMissingTLSTerminationType,
		routeanalysis.FindPathBasedPassthroughRoutes,
		routeanalysis.FindRouteAdmissionFailures,
		routeanalysis.FindMissingRouter,
		// We disable this feature by default and we don't have a capability detection for this sort of thing.  Disable this check for now.
		// kubeanalysis.FindUnmountableSecrets,
	}
}

func printLines(out io.Writer, indent string, depth int, lines ...string) {
	for i, s := range lines {
		fmt.Fprintf(out, strings.Repeat(indent, depth))
		if i != 0 {
			fmt.Fprint(out, indent)
		}
		fmt.Fprintln(out, s)
	}
}

func indentLines(indent string, lines ...string) []string {
	ret := make([]string, 0, len(lines))
	for _, line := range lines {
		ret = append(ret, indent+line)
	}

	return ret
}

type formatter interface {
	ResourceName(obj interface{}) string
}

func namespaceNameWithType(resource, name, namespace, defaultNamespace string, noNamespace bool) string {
	if noNamespace || namespace == defaultNamespace || len(namespace) == 0 {
		return resource + "/" + name
	}
	return resource + "/" + name + "[" + namespace + "]"
}

var namespaced = namespacedFormatter{}

type namespacedFormatter struct {
	hideNamespace    bool
	currentNamespace string
}

func (f namespacedFormatter) ResourceName(obj interface{}) string {
	switch t := obj.(type) {

	case *kubegraph.PodNode:
		return namespaceNameWithType("pod", t.Name, t.Namespace, f.currentNamespace, f.hideNamespace)
	case *kubegraph.ServiceNode:
		return namespaceNameWithType("svc", t.Name, t.Namespace, f.currentNamespace, f.hideNamespace)
	case *kubegraph.SecretNode:
		return namespaceNameWithType("secret", t.Name, t.Namespace, f.currentNamespace, f.hideNamespace)
	case *kubegraph.ServiceAccountNode:
		return namespaceNameWithType("sa", t.Name, t.Namespace, f.currentNamespace, f.hideNamespace)
	case *kubegraph.ReplicationControllerNode:
		return namespaceNameWithType("rc", t.Name, t.Namespace, f.currentNamespace, f.hideNamespace)
	case *kubegraph.HorizontalPodAutoscalerNode:
		return namespaceNameWithType("hpa", t.HorizontalPodAutoscaler.Name, t.HorizontalPodAutoscaler.Namespace, f.currentNamespace, f.hideNamespace)

	case *imagegraph.ImageStreamNode:
		return namespaceNameWithType("is", t.ImageStream.Name, t.ImageStream.Namespace, f.currentNamespace, f.hideNamespace)
	case *imagegraph.ImageStreamTagNode:
		return namespaceNameWithType("istag", t.ImageStreamTag.Name, t.ImageStreamTag.Namespace, f.currentNamespace, f.hideNamespace)
	case *imagegraph.ImageStreamImageNode:
		return namespaceNameWithType("isi", t.ImageStreamImage.Name, t.ImageStreamImage.Namespace, f.currentNamespace, f.hideNamespace)
	case *imagegraph.ImageNode:
		return namespaceNameWithType("image", t.Image.Name, t.Image.Namespace, f.currentNamespace, f.hideNamespace)
	case *buildgraph.BuildConfigNode:
		return namespaceNameWithType("bc", t.BuildConfig.Name, t.BuildConfig.Namespace, f.currentNamespace, f.hideNamespace)
	case *buildgraph.BuildNode:
		return namespaceNameWithType("build", t.Build.Name, t.Build.Namespace, f.currentNamespace, f.hideNamespace)

	case *deploygraph.DeploymentConfigNode:
		return namespaceNameWithType("dc", t.DeploymentConfig.Name, t.DeploymentConfig.Namespace, f.currentNamespace, f.hideNamespace)

	case *routegraph.RouteNode:
		return namespaceNameWithType("route", t.Route.Name, t.Route.Namespace, f.currentNamespace, f.hideNamespace)

	default:
		return fmt.Sprintf("<unrecognized object: %#v>", obj)
	}
}

func describeProjectAndServer(f formatter, project *projectapi.Project, server string) string {
	if len(server) == 0 {
		return fmt.Sprintf("In project %s on server %s\n", projectapi.DisplayNameAndNameForProject(project), server)
	}
	return fmt.Sprintf("In project %s on server %s\n", projectapi.DisplayNameAndNameForProject(project), server)

}

func describeAllProjectsOnServer(f formatter, server string) string {
	if len(server) == 0 {
		return "Showing all projects\n"
	}
	return fmt.Sprintf("Showing all projects on server %s\n", server)
}

func describeDeploymentInServiceGroup(f formatter, deploy graphview.DeploymentConfigPipeline) []string {
	local := namespacedFormatter{currentNamespace: deploy.Deployment.Namespace}

	includeLastPass := deploy.ActiveDeployment == nil
	if len(deploy.Images) == 1 {
		format := "%s deploys %s %s"
		if deploy.Deployment.Spec.Test {
			format = "%s test deploys %s %s"
		}
		lines := []string{fmt.Sprintf(format, f.ResourceName(deploy.Deployment), describeImageInPipeline(local, deploy.Images[0], deploy.Deployment.Namespace), describeDeploymentConfigTrigger(deploy.Deployment.DeploymentConfig))}
		if len(lines[0]) > 120 && strings.Contains(lines[0], " <- ") {
			segments := strings.SplitN(lines[0], " <- ", 2)
			lines[0] = segments[0] + " <-"
			lines = append(lines, segments[1])
		}
		lines = append(lines, indentLines("  ", describeAdditionalBuildDetail(deploy.Images[0].Build, deploy.Images[0].LastSuccessfulBuild, deploy.Images[0].LastUnsuccessfulBuild, deploy.Images[0].ActiveBuilds, deploy.Images[0].DestinationResolved, includeLastPass)...)...)
		lines = append(lines, describeDeployments(local, deploy.Deployment, deploy.ActiveDeployment, deploy.InactiveDeployments, maxDisplayDeployments)...)
		return lines
	}

	format := "%s deploys %s"
	if deploy.Deployment.Spec.Test {
		format = "%s test deploys %s"
	}
	lines := []string{fmt.Sprintf(format, f.ResourceName(deploy.Deployment), describeDeploymentConfigTrigger(deploy.Deployment.DeploymentConfig))}
	for _, image := range deploy.Images {
		lines = append(lines, describeImageInPipeline(local, image, deploy.Deployment.Namespace))
		lines = append(lines, indentLines("  ", describeAdditionalBuildDetail(image.Build, image.LastSuccessfulBuild, image.LastUnsuccessfulBuild, image.ActiveBuilds, image.DestinationResolved, includeLastPass)...)...)
		lines = append(lines, describeDeployments(local, deploy.Deployment, deploy.ActiveDeployment, deploy.InactiveDeployments, maxDisplayDeployments)...)
	}
	return lines
}

func describeRCInServiceGroup(f formatter, rcNode *kubegraph.ReplicationControllerNode) []string {
	if rcNode.ReplicationController.Spec.Template == nil {
		return []string{}
	}

	images := []string{}
	for _, container := range rcNode.ReplicationController.Spec.Template.Spec.Containers {
		images = append(images, container.Image)
	}

	lines := []string{fmt.Sprintf("%s runs %s", f.ResourceName(rcNode), strings.Join(images, ", "))}
	lines = append(lines, describeRCStatus(rcNode.ReplicationController))

	return lines
}

func describePodInServiceGroup(f formatter, podNode *kubegraph.PodNode) []string {
	images := []string{}
	for _, container := range podNode.Pod.Spec.Containers {
		images = append(images, container.Image)
	}

	lines := []string{fmt.Sprintf("%s runs %s", f.ResourceName(podNode), strings.Join(images, ", "))}
	return lines
}

func describeMonopod(f formatter, podNode *kubegraph.PodNode) []string {
	images := []string{}
	for _, container := range podNode.Pod.Spec.Containers {
		images = append(images, container.Image)
	}

	lines := []string{fmt.Sprintf("%s runs %s", f.ResourceName(podNode), strings.Join(images, ", "))}
	return lines
}

// exposedRoutes orders strings by their leading prefix (https:// -> http:// other prefixes), then by
// the shortest distance up to the first space (indicating a break), then alphabetically:
//
//   https://test.com
//   https://www.test.com
//   http://t.com
//   other string
//
type exposedRoutes []string

func (e exposedRoutes) Len() int      { return len(e) }
func (e exposedRoutes) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
func (e exposedRoutes) Less(i, j int) bool {
	a, b := e[i], e[j]
	prefixA, prefixB := strings.HasPrefix(a, "https://"), strings.HasPrefix(b, "https://")
	switch {
	case prefixA && !prefixB:
		return true
	case !prefixA && prefixB:
		return false
	case !prefixA && !prefixB:
		prefixA, prefixB = strings.HasPrefix(a, "http://"), strings.HasPrefix(b, "http://")
		switch {
		case prefixA && !prefixB:
			return true
		case !prefixA && prefixB:
			return false
		case !prefixA && !prefixB:
			return a < b
		default:
			a, b = a[7:], b[7:]
		}
	default:
		a, b = a[8:], b[8:]
	}
	lA, lB := strings.Index(a, " "), strings.Index(b, " ")
	if lA == -1 {
		lA = len(a)
	}
	if lB == -1 {
		lB = len(b)
	}
	switch {
	case lA < lB:
		return true
	case lA > lB:
		return false
	default:
		return a < b
	}
}

func extractRouteInfo(route *routeapi.Route) (requested bool, other []string, errors []string) {
	reasons := sets.NewString()
	for _, ingress := range route.Status.Ingress {
		exact := route.Spec.Host == ingress.Host
		switch status, condition := routeapi.IngressConditionStatus(&ingress, routeapi.RouteAdmitted); status {
		case kapi.ConditionFalse:
			reasons.Insert(condition.Reason)
		default:
			if exact {
				requested = true
			} else {
				other = append(other, ingress.Host)
			}
		}
	}
	return requested, other, reasons.List()
}

func describeRouteExposed(host string, route *routeapi.Route, errors bool) string {
	var trailer string
	if errors {
		trailer = " (!)"
	}
	var prefix string
	switch {
	case route.Spec.TLS == nil:
		prefix = fmt.Sprintf("http://%s", host)
	case route.Spec.TLS.Termination == routeapi.TLSTerminationPassthrough:
		prefix = fmt.Sprintf("https://%s (passthrough)", host)
	case route.Spec.TLS.Termination == routeapi.TLSTerminationReencrypt:
		prefix = fmt.Sprintf("https://%s (reencrypt)", host)
	case route.Spec.TLS.Termination != routeapi.TLSTerminationEdge:
		// future proof against other types of TLS termination being added
		prefix = fmt.Sprintf("https://%s", host)
	case route.Spec.TLS.InsecureEdgeTerminationPolicy == routeapi.InsecureEdgeTerminationPolicyRedirect:
		prefix = fmt.Sprintf("https://%s (redirects)", host)
	case route.Spec.TLS.InsecureEdgeTerminationPolicy == routeapi.InsecureEdgeTerminationPolicyAllow:
		prefix = fmt.Sprintf("https://%s (and http)", host)
	default:
		prefix = fmt.Sprintf("https://%s", host)
	}

	if route.Spec.Port != nil && len(route.Spec.Port.TargetPort.String()) > 0 {
		return fmt.Sprintf("%s to pod port %s%s", prefix, route.Spec.Port.TargetPort.String(), trailer)
	}
	return fmt.Sprintf("%s%s", prefix, trailer)
}

func describeRouteInServiceGroup(f formatter, routeNode *routegraph.RouteNode) []string {
	// markers should cover printing information about admission failure
	requested, other, errors := extractRouteInfo(routeNode.Route)
	var lines []string
	if requested {
		lines = append(lines, describeRouteExposed(routeNode.Spec.Host, routeNode.Route, len(errors) > 0))
	}
	for _, s := range other {
		lines = append(lines, describeRouteExposed(s, routeNode.Route, len(errors) > 0))
	}
	if len(lines) == 0 {
		switch {
		case len(errors) >= 1:
			// router rejected the output
			lines = append(lines, fmt.Sprintf("%s not accepted: %s", f.ResourceName(routeNode), errors[0]))
		case len(routeNode.Spec.Host) == 0:
			// no errors or output, likely no router running and no default domain
			lines = append(lines, fmt.Sprintf("%s has no host set", f.ResourceName(routeNode)))
		case len(routeNode.Status.Ingress) == 0:
			// host set, but no ingress, an older legacy router
			lines = append(lines, describeRouteExposed(routeNode.Spec.Host, routeNode.Route, false))
		default:
			// multiple conditions but no host exposed, use the generic legacy output
			lines = append(lines, fmt.Sprintf("exposed as %s by %s", routeNode.Spec.Host, f.ResourceName(routeNode)))
		}
	}
	return lines
}

func describeDeploymentConfigTrigger(dc *deployapi.DeploymentConfig) string {
	if len(dc.Spec.Triggers) == 0 {
		return "(manual)"
	}

	return ""
}

func describeStandaloneBuildGroup(f formatter, pipeline graphview.ImagePipeline, namespace string) []string {
	switch {
	case pipeline.Build != nil:
		lines := []string{describeBuildInPipeline(f, pipeline, namespace)}
		if pipeline.Image != nil {
			lines = append(lines, fmt.Sprintf("-> %s", describeImageTagInPipeline(f, pipeline.Image, namespace)))
		}
		return lines
	case pipeline.Image != nil:
		return []string{describeImageTagInPipeline(f, pipeline.Image, namespace)}
	default:
		return []string{"<unknown>"}
	}
}

func describeImageInPipeline(f formatter, pipeline graphview.ImagePipeline, namespace string) string {
	switch {
	case pipeline.Image != nil && pipeline.Build != nil:
		return fmt.Sprintf("%s <- %s", describeImageTagInPipeline(f, pipeline.Image, namespace), describeBuildInPipeline(f, pipeline, namespace))
	case pipeline.Image != nil:
		return describeImageTagInPipeline(f, pipeline.Image, namespace)
	case pipeline.Build != nil:
		return describeBuildInPipeline(f, pipeline, namespace)
	default:
		return "<unknown>"
	}
}

func describeImageTagInPipeline(f formatter, image graphview.ImageTagLocation, namespace string) string {
	switch t := image.(type) {
	case *imagegraph.ImageStreamTagNode:
		if t.ImageStreamTag.Namespace != namespace {
			return image.ImageSpec()
		}
		return f.ResourceName(t)
	default:
		return image.ImageSpec()
	}
}

func describeBuildInPipeline(f formatter, pipeline graphview.ImagePipeline, namespace string) string {
	bldType := ""
	switch {
	case pipeline.Build.BuildConfig.Spec.Strategy.DockerStrategy != nil:
		bldType = "docker"
	case pipeline.Build.BuildConfig.Spec.Strategy.SourceStrategy != nil:
		bldType = "source"
	case pipeline.Build.BuildConfig.Spec.Strategy.CustomStrategy != nil:
		bldType = "custom"
	case pipeline.Build.BuildConfig.Spec.Strategy.JenkinsPipelineStrategy != nil:
		return fmt.Sprintf("bc/%s is a Jenkins Pipeline", pipeline.Build.BuildConfig.Name)
	default:
		return fmt.Sprintf("bc/%s unrecognized build", pipeline.Build.BuildConfig.Name)
	}

	source, ok := describeSourceInPipeline(&pipeline.Build.BuildConfig.Spec.Source)
	if !ok {
		return fmt.Sprintf("bc/%s unconfigured %s build", pipeline.Build.BuildConfig.Name, bldType)
	}

	retStr := fmt.Sprintf("bc/%s %s builds %s", pipeline.Build.BuildConfig.Name, bldType, source)
	if pipeline.BaseImage != nil {
		retStr = retStr + fmt.Sprintf(" on %s", describeImageTagInPipeline(f, pipeline.BaseImage, namespace))
	}
	return retStr
}

func describeAdditionalBuildDetail(build *buildgraph.BuildConfigNode, lastSuccessfulBuild *buildgraph.BuildNode, lastUnsuccessfulBuild *buildgraph.BuildNode, activeBuilds []*buildgraph.BuildNode, pushTargetResolved bool, includeSuccess bool) []string {
	if build == nil {
		return nil
	}
	out := []string{}

	passTime := unversioned.Time{}
	if lastSuccessfulBuild != nil {
		passTime = buildTimestamp(lastSuccessfulBuild.Build)
	}
	failTime := unversioned.Time{}
	if lastUnsuccessfulBuild != nil {
		failTime = buildTimestamp(lastUnsuccessfulBuild.Build)
	}

	lastTime := failTime
	if passTime.After(failTime.Time) {
		lastTime = passTime
	}

	// display the last successful build if specifically requested or we're going to display an active build for context
	if lastSuccessfulBuild != nil && (includeSuccess || len(activeBuilds) > 0) {
		out = append(out, describeBuildPhase(lastSuccessfulBuild.Build, &passTime, build.BuildConfig.Name, pushTargetResolved))
	}
	if passTime.Before(failTime) {
		out = append(out, describeBuildPhase(lastUnsuccessfulBuild.Build, &failTime, build.BuildConfig.Name, pushTargetResolved))
	}

	if len(activeBuilds) > 0 {
		activeOut := []string{}
		for i := range activeBuilds {
			activeOut = append(activeOut, describeBuildPhase(activeBuilds[i].Build, nil, build.BuildConfig.Name, pushTargetResolved))
		}

		if buildTimestamp(activeBuilds[0].Build).Before(lastTime) {
			out = append(out, activeOut...)
		} else {
			out = append(activeOut, out...)
		}
	}
	if len(out) == 0 && lastSuccessfulBuild == nil {
		out = append(out, "not built yet")
	}
	return out
}

func describeBuildPhase(build *buildapi.Build, t *unversioned.Time, parentName string, pushTargetResolved bool) string {
	imageStreamFailure := ""
	// if we're using an image stream and that image stream is the internal registry and that registry doesn't exist
	if (build.Spec.Output.To != nil) && !pushTargetResolved {
		imageStreamFailure = " (can't push to image)"
	}

	if t == nil {
		ts := buildTimestamp(build)
		t = &ts
	}
	var time string
	if t.IsZero() {
		time = "<unknown>"
	} else {
		time = strings.ToLower(formatRelativeTime(t.Time))
	}
	buildIdentification := fmt.Sprintf("build/%s", build.Name)
	prefix := parentName + "-"
	if strings.HasPrefix(build.Name, prefix) {
		suffix := build.Name[len(prefix):]

		if buildNumber, err := strconv.Atoi(suffix); err == nil {
			buildIdentification = fmt.Sprintf("build #%d", buildNumber)
		}
	}

	revision := describeSourceRevision(build.Spec.Revision)
	if len(revision) != 0 {
		revision = fmt.Sprintf(" - %s", revision)
	}
	switch build.Status.Phase {
	case buildapi.BuildPhaseComplete:
		return fmt.Sprintf("%s succeeded %s ago%s%s", buildIdentification, time, revision, imageStreamFailure)
	case buildapi.BuildPhaseError:
		return fmt.Sprintf("%s stopped with an error %s ago%s%s", buildIdentification, time, revision, imageStreamFailure)
	case buildapi.BuildPhaseFailed:
		return fmt.Sprintf("%s failed %s ago%s%s", buildIdentification, time, revision, imageStreamFailure)
	default:
		status := strings.ToLower(string(build.Status.Phase))
		return fmt.Sprintf("%s %s for %s%s%s", buildIdentification, status, time, revision, imageStreamFailure)
	}
}

func describeSourceRevision(rev *buildapi.SourceRevision) string {
	if rev == nil {
		return ""
	}
	switch {
	case rev.Git != nil:
		author := describeSourceControlUser(rev.Git.Author)
		if len(author) == 0 {
			author = describeSourceControlUser(rev.Git.Committer)
		}
		if len(author) != 0 {
			author = fmt.Sprintf(" (%s)", author)
		}
		commit := rev.Git.Commit
		if len(commit) > 7 {
			commit = commit[:7]
		}
		return fmt.Sprintf("%s: %s%s", commit, rev.Git.Message, author)
	default:
		return ""
	}
}

func describeSourceControlUser(user buildapi.SourceControlUser) string {
	if len(user.Name) == 0 {
		return user.Email
	}
	if len(user.Email) == 0 {
		return user.Name
	}
	return fmt.Sprintf("%s <%s>", user.Name, user.Email)
}

func buildTimestamp(build *buildapi.Build) unversioned.Time {
	if build == nil {
		return unversioned.Time{}
	}
	if !build.Status.CompletionTimestamp.IsZero() {
		return *build.Status.CompletionTimestamp
	}
	if !build.Status.StartTimestamp.IsZero() {
		return *build.Status.StartTimestamp
	}
	return build.CreationTimestamp
}

func describeSourceInPipeline(source *buildapi.BuildSource) (string, bool) {
	switch {
	case source.Git != nil:
		if len(source.Git.Ref) == 0 {
			return source.Git.URI, true
		}
		return fmt.Sprintf("%s#%s", source.Git.URI, source.Git.Ref), true
	case source.Dockerfile != nil:
		return "Dockerfile", true
	case source.Binary != nil:
		return "uploaded code", true
	case len(source.Images) > 0:
		return "contents in other images", true
	}
	return "", false
}

func describeDeployments(f formatter, dcNode *deploygraph.DeploymentConfigNode, activeDeployment *kubegraph.ReplicationControllerNode, inactiveDeployments []*kubegraph.ReplicationControllerNode, count int) []string {
	if dcNode == nil {
		return nil
	}
	out := []string{}
	deploymentsToPrint := append([]*kubegraph.ReplicationControllerNode{}, inactiveDeployments...)

	if activeDeployment == nil {
		on, auto := describeDeploymentConfigTriggers(dcNode.DeploymentConfig)
		if dcNode.DeploymentConfig.Status.LatestVersion == 0 {
			out = append(out, fmt.Sprintf("deployment #1 waiting %s", on))
		} else if auto {
			out = append(out, fmt.Sprintf("deployment #%d pending %s", dcNode.DeploymentConfig.Status.LatestVersion, on))
		}
		// TODO: detect new image available?
	} else {
		deploymentsToPrint = append([]*kubegraph.ReplicationControllerNode{activeDeployment}, inactiveDeployments...)
	}

	for i, deployment := range deploymentsToPrint {
		out = append(out, describeDeploymentStatus(deployment.ReplicationController, i == 0, dcNode.DeploymentConfig.Spec.Test))

		switch {
		case count == -1:
			if deployutil.DeploymentStatusFor(deployment) == deployapi.DeploymentStatusComplete {
				return out
			}
		default:
			if i+1 >= count {
				return out
			}
		}
	}
	return out
}

func describeDeploymentStatus(deploy *kapi.ReplicationController, first, test bool) string {
	timeAt := strings.ToLower(formatRelativeTime(deploy.CreationTimestamp.Time))
	status := deployutil.DeploymentStatusFor(deploy)
	version := deployutil.DeploymentVersionFor(deploy)
	maybeCancelling := ""
	if deployutil.IsDeploymentCancelled(deploy) && !deployutil.IsTerminatedDeployment(deploy) {
		maybeCancelling = " (cancelling)"
	}

	switch status {
	case deployapi.DeploymentStatusFailed:
		reason := deployutil.DeploymentStatusReasonFor(deploy)
		if len(reason) > 0 {
			reason = fmt.Sprintf(": %s", reason)
		}
		// TODO: encode fail time in the rc
		return fmt.Sprintf("deployment #%d failed %s ago%s%s", version, timeAt, reason, describePodSummaryInline(deploy, false))
	case deployapi.DeploymentStatusComplete:
		// TODO: pod status output
		if test {
			return fmt.Sprintf("test deployment #%d deployed %s ago", version, timeAt)
		}
		return fmt.Sprintf("deployment #%d deployed %s ago%s", version, timeAt, describePodSummaryInline(deploy, first))
	case deployapi.DeploymentStatusRunning:
		format := "deployment #%d running%s for %s%s"
		if test {
			format = "test deployment #%d running%s for %s%s"
		}
		return fmt.Sprintf(format, version, maybeCancelling, timeAt, describePodSummaryInline(deploy, false))
	default:
		return fmt.Sprintf("deployment #%d %s%s %s ago%s", version, strings.ToLower(string(status)), maybeCancelling, timeAt, describePodSummaryInline(deploy, false))
	}
}

func describeRCStatus(rc *kapi.ReplicationController) string {
	timeAt := strings.ToLower(formatRelativeTime(rc.CreationTimestamp.Time))
	return fmt.Sprintf("rc/%s created %s ago%s", rc.Name, timeAt, describePodSummaryInline(rc, false))
}

func describePodSummaryInline(rc *kapi.ReplicationController, includeEmpty bool) string {
	s := describePodSummary(rc, includeEmpty)
	if len(s) == 0 {
		return s
	}
	change := ""
	desired := rc.Spec.Replicas
	switch {
	case desired < rc.Status.Replicas:
		change = fmt.Sprintf(" reducing to %d", desired)
	case desired > rc.Status.Replicas:
		change = fmt.Sprintf(" growing to %d", desired)
	}
	return fmt.Sprintf(" - %s%s", s, change)
}

func describePodSummary(rc *kapi.ReplicationController, includeEmpty bool) string {
	actual, requested := rc.Status.Replicas, rc.Spec.Replicas
	if actual == requested {
		switch {
		case actual == 0:
			if !includeEmpty {
				return ""
			}
			return "0 pods"
		case actual > 1:
			return fmt.Sprintf("%d pods", actual)
		default:
			return "1 pod"
		}
	}
	return fmt.Sprintf("%d/%d pods", actual, requested)
}

func describeDeploymentConfigTriggers(config *deployapi.DeploymentConfig) (string, bool) {
	hasConfig, hasImage := false, false
	for _, t := range config.Spec.Triggers {
		switch t.Type {
		case deployapi.DeploymentTriggerOnConfigChange:
			hasConfig = true
		case deployapi.DeploymentTriggerOnImageChange:
			hasImage = true
		}
	}
	switch {
	case hasConfig && hasImage:
		return "on image or update", true
	case hasConfig:
		return "on update", true
	case hasImage:
		return "on image", true
	default:
		return "for manual", false
	}
}

func describeServiceInServiceGroup(f formatter, svc graphview.ServiceGroup, exposed ...string) []string {
	spec := svc.Service.Spec
	ip := spec.ClusterIP
	port := describeServicePorts(spec)
	switch {
	case len(exposed) > 1:
		return append([]string{fmt.Sprintf("%s (%s)", exposed[0], f.ResourceName(svc.Service))}, exposed[1:]...)
	case len(exposed) == 1:
		return []string{fmt.Sprintf("%s (%s)", exposed[0], f.ResourceName(svc.Service))}
	case spec.Type == kapi.ServiceTypeNodePort:
		return []string{fmt.Sprintf("%s (all nodes)%s", f.ResourceName(svc.Service), port)}
	case ip == "None":
		return []string{fmt.Sprintf("%s (headless)%s", f.ResourceName(svc.Service), port)}
	case len(ip) == 0:
		return []string{fmt.Sprintf("%s <initializing>%s", f.ResourceName(svc.Service), port)}
	default:
		return []string{fmt.Sprintf("%s - %s%s", f.ResourceName(svc.Service), ip, port)}
	}
}

func portOrNodePort(spec kapi.ServiceSpec, port kapi.ServicePort) string {
	switch {
	case spec.Type != kapi.ServiceTypeNodePort:
		return strconv.Itoa(int(port.Port))
	case port.NodePort == 0:
		return "<initializing>"
	default:
		return strconv.Itoa(int(port.NodePort))
	}
}

func describeServicePorts(spec kapi.ServiceSpec) string {
	switch len(spec.Ports) {
	case 0:
		return " no ports"

	case 1:
		port := portOrNodePort(spec, spec.Ports[0])
		if spec.Ports[0].TargetPort.String() == "0" || spec.ClusterIP == kapi.ClusterIPNone || port == spec.Ports[0].TargetPort.String() {
			return fmt.Sprintf(":%s", port)
		}
		return fmt.Sprintf(":%s -> %s", port, spec.Ports[0].TargetPort.String())

	default:
		pairs := []string{}
		for _, port := range spec.Ports {
			externalPort := portOrNodePort(spec, port)
			if port.TargetPort.String() == "0" || spec.ClusterIP == kapi.ClusterIPNone {
				pairs = append(pairs, externalPort)
				continue
			}
			if port.Port == port.TargetPort.IntVal {
				pairs = append(pairs, port.TargetPort.String())
			} else {
				pairs = append(pairs, fmt.Sprintf("%s->%s", externalPort, port.TargetPort.String()))
			}
		}
		return " ports " + strings.Join(pairs, ", ")
	}
}

func filterBoringPods(pods []graphview.Pod) ([]graphview.Pod, error) {
	monopods := []graphview.Pod{}

	for _, pod := range pods {
		actualPod, ok := pod.Pod.Object().(*kapi.Pod)
		if !ok {
			continue
		}
		meta, err := kapi.ObjectMetaFor(actualPod)
		if err != nil {
			return nil, err
		}
		_, isDeployerPod := meta.Labels[deployapi.DeployerPodForDeploymentLabel]
		_, isBuilderPod := meta.Annotations[buildapi.BuildAnnotation]
		isFinished := actualPod.Status.Phase == kapi.PodSucceeded || actualPod.Status.Phase == kapi.PodFailed
		if isDeployerPod || isBuilderPod || isFinished {
			continue
		}
		monopods = append(monopods, pod)
	}

	return monopods, nil
}

// GraphLoader is a stateful interface that provides methods for building the nodes of a graph
type GraphLoader interface {
	// Load is responsible for gathering and saving the objects this GraphLoader should AddToGraph
	Load() error
	// AddToGraph
	AddToGraph(g osgraph.Graph) error
}

type rcLoader struct {
	namespace string
	lister    kclient.ReplicationControllersNamespacer
	items     []kapi.ReplicationController
}

func (l *rcLoader) Load() error {
	list, err := l.lister.ReplicationControllers(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *rcLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsureReplicationControllerNode(g, &l.items[i])
	}

	return nil
}

type serviceLoader struct {
	namespace string
	lister    kclient.ServicesNamespacer
	items     []kapi.Service
}

func (l *serviceLoader) Load() error {
	list, err := l.lister.Services(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *serviceLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsureServiceNode(g, &l.items[i])
	}

	return nil
}

type podLoader struct {
	namespace string
	lister    kclient.PodsNamespacer
	items     []kapi.Pod
}

func (l *podLoader) Load() error {
	list, err := l.lister.Pods(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *podLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsurePodNode(g, &l.items[i])
	}

	return nil
}

type horizontalPodAutoscalerLoader struct {
	namespace string
	lister    kclient.HorizontalPodAutoscalersNamespacer
	items     []autoscaling.HorizontalPodAutoscaler
}

func (l *horizontalPodAutoscalerLoader) Load() error {
	list, err := l.lister.HorizontalPodAutoscalers(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *horizontalPodAutoscalerLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsureHorizontalPodAutoscalerNode(g, &l.items[i])
	}

	return nil
}

type serviceAccountLoader struct {
	namespace string
	lister    kclient.ServiceAccountsNamespacer
	items     []kapi.ServiceAccount
}

func (l *serviceAccountLoader) Load() error {
	list, err := l.lister.ServiceAccounts(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *serviceAccountLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsureServiceAccountNode(g, &l.items[i])
	}

	return nil
}

type secretLoader struct {
	namespace string
	lister    kclient.SecretsNamespacer
	items     []kapi.Secret
}

func (l *secretLoader) Load() error {
	list, err := l.lister.Secrets(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *secretLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		kubegraph.EnsureSecretNode(g, &l.items[i])
	}

	return nil
}

type isLoader struct {
	namespace string
	lister    client.ImageStreamsNamespacer
	items     []imageapi.ImageStream
}

func (l *isLoader) Load() error {
	list, err := l.lister.ImageStreams(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *isLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		imagegraph.EnsureImageStreamNode(g, &l.items[i])
		imagegraph.EnsureAllImageStreamTagNodes(g, &l.items[i])
	}

	return nil
}

type dcLoader struct {
	namespace string
	lister    client.DeploymentConfigsNamespacer
	items     []deployapi.DeploymentConfig
}

func (l *dcLoader) Load() error {
	list, err := l.lister.DeploymentConfigs(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *dcLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		deploygraph.EnsureDeploymentConfigNode(g, &l.items[i])
	}

	return nil
}

type bcLoader struct {
	namespace string
	lister    client.BuildConfigsNamespacer
	items     []buildapi.BuildConfig
}

func (l *bcLoader) Load() error {
	list, err := l.lister.BuildConfigs(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return errors.TolerateNotFoundError(err)
	}

	l.items = list.Items
	return nil
}

func (l *bcLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		buildgraph.EnsureBuildConfigNode(g, &l.items[i])
	}

	return nil
}

type buildLoader struct {
	namespace string
	lister    client.BuildsNamespacer
	items     []buildapi.Build
}

func (l *buildLoader) Load() error {
	list, err := l.lister.Builds(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return errors.TolerateNotFoundError(err)
	}

	l.items = list.Items
	return nil
}

func (l *buildLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		buildgraph.EnsureBuildNode(g, &l.items[i])
	}

	return nil
}

type routeLoader struct {
	namespace string
	lister    client.RoutesNamespacer
	items     []routeapi.Route
}

func (l *routeLoader) Load() error {
	list, err := l.lister.Routes(l.namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}

	l.items = list.Items
	return nil
}

func (l *routeLoader) AddToGraph(g osgraph.Graph) error {
	for i := range l.items {
		routegraph.EnsureRouteNode(g, &l.items[i])
	}

	return nil
}