Exemple #1
0
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},
		// 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)
	buildedges.AddAllInputOutputEdges(g)
	buildedges.AddAllBuildEdges(g)
	deployedges.AddAllTriggerEdges(g)
	deployedges.AddAllDeploymentEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)
	routeedges.AddAllRouteEdges(g)

	return g, forbiddenResources, nil
}
func TestBareDCGroup(t *testing.T) {
	g, _, err := osgraphtest.BuildGraph("../../../api/graph/test/bare-dc.yaml")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	kubeedges.AddAllExposedPodTemplateSpecEdges(g)
	buildedges.AddAllInputOutputEdges(g)
	deployedges.AddAllTriggerEdges(g)

	coveredNodes := IntSet{}

	serviceGroups, coveredByServiceGroups := AllServiceGroups(g, coveredNodes)
	coveredNodes.Insert(coveredByServiceGroups.List()...)

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

	bareBCPipelines, coveredByBCs := AllImagePipelinesFromBuildConfig(g, coveredNodes)
	coveredNodes.Insert(coveredByBCs.List()...)

	if e, a := 0, len(serviceGroups); e != a {
		t.Errorf("expected %v, got %v", e, a)
	}
	if e, a := 1, len(bareDCPipelines); e != a {
		t.Errorf("expected %v, got %v", e, a)
	}
	if e, a := 0, len(bareBCPipelines); e != a {
		t.Errorf("expected %v, got %v", e, a)
	}

	if e, a := 1, len(bareDCPipelines[0].Images); e != a {
		t.Errorf("expected %v, got %v", e, a)
	}
}
Exemple #3
0
func TestMissingReadinessProbe(t *testing.T) {
	g, _, err := osgraphtest.BuildGraph("../../../api/graph/test/unpushable-build-2.yaml")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	buildedges.AddAllInputOutputEdges(g)
	deployedges.AddAllTriggerEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)

	markers := FindDeploymentConfigReadinessWarnings(g, osgraph.DefaultNamer, "command probe")
	if e, a := 1, len(markers); e != a {
		t.Fatalf("expected %v, got %v", e, a)
	}

	if got, expected := markers[0].Key, MissingReadinessProbeWarning; got != expected {
		t.Fatalf("expected marker key %q, got %q", expected, got)
	}
}
Exemple #4
0
func TestMissingImageStreamTag(t *testing.T) {
	g, _, err := osgraphtest.BuildGraph("../../../api/graph/test/missing-istag.yaml")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	buildedges.AddAllInputOutputEdges(g)
	deployedges.AddAllTriggerEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)

	markers := FindDeploymentConfigTriggerErrors(g, osgraph.DefaultNamer)
	if e, a := 1, len(markers); e != a {
		t.Fatalf("expected %v, got %v", e, a)
	}

	if got, expected := markers[0].Key, MissingImageStreamTagWarning; got != expected {
		t.Fatalf("expected marker key %q, got %q", expected, got)
	}
}
Exemple #5
0
func TestSyntheticImageStreamTag(t *testing.T) {
	g, _, err := osgraphtest.BuildGraph("../../../api/graph/test/unpushable-build.yaml")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	buildedges.AddAllInputOutputEdges(g)
	deployedges.AddAllTriggerEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)

	markers := FindDeploymentConfigTriggerErrors(g)
	if e, a := 1, len(markers); e != a {
		t.Fatalf("expected %v, got %v", e, a)
	}

	if got, expected := markers[0].Key, ImageStreamTagNotAvailableInfo; got != expected {
		t.Fatalf("expected marker key %q, got %q", expected, got)
	}
}
Exemple #6
0
func (d *ProjectStatusDescriber) MakeGraph(namespace string) (osgraph.Graph, 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},
		&bcLoader{namespace: namespace, lister: d.C},
		&buildLoader{namespace: namespace, lister: d.C},
		&isLoader{namespace: namespace, lister: d.C},
		&dcLoader{namespace: namespace, lister: d.C},
	}
	loadingFuncs := []func() error{}
	for _, loader := range loaders {
		loadingFuncs = append(loadingFuncs, loader.Load)
	}

	if errs := parallel.Run(loadingFuncs...); len(errs) > 0 {
		return g, utilerrors.NewAggregate(errs)
	}

	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)
	buildedges.AddAllInputOutputEdges(g)
	buildedges.AddAllBuildEdges(g)
	deployedges.AddAllTriggerEdges(g)
	deployedges.AddAllDeploymentEdges(g)
	imageedges.AddAllImageStreamRefEdges(g)

	return g, nil
}
Exemple #7
0
func (d *ProjectStatusDescriber) MakeGraph(namespace string) (osgraph.Graph, error) {
	g := osgraph.New()

	loadingFuncs := []GraphLoadingFunc{loadServices, loadBuildConfigs, loadImageStreams, loadDeploymentConfigs, loadBuilds, loadReplicationControllers}

	listingWaitGroup := sync.WaitGroup{}
	graphLock := sync.Mutex{}
	errorChannel := make(chan error, len(loadingFuncs))

	for _, loadingFunc := range loadingFuncs {
		listingWaitGroup.Add(1)
		go func(loadingFunc GraphLoadingFunc) {
			defer listingWaitGroup.Done()
			if err := loadingFunc(g, graphLock, namespace, d.K, d.C); err != nil {
				errorChannel <- err
			}
		}(loadingFunc)
	}
	listingWaitGroup.Wait()
	close(errorChannel)

	// if we had an error.  Aggregate them and return them
	errlist := []error{}
	for err := range errorChannel {
		errlist = append(errlist, err)
	}
	if len(errlist) > 0 {
		return g, utilerrors.NewAggregate(errlist)
	}

	kubeedges.AddAllExposedPodTemplateSpecEdges(g)
	buildedges.AddAllInputOutputEdges(g)
	buildedges.AddAllBuildEdges(g)
	deployedges.AddAllTriggerEdges(g)
	deployedges.AddAllDeploymentEdges(g)

	imageedges.AddAllImageStreamRefEdges(g)

	return g, nil
}
Exemple #8
0
func TestGraph(t *testing.T) {
	g := osgraph.New()
	now := time.Now()
	builds := []buildapi.Build{
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-1-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-10 * time.Second)),
			},
			Status: buildapi.BuildStatusFailed,
		},
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-2-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-5 * time.Second)),
			},
			Status: buildapi.BuildStatusComplete,
		},
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-3-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-15 * time.Second)),
			},
			Status: buildapi.BuildStatusPending,
		},
	}

	bc1Node := buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "build1"},
		Triggers: []buildapi.BuildTriggerPolicy{
			{
				ImageChange: &buildapi.ImageChangeTrigger{},
			},
		},
		Parameters: buildapi.BuildParameters{
			Strategy: buildapi.BuildStrategy{
				Type: buildapi.SourceBuildStrategyType,
				SourceStrategy: &buildapi.SourceBuildStrategy{
					From: kapi.ObjectReference{Kind: "ImageStreamTag", Name: "test:base-image"},
				},
			},
			Output: buildapi.BuildOutput{
				To:  &kapi.ObjectReference{Name: "other"},
				Tag: "tag1",
			},
		},
	})
	buildedges.JoinBuilds(bc1Node, builds)
	bcTestNode := buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "test"},
		Parameters: buildapi.BuildParameters{
			Output: buildapi.BuildOutput{
				To:  &kapi.ObjectReference{Name: "other"},
				Tag: "base-image",
			},
		},
	})
	buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "build2"},
		Parameters: buildapi.BuildParameters{
			Output: buildapi.BuildOutput{
				DockerImageReference: "mycustom/repo/image",
				Tag:                  "tag2",
			},
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc-is-ignored"},
		Spec: kapi.ServiceSpec{
			Selector: nil,
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc1"},
		Spec: kapi.ServiceSpec{
			Selector: map[string]string{
				"deploymentconfig": "deploy1",
			},
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc2"},
		Spec: kapi.ServiceSpec{
			Selector: map[string]string{
				"deploymentconfig": "deploy1",
				"env":              "prod",
			},
		},
	})
	deploygraph.EnsureDeploymentConfigNode(g, &deployapi.DeploymentConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "other", Name: "deploy1"},
		Triggers: []deployapi.DeploymentTriggerPolicy{
			{
				ImageChangeParams: &deployapi.DeploymentTriggerImageChangeParams{
					From:           kapi.ObjectReference{Namespace: "default", Name: "other"},
					ContainerNames: []string{"1", "2"},
					Tag:            "tag1",
				},
			},
		},
		Template: deployapi.DeploymentTemplate{
			ControllerTemplate: kapi.ReplicationControllerSpec{
				Template: &kapi.PodTemplateSpec{
					ObjectMeta: kapi.ObjectMeta{
						Labels: map[string]string{
							"deploymentconfig": "deploy1",
							"env":              "prod",
						},
					},
					Spec: kapi.PodSpec{
						Containers: []kapi.Container{
							{
								Name:  "1",
								Image: "mycustom/repo/image",
							},
							{
								Name:  "2",
								Image: "mycustom/repo/image2",
							},
							{
								Name:  "3",
								Image: "mycustom/repo/image3",
							},
						},
					},
				},
			},
		},
	})
	deploygraph.EnsureDeploymentConfigNode(g, &deployapi.DeploymentConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "deploy2"},
		Template: deployapi.DeploymentTemplate{
			ControllerTemplate: kapi.ReplicationControllerSpec{
				Template: &kapi.PodTemplateSpec{
					ObjectMeta: kapi.ObjectMeta{
						Labels: map[string]string{
							"deploymentconfig": "deploy2",
							"env":              "dev",
						},
					},
					Spec: kapi.PodSpec{
						Containers: []kapi.Container{
							{
								Name:  "1",
								Image: "someother/image:v1",
							},
						},
					},
				},
			},
		},
	})

	kubeedges.AddAllExposedPodTemplateSpecEdges(g)
	buildedges.AddAllInputOutputEdges(g)
	deployedges.AddAllTriggerEdges(g)

	t.Log(g)

	ir, dc, bc, other := 0, 0, 0, 0
	for _, node := range g.NodeList() {
		switch g.Object(node).(type) {
		case *deployapi.DeploymentConfig:
			if g.Kind(node) != deploygraph.DeploymentConfigNodeKind {
				t.Fatalf("unexpected kind: %v", g.Kind(node))
			}
			dc++
		case *buildapi.BuildConfig:
			if g.Kind(node) != buildgraph.BuildConfigNodeKind {
				t.Fatalf("unexpected kind: %v", g.Kind(node))
			}
			bc++
		case *imageapi.ImageStream:
			// TODO resolve this check for 2 kinds, since both have the same object type
			if g.Kind(node) != imagegraph.ImageStreamNodeKind && g.Kind(node) != imagegraph.ImageStreamTagNodeKind {
				t.Fatalf("unexpected kind: %v", g.Kind(node))
			}
			ir++
		default:
			other++
		}
	}

	if dc != 2 || bc != 3 || ir != 3 || other != 12 {
		t.Errorf("unexpected nodes: %d %d %d %d", dc, bc, ir, other)
	}
	for _, edge := range g.EdgeList() {
		if g.EdgeKind(edge) == osgraph.UnknownEdgeKind {
			t.Errorf("edge reported unknown kind: %#v", edge)
		}
	}

	// imagestreamtag default/other:base-image
	istID := 0
	for _, node := range g.NodeList() {
		if g.Name(node) == "<imagestreamtag default/other:base-image>" {
			istID = node.ID()
			break
		}
	}

	edge := g.EdgeBetween(concrete.Node(bcTestNode.ID()), concrete.Node(istID))
	if edge == nil {
		t.Fatalf("failed to find edge between %d and %d", bcTestNode.ID(), istID)
	}
	if len(g.SubgraphWithNodes([]graph.Node{edge.Head(), edge.Tail()}, osgraph.ExistingDirectEdge).EdgeList()) != 1 {
		t.Fatalf("expected one edge")
	}
	if len(g.SubgraphWithNodes([]graph.Node{edge.Tail(), edge.Head()}, osgraph.ExistingDirectEdge).EdgeList()) != 1 {
		t.Fatalf("expected one edge")
	}

	if e := g.EdgeBetween(concrete.Node(bcTestNode.ID()), concrete.Node(istID)); e == nil {
		t.Errorf("expected edge for %d-%d", bcTestNode.ID(), istID)
	}
	if e := g.EdgeBetween(concrete.Node(istID), concrete.Node(bcTestNode.ID())); e == nil {
		t.Errorf("expected edge for %d-%d", bcTestNode.ID(), istID)
	}

	pipelines, covered := DeploymentPipelines(g)
	if len(pipelines) != 2 {
		t.Fatalf("unexpected pipelines: %#v", pipelines)
	}
	if len(covered) != 7 {
		t.Fatalf("unexpected covered nodes: %#v", covered)
	}
	for from, images := range pipelines {
		t.Logf("from %s", from.Name)
		for _, path := range images {
			t.Logf("  %v", path)
		}
	}

	serviceGroups := ServiceAndDeploymentGroups(g)
	if len(serviceGroups) != 5 {
		t.Errorf("unexpected service groups: %#v", serviceGroups)
	}
	if len(serviceGroups[3].Builds) != 1 {
		t.Fatalf("unexpected final group: %#v", serviceGroups[2])
	}
	for _, group := range serviceGroups {
		dcs := len(group.Deployments)
		svcs := len(group.Services)
		for _, svc := range group.Services {
			t.Logf("service %s", svc.Service.Name)
		}
		indent := ""
		if svcs > 0 {
			indent = "  "
		}
		for _, deployment := range group.Deployments {
			t.Logf("%sdeployment %s", indent, deployment.Deployment.Name)
			for _, image := range deployment.Images {
				t.Logf("%s  image %s", indent, image.Image.ImageSpec())
				if image.Build != nil {
					if image.Build.LastSuccessfulBuild != nil {
						t.Logf("%s    built at %s", indent, image.Build.LastSuccessfulBuild.CreationTimestamp)
					} else if image.Build.LastUnsuccessfulBuild != nil {
						t.Logf("%s    build %s at %s", indent, image.Build.LastUnsuccessfulBuild.Status, image.Build.LastSuccessfulBuild.CreationTimestamp)
					}
					for _, b := range image.Build.ActiveBuilds {
						t.Logf("%s    build %s %s", indent, b.Name, b.Status)
					}
				}
			}
		}
		if dcs != 0 || svcs != 0 {
			continue
		}
		for _, build := range group.Builds {
			if build.Image != nil {
				if build.Build != nil {
					t.Logf("%s <- build %s (%d)", build.Image.ImageSpec(), build.Build.Name, build.Image.ID())
				} else {
					t.Logf("%s (%d)", build.Image.ImageSpec(), build.Image.ID())
				}
			} else {
				t.Logf("build %s (%d)", build.Build.Name, build.Build.ID())
				t.Errorf("expected build %d to have an image edge", build.Build.ID())
			}
		}
	}
}
func TestGraph(t *testing.T) {
	g := osgraph.New()
	now := time.Now()
	builds := []buildapi.Build{
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-1-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-10 * time.Second)),
			},
			Status: buildapi.BuildStatus{
				Phase: buildapi.BuildPhaseFailed,
			},
		},
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-2-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-5 * time.Second)),
			},
			Status: buildapi.BuildStatus{
				Phase: buildapi.BuildPhaseComplete,
			},
		},
		{
			ObjectMeta: kapi.ObjectMeta{
				Name:              "build1-3-abc",
				Labels:            map[string]string{buildapi.BuildConfigLabel: "build1"},
				CreationTimestamp: util.NewTime(now.Add(-15 * time.Second)),
			},
			Status: buildapi.BuildStatus{
				Phase: buildapi.BuildPhasePending,
			},
		},
	}
	for i := range builds {
		buildgraph.EnsureBuildNode(g, &builds[i])
	}

	buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "build1"},
		Spec: buildapi.BuildConfigSpec{
			Triggers: []buildapi.BuildTriggerPolicy{
				{
					ImageChange: &buildapi.ImageChangeTrigger{},
				},
			},
			BuildSpec: buildapi.BuildSpec{
				Strategy: buildapi.BuildStrategy{
					Type: buildapi.SourceBuildStrategyType,
					SourceStrategy: &buildapi.SourceBuildStrategy{
						From: kapi.ObjectReference{Kind: "ImageStreamTag", Name: "test:base-image"},
					},
				},
				Output: buildapi.BuildOutput{
					To: &kapi.ObjectReference{Kind: "ImageStreamTag", Name: "other:tag1"},
				},
			},
		},
	})
	bcTestNode := buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "test"},
		Spec: buildapi.BuildConfigSpec{
			BuildSpec: buildapi.BuildSpec{
				Output: buildapi.BuildOutput{
					To: &kapi.ObjectReference{Kind: "ImageStreamTag", Name: "other:base-image"},
				},
			},
		},
	})
	buildgraph.EnsureBuildConfigNode(g, &buildapi.BuildConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "build2"},
		Spec: buildapi.BuildConfigSpec{
			BuildSpec: buildapi.BuildSpec{
				Output: buildapi.BuildOutput{
					To: &kapi.ObjectReference{Kind: "DockerImage", Name: "mycustom/repo/image:tag2"},
				},
			},
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc-is-ignored"},
		Spec: kapi.ServiceSpec{
			Selector: nil,
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc1"},
		Spec: kapi.ServiceSpec{
			Selector: map[string]string{
				"deploymentconfig": "deploy1",
			},
		},
	})
	kubegraph.EnsureServiceNode(g, &kapi.Service{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "svc2"},
		Spec: kapi.ServiceSpec{
			Selector: map[string]string{
				"deploymentconfig": "deploy1",
				"env":              "prod",
			},
		},
	})
	deploygraph.EnsureDeploymentConfigNode(g, &deployapi.DeploymentConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "other", Name: "deploy1"},
		Triggers: []deployapi.DeploymentTriggerPolicy{
			{
				ImageChangeParams: &deployapi.DeploymentTriggerImageChangeParams{
					From:           kapi.ObjectReference{Namespace: "default", Name: "other"},
					ContainerNames: []string{"1", "2"},
					Tag:            "tag1",
				},
			},
		},
		Template: deployapi.DeploymentTemplate{
			ControllerTemplate: kapi.ReplicationControllerSpec{
				Template: &kapi.PodTemplateSpec{
					ObjectMeta: kapi.ObjectMeta{
						Labels: map[string]string{
							"deploymentconfig": "deploy1",
							"env":              "prod",
						},
					},
					Spec: kapi.PodSpec{
						Containers: []kapi.Container{
							{
								Name:  "1",
								Image: "mycustom/repo/image",
							},
							{
								Name:  "2",
								Image: "mycustom/repo/image2",
							},
							{
								Name:  "3",
								Image: "mycustom/repo/image3",
							},
						},
					},
				},
			},
		},
	})
	deploygraph.EnsureDeploymentConfigNode(g, &deployapi.DeploymentConfig{
		ObjectMeta: kapi.ObjectMeta{Namespace: "default", Name: "deploy2"},
		Template: deployapi.DeploymentTemplate{
			ControllerTemplate: kapi.ReplicationControllerSpec{
				Template: &kapi.PodTemplateSpec{
					ObjectMeta: kapi.ObjectMeta{
						Labels: map[string]string{
							"deploymentconfig": "deploy2",
							"env":              "dev",
						},
					},
					Spec: kapi.PodSpec{
						Containers: []kapi.Container{
							{
								Name:  "1",
								Image: "someother/image:v1",
							},
						},
					},
				},
			},
		},
	})

	kubeedges.AddAllExposedPodTemplateSpecEdges(g)
	buildedges.AddAllInputOutputEdges(g)
	buildedges.AddAllBuildEdges(g)
	deployedges.AddAllTriggerEdges(g)
	deployedges.AddAllDeploymentEdges(g)

	t.Log(g)

	for _, edge := range g.Edges() {
		if g.EdgeKinds(edge).Has(osgraph.UnknownEdgeKind) {
			t.Errorf("edge reported unknown kind: %#v", edge)
		}
	}

	// imagestreamtag default/other:base-image
	istID := 0
	for _, node := range g.Nodes() {
		if g.Name(node) == "ImageStreamTag|default/other:base-image" {
			istID = node.ID()
			break
		}
	}

	edge := g.Edge(concrete.Node(bcTestNode.ID()), concrete.Node(istID))
	if edge == nil {
		t.Fatalf("failed to find edge between %d and %d", bcTestNode.ID(), istID)
	}
	if len(g.SubgraphWithNodes([]graph.Node{edge.From(), edge.To()}, osgraph.ExistingDirectEdge).Edges()) != 1 {
		t.Fatalf("expected one edge")
	}
	if len(g.SubgraphWithNodes([]graph.Node{edge.To(), edge.From()}, osgraph.ExistingDirectEdge).Edges()) != 1 {
		t.Fatalf("expected one edge")
	}

	if e := g.Edge(concrete.Node(bcTestNode.ID()), concrete.Node(istID)); e == nil {
		t.Errorf("expected edge for %d-%d", bcTestNode.ID(), istID)
	}

	coveredNodes := IntSet{}

	serviceGroups, coveredByServiceGroups := AllServiceGroups(g, coveredNodes)
	coveredNodes.Insert(coveredByServiceGroups.List()...)

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

	if len(bareDCPipelines) != 1 {
		t.Fatalf("unexpected pipelines: %#v", bareDCPipelines)
	}
	if len(coveredNodes) != 10 {
		t.Fatalf("unexpected covered nodes: %#v", coveredNodes)
	}

	for _, bareDCPipeline := range bareDCPipelines {
		t.Logf("from %s", bareDCPipeline.Deployment.Name)
		for _, path := range bareDCPipeline.Images {
			t.Logf("  %v", path)
		}
	}

	if len(serviceGroups) != 3 {
		t.Errorf("unexpected service groups: %#v", serviceGroups)
	}
	for _, serviceGroup := range serviceGroups {
		t.Logf("service %s", serviceGroup.Service.Name)
		indent := "  "

		for _, deployment := range serviceGroup.DeploymentConfigPipelines {
			t.Logf("%sdeployment %s", indent, deployment.Deployment.Name)
			for _, image := range deployment.Images {
				t.Logf("%s  image %s", indent, image.Image.ImageSpec())
				if image.Build != nil {
					if image.LastSuccessfulBuild != nil {
						t.Logf("%s    built at %s", indent, image.LastSuccessfulBuild.Build.CreationTimestamp)
					} else if image.LastUnsuccessfulBuild != nil {
						t.Logf("%s    build %s at %s", indent, image.LastUnsuccessfulBuild.Build.Status, image.LastUnsuccessfulBuild.Build.CreationTimestamp)
					}
					for _, b := range image.ActiveBuilds {
						t.Logf("%s    build %s %s", indent, b.Build.Name, b.Build.Status)
					}
				}
			}
		}
	}
}