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 }