talos/pkg/cli/formatters.go
Andrey Smirnov 8f3e1a4ad6
fix: drop unpacked layers from containerd image store
See https://github.com/containerd/cri/pull/1543

Fixes #4274

Fix is applied on two levels:

* for Talos-initiated pulls, update API call
* for Kubernetes-initiated pulls, update CRI plugin config

Comparison of `/var` usage before/after, as reported by
`talosctl mounts` (in GiB):

|              | before | after |
|--------------|:------:|------:|
| controlplane |  1.98  |  1.74 |
| worker       |  1.17  |  1.01 |

It's hard to measure effect on pulls to system containerd, like
`installer` image, as it's ephemeral, but it should also reduce space
usage in `tmpfs`.

Also fixes output of `talosctl mounts`.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
2021-12-06 20:41:48 +03:00

276 lines
7.9 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package cli
import (
"context"
"fmt"
"io"
"math"
"strings"
"text/tabwriter"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/emicklei/dot"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"github.com/talos-systems/talos/pkg/machinery/api/inspect"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
)
// RenderMounts renders mounts output.
func RenderMounts(resp *machine.MountsResponse, output io.Writer, remotePeer *peer.Peer) error {
w := tabwriter.NewWriter(output, 0, 0, 3, ' ', 0)
parts := []string{"FILESYSTEM", "SIZE(GB)", "USED(GB)", "AVAILABLE(GB)", "PERCENT USED", "MOUNTED ON"}
var defaultNode string
if remotePeer != nil {
parts = append([]string{"NODE"}, parts...)
defaultNode = client.AddrFromPeer(remotePeer)
}
fmt.Fprintln(w, strings.Join(parts, "\t"))
for _, msg := range resp.Messages {
for _, r := range msg.Stats {
percentAvailable := 100.0 - 100.0*(float64(r.Available)/float64(r.Size))
if math.IsNaN(percentAvailable) {
continue
}
node := defaultNode
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}
format := "%s\t%.02f\t%.02f\t%.02f\t%.02f%%\t%s\n"
args := []interface{}{r.Filesystem, float64(r.Size) * 1e-9, float64(r.Size-r.Available) * 1e-9, float64(r.Available) * 1e-9, percentAvailable, r.MountedOn}
if defaultNode != "" {
format = "%s\t" + format
args = append([]interface{}{node}, args...)
}
fmt.Fprintf(w, format, args...)
}
}
return w.Flush()
}
// RenderGraph renders inspect controller runtime graph.
//nolint:gocyclo,cyclop
func RenderGraph(ctx context.Context, c *client.Client, resp *inspect.ControllerRuntimeDependenciesResponse, output io.Writer, withResources bool) error {
graph := dot.NewGraph(dot.Directed)
resourceTypeID := func(edge *inspect.ControllerDependencyEdge) string {
return edge.GetResourceType()
}
resourceID := func(r resource.Resource) string {
return fmt.Sprintf("%s/%s/%s", r.Metadata().Namespace(), r.Metadata().Type(), r.Metadata().ID())
}
if withResources {
resources := map[string][]resource.Resource{}
for _, msg := range resp.GetMessages() {
for _, edge := range msg.GetEdges() {
resourceType := resourceTypeID(edge)
if _, ok := resources[resourceType]; ok {
continue
}
listClient, err := c.Resources.List(ctx, edge.GetResourceNamespace(), edge.GetResourceType())
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for {
resp, err := listClient.Recv()
if err != nil {
if err == io.EOF || client.StatusCode(err) == codes.Canceled {
break
}
return fmt.Errorf("error listing resources: %w", err)
}
if resp.Resource != nil {
resources[resourceType] = append(resources[resourceType], resp.Resource)
}
}
}
}
for _, msg := range resp.GetMessages() {
for _, edge := range msg.GetEdges() {
graph.Node(edge.ControllerName).Box()
}
}
for resourceType, resourceList := range resources {
cluster := graph.Subgraph(resourceType, dot.ClusterOption{})
for _, resource := range resourceList {
cluster.Node(resourceID(resource)).
Attr("shape", "note").
Attr("fillcolor", "azure2").
Attr("style", "filled")
}
}
for _, msg := range resp.GetMessages() {
for _, edge := range msg.GetEdges() {
for _, resource := range resources[resourceTypeID(edge)] {
if edge.GetResourceId() != "" && resource.Metadata().ID() != edge.GetResourceId() {
continue
}
if (edge.GetEdgeType() == inspect.DependencyEdgeType_OUTPUT_EXCLUSIVE ||
edge.GetEdgeType() == inspect.DependencyEdgeType_OUTPUT_SHARED) &&
edge.GetControllerName() != resource.Metadata().Owner() {
continue
}
switch edge.GetEdgeType() {
case inspect.DependencyEdgeType_OUTPUT_EXCLUSIVE:
graph.Edge(graph.Node(edge.ControllerName), graph.Subgraph(resourceTypeID(edge)).Node(resourceID(resource))).Solid()
case inspect.DependencyEdgeType_OUTPUT_SHARED:
graph.Edge(graph.Node(edge.ControllerName), graph.Subgraph(resourceTypeID(edge)).Node(resourceID(resource))).Solid()
case inspect.DependencyEdgeType_INPUT_STRONG:
graph.Edge(graph.Subgraph(resourceTypeID(edge)).Node(resourceID(resource)), graph.Node(edge.ControllerName)).Solid()
case inspect.DependencyEdgeType_INPUT_WEAK:
graph.Edge(graph.Subgraph(resourceTypeID(edge)).Node(resourceID(resource)), graph.Node(edge.ControllerName)).Dotted()
case inspect.DependencyEdgeType_INPUT_DESTROY_READY: // don't show the DestroyReady inputs to reduce the visual clutter
}
}
}
}
} else {
for _, msg := range resp.GetMessages() {
for _, edge := range msg.GetEdges() {
graph.Node(edge.ControllerName).Box()
graph.Node(resourceTypeID(edge)).
Attr("shape", "note").
Attr("fillcolor", "azure2").
Attr("style", "filled")
}
}
for _, msg := range resp.GetMessages() {
for _, edge := range msg.GetEdges() {
idLabels := []string{}
if edge.GetResourceId() != "" {
idLabels = append(idLabels, edge.GetResourceId())
}
switch edge.GetEdgeType() {
case inspect.DependencyEdgeType_OUTPUT_EXCLUSIVE:
graph.Edge(graph.Node(edge.ControllerName), graph.Node(resourceTypeID(edge))).Bold()
case inspect.DependencyEdgeType_OUTPUT_SHARED:
graph.Edge(graph.Node(edge.ControllerName), graph.Node(resourceTypeID(edge))).Solid()
case inspect.DependencyEdgeType_INPUT_STRONG:
graph.Edge(graph.Node(resourceTypeID(edge)), graph.Node(edge.ControllerName), idLabels...).Solid()
case inspect.DependencyEdgeType_INPUT_WEAK:
graph.Edge(graph.Node(resourceTypeID(edge)), graph.Node(edge.ControllerName), idLabels...).Dotted()
case inspect.DependencyEdgeType_INPUT_DESTROY_READY:
// don't show the DestroyReady inputs to reduce the visual clutter
}
}
}
}
graph.Write(output)
return nil
}
// RenderServicesInfo writes human readable service information to the io.Writer.
func RenderServicesInfo(services []client.ServiceInfo, output io.Writer, defaultNode string, withNodeInfo bool) error {
w := tabwriter.NewWriter(output, 0, 0, 3, ' ', 0)
node := defaultNode
for _, s := range services {
if s.Metadata != nil {
node = s.Metadata.Hostname
}
if withNodeInfo {
fmt.Fprintf(w, "NODE\t%s\n", node)
}
svc := ServiceInfoWrapper{s.Service}
fmt.Fprintf(w, "ID\t%s\n", svc.Id)
fmt.Fprintf(w, "STATE\t%s\n", svc.State)
fmt.Fprintf(w, "HEALTH\t%s\n", svc.HealthStatus())
if svc.Health.LastMessage != "" {
fmt.Fprintf(w, "LAST HEALTH MESSAGE\t%s\n", svc.Health.LastMessage)
}
label := "EVENTS"
for i := range svc.Events.Events {
event := svc.Events.Events[len(svc.Events.Events)-1-i]
ts := event.Ts.AsTime()
fmt.Fprintf(w, "%s\t[%s]: %s (%s ago)\n", label, event.State, event.Msg, time.Since(ts).Round(time.Second))
label = ""
}
}
return w.Flush()
}
// ServiceInfoWrapper helper that allows generating rich service information.
type ServiceInfoWrapper struct {
*machine.ServiceInfo
}
// LastUpdated derive last updated time from events stream.
func (svc ServiceInfoWrapper) LastUpdated() string {
if len(svc.Events.Events) == 0 {
return ""
}
ts := svc.Events.Events[len(svc.Events.Events)-1].Ts.AsTime()
return time.Since(ts).Round(time.Second).String()
}
// LastEvent return last service event.
func (svc ServiceInfoWrapper) LastEvent() string {
if len(svc.Events.Events) == 0 {
return "<none>"
}
return svc.Events.Events[len(svc.Events.Events)-1].Msg
}
// HealthStatus service health status.
func (svc ServiceInfoWrapper) HealthStatus() string {
if svc.Health.Unknown {
return "?"
}
if svc.Health.Healthy {
return "OK"
}
return "Fail"
}