github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/node/formatter_test.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package node
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/cli/cli/command/formatter"
    14  	"github.com/docker/cli/internal/test"
    15  	"github.com/docker/docker/api/types/swarm"
    16  	"github.com/docker/docker/api/types/system"
    17  	"github.com/docker/docker/pkg/stringid"
    18  	"gotest.tools/v3/assert"
    19  	is "gotest.tools/v3/assert/cmp"
    20  )
    21  
    22  func TestNodeContext(t *testing.T) {
    23  	nodeID := stringid.GenerateRandomID()
    24  
    25  	var ctx nodeContext
    26  	cases := []struct {
    27  		nodeCtx  nodeContext
    28  		expValue string
    29  		call     func() string
    30  	}{
    31  		{nodeContext{
    32  			n: swarm.Node{ID: nodeID},
    33  		}, nodeID, ctx.ID},
    34  		{nodeContext{
    35  			n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}},
    36  		}, "node_hostname", ctx.Hostname},
    37  		{nodeContext{
    38  			n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}},
    39  		}, "Foo", ctx.Status},
    40  		{nodeContext{
    41  			n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}},
    42  		}, "Drain", ctx.Availability},
    43  		{nodeContext{
    44  			n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}},
    45  		}, "Leader", ctx.ManagerStatus},
    46  	}
    47  
    48  	for _, c := range cases {
    49  		ctx = c.nodeCtx
    50  		v := c.call()
    51  		if strings.Contains(v, ",") {
    52  			test.CompareMultipleValues(t, v, c.expValue)
    53  		} else if v != c.expValue {
    54  			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
    55  		}
    56  	}
    57  }
    58  
    59  func TestNodeContextWrite(t *testing.T) {
    60  	cases := []struct {
    61  		context     formatter.Context
    62  		expected    string
    63  		clusterInfo swarm.ClusterInfo
    64  	}{
    65  		// Errors
    66  		{
    67  			context:     formatter.Context{Format: "{{InvalidFunction}}"},
    68  			expected:    `template parsing error: template: :1: function "InvalidFunction" not defined`,
    69  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
    70  		},
    71  		{
    72  			context:     formatter.Context{Format: "{{nil}}"},
    73  			expected:    `template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`,
    74  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
    75  		},
    76  		// Table format
    77  		{
    78  			context: formatter.Context{Format: NewFormat("table", false)},
    79  			expected: `ID          HOSTNAME     STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
    80  nodeID1     foobar_baz   Foo       Drain          Leader           18.03.0-ce
    81  nodeID2     foobar_bar   Bar       Active         Reachable        1.2.3
    82  nodeID3     foobar_boo   Boo       Active                          ` + "\n", // (to preserve whitespace)
    83  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
    84  		},
    85  		{
    86  			context: formatter.Context{Format: NewFormat("table", true)},
    87  			expected: `nodeID1
    88  nodeID2
    89  nodeID3
    90  `,
    91  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
    92  		},
    93  		{
    94  			context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)},
    95  			expected: `HOSTNAME
    96  foobar_baz
    97  foobar_bar
    98  foobar_boo
    99  `,
   100  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   101  		},
   102  		{
   103  			context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)},
   104  			expected: `HOSTNAME
   105  foobar_baz
   106  foobar_bar
   107  foobar_boo
   108  `,
   109  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   110  		},
   111  		{
   112  			context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
   113  			expected: `ID        HOSTNAME     TLS STATUS
   114  nodeID1   foobar_baz   Needs Rotation
   115  nodeID2   foobar_bar   Ready
   116  nodeID3   foobar_boo   Unknown
   117  `,
   118  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   119  		},
   120  		{ // no cluster TLS status info, TLS status for all nodes is unknown
   121  			context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)},
   122  			expected: `ID        HOSTNAME     TLS STATUS
   123  nodeID1   foobar_baz   Unknown
   124  nodeID2   foobar_bar   Unknown
   125  nodeID3   foobar_boo   Unknown
   126  `,
   127  			clusterInfo: swarm.ClusterInfo{},
   128  		},
   129  		// Raw Format
   130  		{
   131  			context: formatter.Context{Format: NewFormat("raw", false)},
   132  			expected: `node_id: nodeID1
   133  hostname: foobar_baz
   134  status: Foo
   135  availability: Drain
   136  manager_status: Leader
   137  
   138  node_id: nodeID2
   139  hostname: foobar_bar
   140  status: Bar
   141  availability: Active
   142  manager_status: Reachable
   143  
   144  node_id: nodeID3
   145  hostname: foobar_boo
   146  status: Boo
   147  availability: Active
   148  manager_status: ` + "\n\n", // to preserve whitespace
   149  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   150  		},
   151  		{
   152  			context: formatter.Context{Format: NewFormat("raw", true)},
   153  			expected: `node_id: nodeID1
   154  node_id: nodeID2
   155  node_id: nodeID3
   156  `,
   157  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   158  		},
   159  		// Custom Format
   160  		{
   161  			context: formatter.Context{Format: NewFormat("{{.Hostname}}  {{.TLSStatus}}", false)},
   162  			expected: `foobar_baz  Needs Rotation
   163  foobar_bar  Ready
   164  foobar_boo  Unknown
   165  `,
   166  			clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
   167  		},
   168  	}
   169  
   170  	nodes := []swarm.Node{
   171  		{
   172  			ID: "nodeID1",
   173  			Description: swarm.NodeDescription{
   174  				Hostname: "foobar_baz",
   175  				TLSInfo:  swarm.TLSInfo{TrustRoot: "no"},
   176  				Engine:   swarm.EngineDescription{EngineVersion: "18.03.0-ce"},
   177  			},
   178  			Status:        swarm.NodeStatus{State: swarm.NodeState("foo")},
   179  			Spec:          swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")},
   180  			ManagerStatus: &swarm.ManagerStatus{Leader: true},
   181  		},
   182  		{
   183  			ID: "nodeID2",
   184  			Description: swarm.NodeDescription{
   185  				Hostname: "foobar_bar",
   186  				TLSInfo:  swarm.TLSInfo{TrustRoot: "hi"},
   187  				Engine:   swarm.EngineDescription{EngineVersion: "1.2.3"},
   188  			},
   189  			Status: swarm.NodeStatus{State: swarm.NodeState("bar")},
   190  			Spec:   swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
   191  			ManagerStatus: &swarm.ManagerStatus{
   192  				Leader:       false,
   193  				Reachability: swarm.Reachability("Reachable"),
   194  			},
   195  		},
   196  		{
   197  			ID:          "nodeID3",
   198  			Description: swarm.NodeDescription{Hostname: "foobar_boo"},
   199  			Status:      swarm.NodeStatus{State: swarm.NodeState("boo")},
   200  			Spec:        swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
   201  		},
   202  	}
   203  
   204  	for _, tc := range cases {
   205  		tc := tc
   206  		t.Run(string(tc.context.Format), func(t *testing.T) {
   207  			var out bytes.Buffer
   208  			tc.context.Output = &out
   209  
   210  			err := FormatWrite(tc.context, nodes, system.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}})
   211  			if err != nil {
   212  				assert.Error(t, err, tc.expected)
   213  			} else {
   214  				assert.Equal(t, out.String(), tc.expected)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestNodeContextWriteJSON(t *testing.T) {
   221  	cases := []struct {
   222  		expected []map[string]any
   223  		info     system.Info
   224  	}{
   225  		{
   226  			expected: []map[string]any{
   227  				{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "1.2.3"},
   228  				{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": ""},
   229  				{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
   230  			},
   231  			info: system.Info{},
   232  		},
   233  		{
   234  			expected: []map[string]any{
   235  				{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Ready", "EngineVersion": "1.2.3"},
   236  				{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation", "EngineVersion": ""},
   237  				{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
   238  			},
   239  			info: system.Info{
   240  				Swarm: swarm.Info{
   241  					Cluster: &swarm.ClusterInfo{
   242  						TLSInfo:                swarm.TLSInfo{TrustRoot: "hi"},
   243  						RootRotationInProgress: true,
   244  					},
   245  				},
   246  			},
   247  		},
   248  	}
   249  
   250  	for _, testcase := range cases {
   251  		nodes := []swarm.Node{
   252  			{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}},
   253  			{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}},
   254  			{ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}},
   255  		}
   256  		out := bytes.NewBufferString("")
   257  		err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info)
   258  		if err != nil {
   259  			t.Fatal(err)
   260  		}
   261  		for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
   262  			msg := fmt.Sprintf("Output: line %d: %s", i, line)
   263  			var m map[string]any
   264  			err := json.Unmarshal([]byte(line), &m)
   265  			assert.NilError(t, err, msg)
   266  			assert.Check(t, is.DeepEqual(testcase.expected[i], m), msg)
   267  		}
   268  	}
   269  }
   270  
   271  func TestNodeContextWriteJSONField(t *testing.T) {
   272  	nodes := []swarm.Node{
   273  		{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
   274  		{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
   275  	}
   276  	out := bytes.NewBufferString("")
   277  	err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, system.Info{})
   278  	if err != nil {
   279  		t.Fatal(err)
   280  	}
   281  	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
   282  		msg := fmt.Sprintf("Output: line %d: %s", i, line)
   283  		var s string
   284  		err := json.Unmarshal([]byte(line), &s)
   285  		assert.NilError(t, err, msg)
   286  		assert.Check(t, is.Equal(nodes[i].ID, s), msg)
   287  	}
   288  }
   289  
   290  func TestNodeInspectWriteContext(t *testing.T) {
   291  	node := swarm.Node{
   292  		ID: "nodeID1",
   293  		Description: swarm.NodeDescription{
   294  			Hostname: "foobar_baz",
   295  			TLSInfo: swarm.TLSInfo{
   296  				TrustRoot:           "-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----\n",
   297  				CertIssuerPublicKey: []byte("pubKey"),
   298  				CertIssuerSubject:   []byte("subject"),
   299  			},
   300  			Platform: swarm.Platform{
   301  				OS:           "linux",
   302  				Architecture: "amd64",
   303  			},
   304  			Resources: swarm.Resources{
   305  				MemoryBytes: 1,
   306  			},
   307  			Engine: swarm.EngineDescription{
   308  				EngineVersion: "0.1.1",
   309  			},
   310  		},
   311  		Status: swarm.NodeStatus{
   312  			State: swarm.NodeState("ready"),
   313  			Addr:  "1.1.1.1",
   314  		},
   315  		Spec: swarm.NodeSpec{
   316  			Availability: swarm.NodeAvailability("drain"),
   317  			Role:         swarm.NodeRole("manager"),
   318  		},
   319  	}
   320  	out := bytes.NewBufferString("")
   321  	context := formatter.Context{
   322  		Format: NewFormat("pretty", false),
   323  		Output: out,
   324  	}
   325  	err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) {
   326  		return node, nil, nil
   327  	})
   328  	if err != nil {
   329  		t.Fatal(err)
   330  	}
   331  	expected := `ID:			nodeID1
   332  Hostname:              	foobar_baz
   333  Joined at:             	0001-01-01 00:00:00 +0000 utc
   334  Status:
   335   State:			Ready
   336   Availability:         	Drain
   337   Address:		1.1.1.1
   338  Platform:
   339   Operating System:	linux
   340   Architecture:		amd64
   341  Resources:
   342   CPUs:			0
   343   Memory:		1B
   344  Engine Version:		0.1.1
   345  TLS Info:
   346   TrustRoot:
   347  -----BEGIN CERTIFICATE-----
   348  data
   349  -----END CERTIFICATE-----
   350  
   351   Issuer Subject:	c3ViamVjdA==
   352   Issuer Public Key:	cHViS2V5
   353  `
   354  	assert.Check(t, is.Equal(expected, out.String()))
   355  }