-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add oadm top command for analyzing image and imagestream usage.
- Loading branch information
Showing
8 changed files
with
1,369 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.