diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index f7ff4a5f8a53..6bbc8a83b9ea 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -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" @@ -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), }, }, { diff --git a/pkg/cmd/admin/top/image.go b/pkg/cmd/admin/top/image.go new file mode 100644 index 000000000000..564b386b6f2f --- /dev/null +++ b/pkg/cmd/admin/top/image.go @@ -0,0 +1,111 @@ +package top + +import ( + "fmt" + + kapi "k8s.io/kubernetes/pkg/api" + + "github.com/openshift/origin/pkg/api/graph" + kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes" + buildapi "github.com/openshift/origin/pkg/build/api" + 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" +) + +// ImageInfo contains statistic information about Image usage. +type ImageInfo struct { + Image string + ImageStreamTag string + Parents []string + Usage []string + Metadata bool + Storage int64 +} + +// ImageInfo generates Image information from a graph and returns this as a list +// of ImageInfo array. +func ImageTop(g graph.Graph) []ImageInfo { + infos := []ImageInfo{} + + imageNodes := getImageNodes(g.Nodes()) + for _, in := range imageNodes { + image := in.Image + istag := getImageStreamTag(g, in) + parents := getImageParents(g, in) + usage := getImageUsage(g, in) + infos = append(infos, ImageInfo{ + Image: image.Name, + ImageStreamTag: istag, + Parents: parents, + Usage: usage, + Metadata: len(image.DockerImageManifest) != 0, + 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 := "" + if pod.Annotations == nil { + return controller + } + + if bc, ok := pod.Annotations[buildapi.BuildAnnotation]; ok { + return fmt.Sprintf("Build: %s/%s", bc, pod.Namespace) + } + if dc, ok := pod.Annotations[deployapi.DeploymentAnnotation]; ok { + return fmt.Sprintf("Deployment: %s/%s", dc, pod.Namespace) + } + if dc, ok := pod.Annotations[deployapi.DeploymentPodAnnotation]; ok { + return fmt.Sprintf("Deployer: %s/%s", dc, pod.Namespace) + } + + return controller +} diff --git a/pkg/cmd/admin/top/imagestream.go b/pkg/cmd/admin/top/imagestream.go new file mode 100644 index 000000000000..b4d9e613bd91 --- /dev/null +++ b/pkg/cmd/admin/top/imagestream.go @@ -0,0 +1,64 @@ +package top + +import ( + gonum "github.com/gonum/graph" + + "github.com/openshift/origin/pkg/api/graph" + imageapi "github.com/openshift/origin/pkg/image/api" + imagegraph "github.com/openshift/origin/pkg/image/graph/nodes" +) + +// ImageStreamInfo contains contains statistic information about ImageStream usage. +type ImageStreamInfo struct { + ImageStream *imageapi.ImageStream + Storage int64 + Images int + Layers int +} + +// ImageStreamInfo generates ImageStream information from a graph and +// returns this as a list of ImageStreamInfo array. +func ImageStreamTop(g graph.Graph) []ImageStreamInfo { + infos := []ImageStreamInfo{} + + streamNodes := getImageStreamNodes(g.Nodes()) + for _, sn := range streamNodes { + storage, images, layers := getImageStreamSize(g, sn) + infos = append(infos, ImageStreamInfo{ + ImageStream: sn.ImageStream, + Storage: storage, + Images: images, + Layers: layers, + }) + } + + return infos +} + +func getImageStreamSize(g graph.Graph, node *imagegraph.ImageStreamNode) (int64, int, int) { + imageEdges := g.OutboundEdges(node, ImageStreamEdgeKind) + storage := int64(0) + images := len(imageEdges) + layers := 0 + for _, e := range imageEdges { + imageNode, ok := e.To().(*imagegraph.ImageNode) + if !ok { + continue + } + layerEdges := g.OutboundEdges(imageNode, ImageLayerEdgeKind, ImageTopLayerEdgeKind) + layers += len(layerEdges) + storage += imageNode.Image.DockerImageMetadata.Size + } + + return storage, images, layers +} + +func getImageStreamNodes(nodes []gonum.Node) []*imagegraph.ImageStreamNode { + ret := []*imagegraph.ImageStreamNode{} + for i := range nodes { + if node, ok := nodes[i].(*imagegraph.ImageStreamNode); ok { + ret = append(ret, node) + } + } + return ret +} diff --git a/pkg/cmd/admin/top/top.go b/pkg/cmd/admin/top/top.go new file mode 100644 index 000000000000..dd3f39747344 --- /dev/null +++ b/pkg/cmd/admin/top/top.go @@ -0,0 +1,282 @@ +package top + +import ( + "fmt" + "io" + + "github.com/golang/glog" + gonum "github.com/gonum/graph" + "github.com/spf13/cobra" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + 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" + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" + imageapi "github.com/openshift/origin/pkg/image/api" + imagegraph "github.com/openshift/origin/pkg/image/graph/nodes" +) + +const ( + TopRecommendedName = "top" + + ImageLayerEdgeKind = "ImageLayer" + ImageTopLayerEdgeKind = "ImageTopLayer" + ImageStreamEdgeKind = "ImageStream" + HistoricImageStreamEdgeKind = "HistoricImageStream" + PodImageEdgeKind = "PodImage" + ParentImageEdgeKind = "ParentImage" + + // this is empty layer check sum from schema1 + emptyLayerSHA256 = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + + topLong = ` +Show usage statistics for Images or ImageStreams + +This command analyzes all the images managed by the platform and presents current +usage statistics for Images or ImageStreams.` + + topExample = ` # Show usage statistics for Images. + %[1]s top image + + # Show usage statistics for ImageStreams. + %[1]s top imagestream` +) + +func NewCommandTop(name, fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { + opts := &TopOptions{} + cmd := &cobra.Command{ + Use: "top (image | imagestream)", + Short: "Show usage statistics for Images or ImageStreams.", + Long: topLong, + Example: fmt.Sprintf(topExample, fullName), + 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 TopOptions struct { + // internal values + Images *imageapi.ImageList + Streams *imageapi.ImageStreamList + Pods *kapi.PodList + Kind unversioned.GroupKind + + // helpers + out io.Writer + osClient client.Interface + kClient kclient.Interface +} + +// Complete turns a partially defined TopOptions into a solvent structure +// which can be validated and used for showing limits usage. +func (o *TopOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error { + if len(args) == 0 { + return kcmdutil.UsageError(cmd, "you must specify the type of resource to get (image|imagestream)") + } + + input := args[0] + mapper, _ := f.Object(false) + gvks, err := mapper.KindsFor(unversioned.GroupVersionResource{Group: imageapi.GroupName, Resource: input}) + if err != nil { + return err + } + o.Kind = gvks[0].GroupKind() + + 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 TopOptions is valid and can be used to execute command. +func (o TopOptions) Validate(cmd *cobra.Command) error { + switch o.Kind { + case imageapi.Kind("Image"), imageapi.Kind("ImageStream"): + break + default: + return fmt.Errorf("only image or imagestream is allowed") + } + + return nil +} + +// Run contains all the necessary functionality to show current image references. +func (o TopOptions) Run() error { + g := graph.New() + addImagesToGraph(g, o.Images) + addImageStreamsToGraph(g, o.Streams) + addPodsToGraph(g, o.Pods) + markParentsInGraph(g) + + switch o.Kind { + case imageapi.Kind("Image"): + infos := ImageTop(g) + for _, info := range infos { + fmt.Fprintf(o.out, "%s %s %#v %#v %v %v\n", info.Image, info.ImageStreamTag, info.Parents, info.Usage, info.Metadata, info.Storage) + } + case imageapi.Kind("ImageStream"): + infos := ImageStreamTop(g) + for _, info := range infos { + fmt.Fprintf(o.out, "%s/%s %v %v %v\n", info.ImageStream.Namespace, info.ImageStream.Name, info.Storage, info.Images, info.Layers) + } + } + + return nil +} + +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 + for _, layer := range image.DockerImageLayers { + layerNode := imagegraph.EnsureImageLayerNode(g, layer.Name) + edgeKind := ImageLayerEdgeKind + // this is very hacky, because we're looking for first layer that is + // not emptyLayer in schema1 and mark it as top layer, schema2 does + // not have such problem + 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 + } + } +}