github.com/tilt-dev/tilt@v0.36.0/internal/docker/fake_client.go (about)

     1  package docker
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  	"unicode"
    12  
    13  	"github.com/docker/go-units"
    14  	"github.com/opencontainers/go-digest"
    15  	"github.com/pkg/errors"
    16  	"golang.org/x/sync/errgroup"
    17  
    18  	"github.com/distribution/reference"
    19  	"github.com/docker/docker/api/types"
    20  	typescontainer "github.com/docker/docker/api/types/container"
    21  	"github.com/docker/docker/api/types/filters"
    22  	typesimage "github.com/docker/docker/api/types/image"
    23  
    24  	"github.com/tilt-dev/tilt/internal/container"
    25  	"github.com/tilt-dev/tilt/pkg/model"
    26  )
    27  
    28  const ExampleBuildSHA1 = "sha256:11cd0b38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab"
    29  
    30  const ExampleBuildOutput1 = `{"stream":"Run 1/1 : FROM alpine"}
    31  	{"stream":"\n"}
    32  	{"stream":" ---\u003e 11cd0b38bc3c\n"}
    33  	{"aux":{"ID":"sha256:11cd0b38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab"}}
    34  	{"stream":"Successfully built 11cd0b38bc3c\n"}
    35  	{"stream":"Successfully tagged hi:latest\n"}
    36  `
    37  const ExampleBuildOutputV1_23 = `{"stream":"Run 1/1 : FROM alpine"}
    38  	{"stream":"\n"}
    39  	{"stream":" ---\u003e 11cd0b38bc3c\n"}
    40  	{"stream":"Successfully built 11cd0b38bc3c\n"}
    41  	{"stream":"Successfully tagged hi:latest\n"}
    42  `
    43  
    44  // same as ExampleBuildOutput1 but with a different digest
    45  const ExampleBuildOutput2 = `{"stream":"Run 1/1 : FROM alpine"}
    46  	{"stream":"\n"}
    47  	{"stream":" ---\u003e 20372c132963\n"}
    48  	{"aux":{"ID":"sha256:20372c132963eb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af20372c132963"}}
    49  	{"stream":"Successfully built 20372c132963\n"}
    50  	{"stream":"Successfully tagged hi:latest\n"}
    51  `
    52  
    53  const ExamplePushSHA1 = "sha256:cc5f4c463f81c55183d8d737ba2f0d30b3e6f3670dbe2da68f0aac168e93fbb1"
    54  
    55  var ExamplePushOutput1 = `{"status":"The push refers to repository [localhost:5005/myimage]"}
    56  	{"status":"Preparing","progressDetail":{},"id":"2a88b569da78"}
    57  	{"status":"Preparing","progressDetail":{},"id":"73046094a9b8"}
    58  	{"status":"Pushing","progressDetail":{"current":512,"total":41},"progress":"[==================================================\u003e]     512B","id":"2a88b569da78"}
    59  	{"status":"Pushing","progressDetail":{"current":68608,"total":4413370},"progress":"[\u003e                                                  ]  68.61kB/4.413MB","id":"73046094a9b8"}
    60  	{"status":"Pushing","progressDetail":{"current":5120,"total":41},"progress":"[==================================================\u003e]   5.12kB","id":"2a88b569da78"}
    61  	{"status":"Pushed","progressDetail":{},"id":"2a88b569da78"}
    62  	{"status":"Pushing","progressDetail":{"current":1547776,"total":4413370},"progress":"[=================\u003e                                 ]  1.548MB/4.413MB","id":"73046094a9b8"}
    63  	{"status":"Pushing","progressDetail":{"current":3247616,"total":4413370},"progress":"[====================================\u003e              ]  3.248MB/4.413MB","id":"73046094a9b8"}
    64  	{"status":"Pushing","progressDetail":{"current":4672000,"total":4413370},"progress":"[==================================================\u003e]  4.672MB","id":"73046094a9b8"}
    65  	{"status":"Pushed","progressDetail":{},"id":"73046094a9b8"}
    66  	{"status":"tilt-11cd0b38bc3ceb95: digest: sha256:cc5f4c463f81c55183d8d737ba2f0d30b3e6f3670dbe2da68f0aac168e93fbb1 size: 735"}
    67  	{"progressDetail":{},"aux":{"Tag":"tilt-11cd0b38bc3ceb95","Digest":"sha256:cc5f4c463f81c55183d8d737ba2f0d30b3e6f3670dbe2da68f0aac168e93fbb1","Size":735}}`
    68  
    69  const (
    70  	TestPod       = "test_pod"
    71  	TestContainer = "test_container"
    72  )
    73  
    74  var DefaultContainerListOutput = map[string][]types.Container{
    75  	TestPod: []types.Container{
    76  		types.Container{ID: TestContainer, ImageID: ExampleBuildSHA1, Command: "./stuff"},
    77  	},
    78  	"two-containers": []types.Container{
    79  		types.Container{ID: "not a match", ImageID: ExamplePushSHA1, Command: "/pause"},
    80  		types.Container{ID: "the right container", ImageID: ExampleBuildSHA1, Command: "./stuff"},
    81  	},
    82  }
    83  
    84  type ExecCall struct {
    85  	Container string
    86  	Cmd       model.Cmd
    87  }
    88  
    89  type FakeClient struct {
    90  	FakeEnv Env
    91  
    92  	PushCount   int
    93  	PushImage   string
    94  	PushOptions typesimage.PushOptions
    95  	PushOutput  string
    96  
    97  	BuildCount        int
    98  	BuildOptions      BuildOptions
    99  	BuildContext      *bytes.Buffer
   100  	BuildOutput       string
   101  	BuildErrorToThrow error // next call to Build will throw this err (after which we clear the error)
   102  
   103  	ImageListCount int
   104  	ImageListOpts  []typesimage.ListOptions
   105  
   106  	TagCount  int
   107  	TagSource string
   108  	TagTarget string
   109  
   110  	ContainerListOutput map[string][]types.Container
   111  
   112  	CopyCount     int
   113  	CopyContainer string
   114  	CopyContent   io.Reader
   115  
   116  	ExecCalls         []ExecCall
   117  	ExecErrorsToThrow []error // next call to exec will throw ExecError[0] (which we then pop)
   118  
   119  	RestartsByContainer map[string]int
   120  	RemovedImageIDs     []string
   121  
   122  	// Images returned by ImageInspect.
   123  	Images map[string]types.ImageInspect
   124  
   125  	// Containers returned by ContainerInspect
   126  	Containers        map[string]types.ContainerState
   127  	ContainerLogChans map[string]<-chan string
   128  
   129  	// If true, ImageInspectWithRaw will always return an ImageInspect,
   130  	// even if one hasn't been explicitly pre-loaded.
   131  	ImageAlwaysExists bool
   132  
   133  	Orchestrator      model.Orchestrator
   134  	CheckConnectedErr error
   135  
   136  	ThrowNewVersionError   bool
   137  	BuildCachePruneErr     error
   138  	BuildCachePruneOpts    types.BuildCachePruneOptions
   139  	BuildCachesPruned      []string
   140  	ContainersPruneErr     error
   141  	ContainersPruneFilters filters.Args
   142  	ContainersPruned       []string
   143  }
   144  
   145  var _ Client = &FakeClient{}
   146  
   147  func NewFakeClient() *FakeClient {
   148  	return &FakeClient{
   149  		PushOutput:          ExamplePushOutput1,
   150  		BuildOutput:         ExampleBuildOutput1,
   151  		ContainerListOutput: make(map[string][]types.Container),
   152  		RestartsByContainer: make(map[string]int),
   153  		Images:              make(map[string]types.ImageInspect),
   154  		Containers:          make(map[string]types.ContainerState),
   155  		ContainerLogChans:   make(map[string]<-chan string),
   156  	}
   157  }
   158  
   159  func (c *FakeClient) SetOrchestrator(orc model.Orchestrator) {
   160  	c.Orchestrator = orc
   161  }
   162  func (c *FakeClient) ForOrchestrator(orc model.Orchestrator) Client {
   163  	return c
   164  }
   165  func (c *FakeClient) CheckConnected() error {
   166  	return c.CheckConnectedErr
   167  }
   168  func (c *FakeClient) Env() Env {
   169  	return c.FakeEnv
   170  }
   171  func (c *FakeClient) BuilderVersion(ctx context.Context) (types.BuilderVersion, error) {
   172  	return types.BuilderV1, nil
   173  }
   174  func (c *FakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
   175  	return types.Version{
   176  		Arch:    "amd64",
   177  		Version: "20.10.11",
   178  	}, nil
   179  }
   180  
   181  func (c *FakeClient) SetExecError(err error) {
   182  	c.ExecErrorsToThrow = []error{err}
   183  }
   184  
   185  func (c *FakeClient) ContainerLogs(ctx context.Context, containerID string, options typescontainer.LogsOptions) (io.ReadCloser, error) {
   186  	output := c.ContainerLogChans[containerID]
   187  	reader, writer := io.Pipe()
   188  	go func() {
   189  		if ctx.Err() != nil {
   190  			return
   191  		}
   192  
   193  		done := false
   194  		for !done {
   195  			select {
   196  			case <-ctx.Done():
   197  				done = true
   198  			case s, ok := <-output:
   199  				if !ok {
   200  					done = true
   201  				} else {
   202  					logLine := fmt.Sprintf("%s\n",
   203  						strings.TrimRightFunc(s, unicode.IsSpace))
   204  					_, _ = writer.Write([]byte(logLine))
   205  				}
   206  			}
   207  		}
   208  	}()
   209  	return reader, nil
   210  }
   211  
   212  func (c *FakeClient) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
   213  	container, ok := c.Containers[containerID]
   214  	if ok {
   215  		return types.ContainerJSON{
   216  			Config: &typescontainer.Config{Tty: true},
   217  			ContainerJSONBase: &types.ContainerJSONBase{
   218  				ID:    containerID,
   219  				State: &container,
   220  			},
   221  		}, nil
   222  	}
   223  	state := NewRunningContainerState()
   224  	return types.ContainerJSON{
   225  		Config: &typescontainer.Config{Tty: true},
   226  		ContainerJSONBase: &types.ContainerJSONBase{
   227  			ID:    containerID,
   228  			State: &state,
   229  		},
   230  	}, nil
   231  }
   232  
   233  func (c *FakeClient) SetContainerListOutput(output map[string][]types.Container) {
   234  	c.ContainerListOutput = output
   235  }
   236  
   237  func (c *FakeClient) SetDefaultContainerListOutput() {
   238  	c.SetContainerListOutput(DefaultContainerListOutput)
   239  }
   240  
   241  func (c *FakeClient) ContainerList(ctx context.Context, options typescontainer.ListOptions) ([]types.Container, error) {
   242  	nameFilter := options.Filters.Get("name")
   243  	if len(nameFilter) != 1 {
   244  		return nil, fmt.Errorf("expected one filter for 'name', got: %v", nameFilter)
   245  	}
   246  
   247  	if len(c.ContainerListOutput) == 0 {
   248  		return nil, fmt.Errorf("FakeClient ContainerListOutput not set (use `SetContainerListOutput`)")
   249  	}
   250  	res := c.ContainerListOutput[nameFilter[0]]
   251  
   252  	// unset containerListOutput
   253  	c.ContainerListOutput = nil
   254  
   255  	return res, nil
   256  }
   257  
   258  func (c *FakeClient) ContainerRestartNoWait(ctx context.Context, containerID string) error {
   259  	c.RestartsByContainer[containerID]++
   260  	return nil
   261  }
   262  
   263  func (c *FakeClient) Run(ctx context.Context, opts RunConfig) (RunResult, error) {
   264  	return RunResult{}, nil
   265  }
   266  
   267  func (c *FakeClient) ExecInContainer(ctx context.Context, cID container.ID, cmd model.Cmd, in io.Reader, out io.Writer) error {
   268  	if cmd.Argv[0] == "tar" {
   269  		c.CopyCount++
   270  		c.CopyContainer = string(cID)
   271  		c.CopyContent = in
   272  		return nil
   273  	}
   274  
   275  	execCall := ExecCall{
   276  		Container: cID.String(),
   277  		Cmd:       cmd,
   278  	}
   279  	c.ExecCalls = append(c.ExecCalls, execCall)
   280  
   281  	// If we're supposed to throw an error on this call, throw it (and pop from
   282  	// the list of ErrorsToThrow)
   283  	var err error
   284  	if len(c.ExecErrorsToThrow) > 0 {
   285  		err = c.ExecErrorsToThrow[0]
   286  		c.ExecErrorsToThrow = append([]error{}, c.ExecErrorsToThrow[1:]...)
   287  	}
   288  
   289  	return err
   290  }
   291  
   292  func (c *FakeClient) ImagePull(_ context.Context, ref reference.Named) (reference.Canonical, error) {
   293  	// fake digest is the reference itself hashed
   294  	// i.e. docker.io/library/_/nginx -> sha256sum(docker.io/library/_/nginx) -> 2ca21a92e8ee99f672764b7619a413019de5ffc7f06dbc7422d41eca17705802
   295  	return reference.WithDigest(ref, digest.FromString(ref.String()))
   296  }
   297  
   298  func (c *FakeClient) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
   299  	c.PushCount++
   300  	c.PushImage = ref.String()
   301  	return NewFakeDockerResponse(c.PushOutput), nil
   302  }
   303  
   304  func (c *FakeClient) ImageBuild(ctx context.Context, g *errgroup.Group, buildContext io.Reader, options BuildOptions) (types.ImageBuildResponse, error) {
   305  	c.BuildCount++
   306  	c.BuildOptions = options
   307  
   308  	data, err := io.ReadAll(buildContext)
   309  	if err != nil {
   310  		return types.ImageBuildResponse{}, errors.Wrap(err, "ImageBuild")
   311  	}
   312  
   313  	c.BuildContext = bytes.NewBuffer(data)
   314  
   315  	// If we're supposed to throw an error on this call, throw it (and reset ErrorToThrow)
   316  	err = c.BuildErrorToThrow
   317  	if err != nil {
   318  		c.BuildErrorToThrow = nil
   319  		return types.ImageBuildResponse{}, err
   320  	}
   321  
   322  	return types.ImageBuildResponse{Body: NewFakeDockerResponse(c.BuildOutput)}, nil
   323  }
   324  
   325  func (c *FakeClient) ImageTag(ctx context.Context, source, target string) error {
   326  	c.TagCount++
   327  	c.TagSource = source
   328  	c.TagTarget = target
   329  	return nil
   330  }
   331  
   332  func (c *FakeClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
   333  	result, ok := c.Images[imageID]
   334  	if ok {
   335  		return result, nil, nil
   336  	}
   337  
   338  	if c.ImageAlwaysExists {
   339  		return types.ImageInspect{}, nil, nil
   340  	}
   341  
   342  	return types.ImageInspect{}, nil, newNotFoundErrorf("fakeClient.Images key: %s", imageID)
   343  }
   344  
   345  func (c *FakeClient) ImageList(ctx context.Context, options typesimage.ListOptions) ([]typesimage.Summary, error) {
   346  	c.ImageListOpts = append(c.ImageListOpts, options)
   347  	summaries := make([]typesimage.Summary, c.ImageListCount)
   348  	for i := range summaries {
   349  		summaries[i] = typesimage.Summary{
   350  			ID:      fmt.Sprintf("build-id-%d", i),
   351  			Created: time.Now().Add(-time.Second).Unix(),
   352  		}
   353  	}
   354  	return summaries, nil
   355  }
   356  
   357  func (c *FakeClient) ImageRemove(ctx context.Context, imageID string, options typesimage.RemoveOptions) ([]typesimage.DeleteResponse, error) {
   358  	c.RemovedImageIDs = append(c.RemovedImageIDs, imageID)
   359  	sort.Strings(c.RemovedImageIDs)
   360  	return []typesimage.DeleteResponse{
   361  		typesimage.DeleteResponse{Deleted: imageID},
   362  	}, nil
   363  }
   364  
   365  func (c *FakeClient) NewVersionError(ctx context.Context, apiRequired, feature string) error {
   366  	if c.ThrowNewVersionError {
   367  		c.ThrowNewVersionError = false
   368  		return c.VersionError(apiRequired, feature)
   369  	}
   370  	return nil
   371  }
   372  
   373  func (c *FakeClient) VersionError(apiRequired, feature string) error {
   374  	return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is... something else", feature, apiRequired)
   375  }
   376  
   377  func (c *FakeClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
   378  	if err := c.BuildCachePruneErr; err != nil {
   379  		c.BuildCachePruneErr = nil
   380  		return nil, err
   381  	}
   382  
   383  	c.BuildCachePruneOpts = types.BuildCachePruneOptions{
   384  		All:         opts.All,
   385  		KeepStorage: opts.KeepStorage,
   386  		Filters:     opts.Filters.Clone(),
   387  	}
   388  	report := &types.BuildCachePruneReport{
   389  		CachesDeleted:  c.BuildCachesPruned,
   390  		SpaceReclaimed: uint64(units.MB * len(c.BuildCachesPruned)), // 1MB per cache pruned
   391  	}
   392  	c.BuildCachesPruned = nil
   393  	return report, nil
   394  }
   395  
   396  func (c *FakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (typescontainer.PruneReport, error) {
   397  	if err := c.ContainersPruneErr; err != nil {
   398  		c.ContainersPruneErr = nil
   399  		return typescontainer.PruneReport{}, err
   400  	}
   401  
   402  	c.ContainersPruneFilters = pruneFilters.Clone()
   403  	report := typescontainer.PruneReport{
   404  		ContainersDeleted: c.ContainersPruned,
   405  		SpaceReclaimed:    uint64(units.MB * len(c.ContainersPruned)), // 1MB per container pruned
   406  	}
   407  	c.ContainersPruned = nil
   408  	return report, nil
   409  }
   410  
   411  var _ Client = &FakeClient{}
   412  
   413  type fakeDockerResponse struct {
   414  	*bytes.Buffer
   415  }
   416  
   417  func NewFakeDockerResponse(contents string) fakeDockerResponse {
   418  	return fakeDockerResponse{Buffer: bytes.NewBufferString(contents)}
   419  }
   420  
   421  func (r fakeDockerResponse) Close() error { return nil }
   422  
   423  var _ io.ReadCloser = fakeDockerResponse{}
   424  
   425  type notFoundError struct {
   426  	details string
   427  }
   428  
   429  func newNotFoundErrorf(s string, a ...interface{}) notFoundError {
   430  	return notFoundError{details: fmt.Sprintf(s, a...)}
   431  }
   432  
   433  func (e notFoundError) NotFound() bool {
   434  	return true
   435  }
   436  
   437  func (e notFoundError) Error() string {
   438  	return fmt.Sprintf("fake docker client error: object not found (%s)", e.details)
   439  }