Skip to content

Commit

Permalink
Add oadm top command for analyzing image and imagestream usage.
Browse files Browse the repository at this point in the history
  • Loading branch information
soltysh committed Jul 27, 2016
1 parent 73ecc72 commit a39d68d
Show file tree
Hide file tree
Showing 8 changed files with 1,369 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/cmd/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/openshift/origin/pkg/cmd/admin/prune"
"github.com/openshift/origin/pkg/cmd/admin/registry"
"github.com/openshift/origin/pkg/cmd/admin/router"
"github.com/openshift/origin/pkg/cmd/admin/top"
"github.com/openshift/origin/pkg/cmd/cli/cmd"
"github.com/openshift/origin/pkg/cmd/experimental/buildchain"
exipfailover "github.com/openshift/origin/pkg/cmd/experimental/ipfailover"
Expand Down Expand Up @@ -82,6 +83,7 @@ func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *co
diagnostics.NewCmdDiagnostics(diagnostics.DiagnosticsRecommendedName, fullName+" "+diagnostics.DiagnosticsRecommendedName, out),
prune.NewCommandPrune(prune.PruneRecommendedName, fullName+" "+prune.PruneRecommendedName, f, out),
buildchain.NewCmdBuildChain(name, fullName+" "+buildchain.BuildChainRecommendedCommandName, f, out),
top.NewCommandTop(top.TopRecommendedName, fullName+" "+top.TopRecommendedName, f, out),
},
},
{
Expand Down
149 changes: 149 additions & 0 deletions pkg/cmd/admin/top/graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package top

import (
"github.com/golang/glog"
gonum "github.com/gonum/graph"

kapi "k8s.io/kubernetes/pkg/api"

"github.com/openshift/origin/pkg/api/graph"
kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
imageapi "github.com/openshift/origin/pkg/image/api"
imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
)

const (
ImageLayerEdgeKind = "ImageLayer"
ImageTopLayerEdgeKind = "ImageTopLayer"
ImageStreamEdgeKind = "ImageStream"
HistoricImageStreamEdgeKind = "HistoricImageStream"
PodImageEdgeKind = "PodImage"
ParentImageEdgeKind = "ParentImage"

// this is empty layer check sum from schema1
emptyLayerSHA256 = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
)

func getImageNodes(nodes []gonum.Node) []*imagegraph.ImageNode {
ret := []*imagegraph.ImageNode{}
for i := range nodes {
if node, ok := nodes[i].(*imagegraph.ImageNode); ok {
ret = append(ret, node)
}
}
return ret
}

func addImagesToGraph(g graph.Graph, images *imageapi.ImageList) {
for i := range images.Items {
image := &images.Items[i]

glog.V(4).Infof("Adding image %q to graph", image.Name)
imageNode := imagegraph.EnsureImageNode(g, image)

topLayerAdded := false
// we're looking through layers in reversed order since we need to
// find first layer (from top) which is not an empty layer, we're omitting
// empty layers because every image has those and they're giving us
// false positives about parents
for i := len(image.DockerImageLayers) - 1; i >= 0; i-- {
layer := image.DockerImageLayers[i]
layerNode := imagegraph.EnsureImageLayerNode(g, layer.Name)
edgeKind := ImageLayerEdgeKind
if !topLayerAdded && layer.Name != emptyLayerSHA256 {
edgeKind = ImageTopLayerEdgeKind
topLayerAdded = true
}
g.AddEdge(imageNode, layerNode, edgeKind)
glog.V(4).Infof("Adding image layer %q to graph (%q) ", layer.Name, edgeKind)
}
}
}

func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList) {
for i := range streams.Items {
stream := &streams.Items[i]
glog.V(4).Infof("Adding ImageStream %s/%s to graph", stream.Namespace, stream.Name)
isNode := imagegraph.EnsureImageStreamNode(g, stream)
imageStreamNode := isNode.(*imagegraph.ImageStreamNode)

// connect IS with underlying images
for tag, history := range stream.Status.Tags {
for i := range history.Items {
image := history.Items[i]
n := imagegraph.FindImage(g, image.Image)
if n == nil {
glog.V(2).Infof("Unable to find image %q in graph (from tag=%q, dockerImageReference=%s)",
history.Items[i].Image, tag, image.DockerImageReference)
continue
}
imageNode := n.(*imagegraph.ImageNode)
glog.V(4).Infof("Adding edge from %q to %q", imageStreamNode.UniqueName(), imageNode.UniqueName())
edgeKind := ImageStreamEdgeKind
if i > 1 {
edgeKind = HistoricImageStreamEdgeKind
}
g.AddEdge(imageStreamNode, imageNode, edgeKind)
}
}
}
}

func addPodsToGraph(g graph.Graph, pods *kapi.PodList) {
for i := range pods.Items {
pod := &pods.Items[i]
if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending {
glog.V(4).Infof("Pod %s/%s is not running nor pending - skipping", pod.Namespace, pod.Name)
continue
}

glog.V(4).Infof("Adding pod %s/%s to graph", pod.Namespace, pod.Name)
podNode := kubegraph.EnsurePodNode(g, pod)
addPodSpecToGraph(g, &pod.Spec, podNode)
}
}

func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node) {
for j := range spec.Containers {
container := spec.Containers[j]

glog.V(4).Infof("Examining container image %q", container.Image)
ref, err := imageapi.ParseDockerImageReference(container.Image)
if err != nil {
glog.V(2).Infof("Unable to parse DockerImageReference %q: %v - skipping", container.Image, err)
continue
}

if len(ref.ID) == 0 {
// ignore not managed images
continue
}

imageNode := imagegraph.FindImage(g, ref.ID)
if imageNode == nil {
glog.V(1).Infof("Unable to find image %q in the graph", ref.ID)
continue
}

glog.V(4).Infof("Adding edge from %v to %v", predecessor, imageNode)
g.AddEdge(predecessor, imageNode, PodImageEdgeKind)
}
}

func markParentsInGraph(g graph.Graph) {
imageNodes := getImageNodes(g.Nodes())
for _, in := range imageNodes {
// find image's top layer, should be just one
for _, e := range g.OutboundEdges(in, ImageTopLayerEdgeKind) {
layerNode, _ := e.To().(*imagegraph.ImageLayerNode)
// find image's containing this layer but not being their top layer
for _, ed := range g.InboundEdges(layerNode, ImageLayerEdgeKind) {
childNode, _ := ed.From().(*imagegraph.ImageNode)
g.AddEdge(in, childNode, ParentImageEdgeKind)
}
// TODO:
// find image's containing THIS layer being their top layer,
// this happens when image contents is not being changed
}
}
}
225 changes: 225 additions & 0 deletions pkg/cmd/admin/top/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package top

import (
"fmt"
"io"

"github.com/spf13/cobra"

kapi "k8s.io/kubernetes/pkg/api"
kclient "k8s.io/kubernetes/pkg/client/unversioned"
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"

"github.com/openshift/origin/pkg/api/graph"
kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
buildapi "github.com/openshift/origin/pkg/build/api"
"github.com/openshift/origin/pkg/client"
"github.com/openshift/origin/pkg/cmd/util/clientcmd"
deployapi "github.com/openshift/origin/pkg/deploy/api"
imageapi "github.com/openshift/origin/pkg/image/api"
imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
)

const (
TopImagesRecommendedName = "images"

topImagesLong = `Show usage statistics for Images
This command analyzes all the Images managed by the platform and presents current
usage statistics.`

topImagesExample = ` # Show usage statistics for Images
%[1]s %[2]s`
)

// NewCmdTopImages implements the OpenShift cli top images command.
func NewCmdTopImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
opts := &TopImagesOptions{}
cmd := &cobra.Command{
Use: name,
Short: "Show usage statistics for Images",
Long: topImagesLong,
Example: fmt.Sprintf(topImagesExample, parentName, name),
Run: func(cmd *cobra.Command, args []string) {
kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
kcmdutil.CheckErr(opts.Validate(cmd))
kcmdutil.CheckErr(opts.Run())
},
}

return cmd
}

type TopImagesOptions struct {
// internal values
Images *imageapi.ImageList
Streams *imageapi.ImageStreamList
Pods *kapi.PodList

// helpers
out io.Writer
osClient client.Interface
kClient kclient.Interface
}

// Complete turns a partially defined TopImagesOptions into a solvent structure
// which can be validated and used for showing limits usage.
func (o *TopImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
osClient, kClient, err := f.Clients()
if err != nil {
return err
}
namespace := cmd.Flag("namespace").Value.String()
if len(namespace) == 0 {
namespace = kapi.NamespaceAll
}
o.out = out

allImages, err := osClient.Images().List(kapi.ListOptions{})
if err != nil {
return err
}
o.Images = allImages

allStreams, err := osClient.ImageStreams(namespace).List(kapi.ListOptions{})
if err != nil {
return err
}
o.Streams = allStreams

allPods, err := kClient.Pods(namespace).List(kapi.ListOptions{})
if err != nil {
return err
}
o.Pods = allPods

return nil
}

// Validate ensures that a TopImagesOptions is valid and can be used to execute command.
func (o TopImagesOptions) Validate(cmd *cobra.Command) error {
return nil
}

// Run contains all the necessary functionality to show current image references.
func (o TopImagesOptions) Run() error {
infos := o.imagesTop()
Print(o.out, ImageColumns, infos)
return nil
}

var ImageColumns = []string{"NAME", "IMAGESTREAMTAG", "PARENTS", "USAGE", "METADATA", "STORAGE"}

// imageInfo contains statistic information about Image usage.
type imageInfo struct {
Image string
ImageStreamTag string
Parents []string
Usage []string
Metadata bool
Storage int64
}

var _ Info = &imageInfo{}

func (i imageInfo) PrintLine(out io.Writer) {
printValue(out, i.Image)
printValue(out, i.ImageStreamTag)
printArray(out, i.Parents)
printArray(out, i.Usage)
printBool(out, i.Metadata)
printSize(out, i.Storage)
}

// imagesTop generates Image information from a graph and returns this as a list
// of imageInfo array.
func (o TopImagesOptions) imagesTop() []Info {
g := graph.New()
addImagesToGraph(g, o.Images)
addImageStreamsToGraph(g, o.Streams)
addPodsToGraph(g, o.Pods)
markParentsInGraph(g)

infos := []Info{}
imageNodes := getImageNodes(g.Nodes())
for _, in := range imageNodes {
image := in.Image
istag := getImageStreamTag(g, in)
parents := getImageParents(g, in)
usage := getImageUsage(g, in)
metadata := len(image.DockerImageManifest) != 0 && len(image.DockerImageLayers) != 0
infos = append(infos, imageInfo{
Image: image.Name,
ImageStreamTag: istag,
Parents: parents,
Usage: usage,
Metadata: metadata,
Storage: image.DockerImageMetadata.Size,
})
}

return infos
}

func getImageStreamTag(g graph.Graph, node *imagegraph.ImageNode) string {
for _, e := range g.InboundEdges(node, ImageStreamEdgeKind) {
streamNode, ok := e.From().(*imagegraph.ImageStreamNode)
if !ok {
continue
}
return getTag(streamNode.ImageStream, node.Image)
}
return ""
}

func getTag(stream *imageapi.ImageStream, image *imageapi.Image) string {
for tag, history := range stream.Status.Tags {
if history.Items[0].Image == image.Name {
return fmt.Sprintf("%s/%s:%s", stream.Namespace, stream.Name, tag)
}
}
return ""
}

func getImageParents(g graph.Graph, node *imagegraph.ImageNode) []string {
parents := []string{}
for _, e := range g.InboundEdges(node, ParentImageEdgeKind) {
imageNode, ok := e.From().(*imagegraph.ImageNode)
if !ok {
continue
}
parents = append(parents, imageNode.Image.Name)
}
return parents
}

func getImageUsage(g graph.Graph, node *imagegraph.ImageNode) []string {
usage := []string{}
for _, e := range g.InboundEdges(node, PodImageEdgeKind) {
podNode, ok := e.From().(*kubegraph.PodNode)
if !ok {
continue
}
usage = append(usage, getController(podNode.Pod))
}
return usage
}

func getController(pod *kapi.Pod) string {
controller := "<unknown>"
if pod.Annotations == nil {
return controller
}

if bc, ok := pod.Annotations[buildapi.BuildAnnotation]; ok {
return fmt.Sprintf("Build: %s/%s", pod.Namespace, bc)
}
if dc, ok := pod.Annotations[deployapi.DeploymentAnnotation]; ok {
return fmt.Sprintf("Deployment: %s/%s", pod.Namespace, dc)
}
if dc, ok := pod.Annotations[deployapi.DeploymentPodAnnotation]; ok {
return fmt.Sprintf("Deployer: %s/%s", pod.Namespace, dc)
}

return controller
}
Loading

0 comments on commit a39d68d

Please sign in to comment.