mirror of
https://github.com/siderolabs/talos.git
synced 2025-09-11 08:51:11 +02:00
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>
276 lines
7.9 KiB
Go
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"
|
|
}
|