github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/cli_test.go (about)

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/docker/docker/api"
    19  	"github.com/docker/docker/api/types"
    20  	"github.com/docker/docker/client"
    21  	"github.com/khulnasoft/cli/cli/config"
    22  	"github.com/khulnasoft/cli/cli/config/configfile"
    23  	"github.com/khulnasoft/cli/cli/flags"
    24  	"github.com/pkg/errors"
    25  	"gotest.tools/v3/assert"
    26  	"gotest.tools/v3/fs"
    27  )
    28  
    29  func TestNewAPIClientFromFlags(t *testing.T) {
    30  	host := "unix://path"
    31  	if runtime.GOOS == "windows" {
    32  		host = "npipe://./"
    33  	}
    34  	opts := &flags.ClientOptions{Hosts: []string{host}}
    35  	apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
    36  	assert.NilError(t, err)
    37  	assert.Equal(t, apiClient.DaemonHost(), host)
    38  	assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
    39  }
    40  
    41  func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
    42  	host := ":2375"
    43  	slug := "tcp://localhost"
    44  	if runtime.GOOS == "windows" {
    45  		slug = "tcp://127.0.0.1"
    46  	}
    47  	opts := &flags.ClientOptions{Hosts: []string{host}}
    48  	apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
    49  	assert.NilError(t, err)
    50  	assert.Equal(t, apiClient.DaemonHost(), slug+host)
    51  	assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
    52  }
    53  
    54  func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
    55  	var received map[string]string
    56  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    57  		received = map[string]string{
    58  			"My-Header":  r.Header.Get("My-Header"),
    59  			"User-Agent": r.Header.Get("User-Agent"),
    60  		}
    61  		_, _ = w.Write([]byte("OK"))
    62  	}))
    63  	defer ts.Close()
    64  	host := strings.Replace(ts.URL, "http://", "tcp://", 1)
    65  	opts := &flags.ClientOptions{Hosts: []string{host}}
    66  	configFile := &configfile.ConfigFile{
    67  		HTTPHeaders: map[string]string{
    68  			"My-Header": "Custom-Value",
    69  		},
    70  	}
    71  
    72  	apiClient, err := NewAPIClientFromFlags(opts, configFile)
    73  	assert.NilError(t, err)
    74  	assert.Equal(t, apiClient.DaemonHost(), host)
    75  	assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
    76  
    77  	// verify User-Agent is not appended to the configfile. see https://github.com/khulnasoft/cli/pull/2756
    78  	assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
    79  
    80  	expectedHeaders := map[string]string{
    81  		"My-Header":  "Custom-Value",
    82  		"User-Agent": UserAgent(),
    83  	}
    84  	_, err = apiClient.Ping(context.Background())
    85  	assert.NilError(t, err)
    86  	assert.DeepEqual(t, received, expectedHeaders)
    87  }
    88  
    89  func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
    90  	customVersion := "v3.3.3"
    91  	t.Setenv("DOCKER_API_VERSION", customVersion)
    92  	t.Setenv("DOCKER_HOST", ":2375")
    93  
    94  	opts := &flags.ClientOptions{}
    95  	configFile := &configfile.ConfigFile{}
    96  	apiclient, err := NewAPIClientFromFlags(opts, configFile)
    97  	assert.NilError(t, err)
    98  	assert.Equal(t, apiclient.ClientVersion(), customVersion)
    99  }
   100  
   101  type fakeClient struct {
   102  	client.Client
   103  	pingFunc   func() (types.Ping, error)
   104  	version    string
   105  	negotiated bool
   106  }
   107  
   108  func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
   109  	return c.pingFunc()
   110  }
   111  
   112  func (c *fakeClient) ClientVersion() string {
   113  	return c.version
   114  }
   115  
   116  func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
   117  	c.negotiated = true
   118  }
   119  
   120  func TestInitializeFromClient(t *testing.T) {
   121  	const defaultVersion = "v1.55"
   122  
   123  	testcases := []struct {
   124  		doc            string
   125  		pingFunc       func() (types.Ping, error)
   126  		expectedServer ServerInfo
   127  		negotiated     bool
   128  	}{
   129  		{
   130  			doc: "successful ping",
   131  			pingFunc: func() (types.Ping, error) {
   132  				return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
   133  			},
   134  			expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
   135  			negotiated:     true,
   136  		},
   137  		{
   138  			doc: "failed ping, no API version",
   139  			pingFunc: func() (types.Ping, error) {
   140  				return types.Ping{}, errors.New("failed")
   141  			},
   142  			expectedServer: ServerInfo{HasExperimental: true},
   143  		},
   144  		{
   145  			doc: "failed ping, with API version",
   146  			pingFunc: func() (types.Ping, error) {
   147  				return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
   148  			},
   149  			expectedServer: ServerInfo{HasExperimental: true},
   150  			negotiated:     true,
   151  		},
   152  	}
   153  
   154  	for _, testcase := range testcases {
   155  		testcase := testcase
   156  		t.Run(testcase.doc, func(t *testing.T) {
   157  			apiclient := &fakeClient{
   158  				pingFunc: testcase.pingFunc,
   159  				version:  defaultVersion,
   160  			}
   161  
   162  			cli := &DockerCli{client: apiclient}
   163  			err := cli.Initialize(flags.NewClientOptions())
   164  			assert.NilError(t, err)
   165  			assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
   166  			assert.Equal(t, apiclient.negotiated, testcase.negotiated)
   167  		})
   168  	}
   169  }
   170  
   171  // Makes sure we don't hang forever on the initial connection.
   172  // https://github.com/khulnasoft/cli/issues/3652
   173  func TestInitializeFromClientHangs(t *testing.T) {
   174  	dir := t.TempDir()
   175  	socket := filepath.Join(dir, "my.sock")
   176  	l, err := net.Listen("unix", socket)
   177  	assert.NilError(t, err)
   178  
   179  	receiveReqCh := make(chan bool)
   180  	timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
   181  	defer cancel()
   182  
   183  	// Simulate a server that hangs on connections.
   184  	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   185  		select {
   186  		case <-timeoutCtx.Done():
   187  		case receiveReqCh <- true: // Blocks until someone receives on the channel.
   188  		}
   189  		_, _ = w.Write([]byte("OK"))
   190  	}))
   191  	ts.Listener = l
   192  	ts.Start()
   193  	defer ts.Close()
   194  
   195  	opts := &flags.ClientOptions{Hosts: []string{fmt.Sprintf("unix://%s", socket)}}
   196  	configFile := &configfile.ConfigFile{}
   197  	apiClient, err := NewAPIClientFromFlags(opts, configFile)
   198  	assert.NilError(t, err)
   199  
   200  	initializedCh := make(chan bool)
   201  
   202  	go func() {
   203  		cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
   204  		err := cli.Initialize(flags.NewClientOptions())
   205  		assert.Check(t, err)
   206  		cli.CurrentVersion()
   207  		close(initializedCh)
   208  	}()
   209  
   210  	select {
   211  	case <-timeoutCtx.Done():
   212  		t.Fatal("timeout waiting for initialization to complete")
   213  	case <-initializedCh:
   214  	}
   215  
   216  	select {
   217  	case <-timeoutCtx.Done():
   218  		t.Fatal("server never received an init request")
   219  	case <-receiveReqCh:
   220  	}
   221  }
   222  
   223  // The CLI no longer disables/hides experimental CLI features, however, we need
   224  // to verify that existing configuration files do not break
   225  func TestExperimentalCLI(t *testing.T) {
   226  	defaultVersion := "v1.55"
   227  
   228  	testcases := []struct {
   229  		doc        string
   230  		configfile string
   231  	}{
   232  		{
   233  			doc:        "default",
   234  			configfile: `{}`,
   235  		},
   236  		{
   237  			doc: "experimental",
   238  			configfile: `{
   239  	"experimental": "enabled"
   240  }`,
   241  		},
   242  	}
   243  
   244  	for _, testcase := range testcases {
   245  		testcase := testcase
   246  		t.Run(testcase.doc, func(t *testing.T) {
   247  			dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
   248  			defer dir.Remove()
   249  			apiclient := &fakeClient{
   250  				version: defaultVersion,
   251  				pingFunc: func() (types.Ping, error) {
   252  					return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
   253  				},
   254  			}
   255  
   256  			cli := &DockerCli{client: apiclient, err: os.Stderr}
   257  			config.SetDir(dir.Path())
   258  			err := cli.Initialize(flags.NewClientOptions())
   259  			assert.NilError(t, err)
   260  		})
   261  	}
   262  }
   263  
   264  func TestNewDockerCliAndOperators(t *testing.T) {
   265  	// Test default operations and also overriding default ones
   266  	cli, err := NewDockerCli(
   267  		WithContentTrust(true),
   268  	)
   269  	assert.NilError(t, err)
   270  	// Check streams are initialized
   271  	assert.Check(t, cli.In() != nil)
   272  	assert.Check(t, cli.Out() != nil)
   273  	assert.Check(t, cli.Err() != nil)
   274  	assert.Equal(t, cli.ContentTrustEnabled(), true)
   275  
   276  	// Apply can modify a dockerCli after construction
   277  	inbuf := bytes.NewBuffer([]byte("input"))
   278  	outbuf := bytes.NewBuffer(nil)
   279  	errbuf := bytes.NewBuffer(nil)
   280  	err = cli.Apply(
   281  		WithInputStream(io.NopCloser(inbuf)),
   282  		WithOutputStream(outbuf),
   283  		WithErrorStream(errbuf),
   284  	)
   285  	assert.NilError(t, err)
   286  	// Check input stream
   287  	inputStream, err := io.ReadAll(cli.In())
   288  	assert.NilError(t, err)
   289  	assert.Equal(t, string(inputStream), "input")
   290  	// Check output stream
   291  	fmt.Fprintf(cli.Out(), "output")
   292  	outputStream, err := io.ReadAll(outbuf)
   293  	assert.NilError(t, err)
   294  	assert.Equal(t, string(outputStream), "output")
   295  	// Check error stream
   296  	fmt.Fprintf(cli.Err(), "error")
   297  	errStream, err := io.ReadAll(errbuf)
   298  	assert.NilError(t, err)
   299  	assert.Equal(t, string(errStream), "error")
   300  }
   301  
   302  func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
   303  	cli, err := NewDockerCli()
   304  	assert.NilError(t, err)
   305  	assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
   306  		return client.NewClientWithOpts()
   307  	})))
   308  	assert.Check(t, cli.ContextStore() != nil)
   309  }
   310  
   311  func TestHooksEnabled(t *testing.T) {
   312  	t.Run("disabled by default", func(t *testing.T) {
   313  		cli, err := NewDockerCli()
   314  		assert.NilError(t, err)
   315  
   316  		assert.Check(t, !cli.HooksEnabled())
   317  	})
   318  
   319  	t.Run("enabled in configFile", func(t *testing.T) {
   320  		configFile := `{
   321      "features": {
   322        "hooks": "true"
   323      }}`
   324  		dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
   325  		defer dir.Remove()
   326  		cli, err := NewDockerCli()
   327  		assert.NilError(t, err)
   328  		config.SetDir(dir.Path())
   329  
   330  		assert.Check(t, cli.HooksEnabled())
   331  	})
   332  
   333  	t.Run("env var overrides configFile", func(t *testing.T) {
   334  		configFile := `{
   335      "features": {
   336        "hooks": "true"
   337      }}`
   338  		t.Setenv("DOCKER_CLI_HOOKS", "false")
   339  		dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
   340  		defer dir.Remove()
   341  		cli, err := NewDockerCli()
   342  		assert.NilError(t, err)
   343  		config.SetDir(dir.Path())
   344  
   345  		assert.Check(t, !cli.HooksEnabled())
   346  	})
   347  
   348  	t.Run("legacy env var overrides configFile", func(t *testing.T) {
   349  		configFile := `{
   350      "features": {
   351        "hooks": "true"
   352      }}`
   353  		t.Setenv("DOCKER_CLI_HINTS", "false")
   354  		dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
   355  		defer dir.Remove()
   356  		cli, err := NewDockerCli()
   357  		assert.NilError(t, err)
   358  		config.SetDir(dir.Path())
   359  
   360  		assert.Check(t, !cli.HooksEnabled())
   361  	})
   362  }