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