github.com/grahambrereton-form3/tilt@v0.10.18/internal/k8s/fake_client.go (about) 1 package k8s 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "io/ioutil" 8 "sync" 9 "time" 10 11 "github.com/docker/distribution/reference" 12 "github.com/google/uuid" 13 "github.com/pkg/errors" 14 v1 "k8s.io/api/core/v1" 15 apierrors "k8s.io/apimachinery/pkg/api/errors" 16 "k8s.io/apimachinery/pkg/labels" 17 "k8s.io/apimachinery/pkg/watch" 18 19 "github.com/windmilleng/tilt/internal/container" 20 "github.com/windmilleng/tilt/pkg/logger" 21 ) 22 23 // A magic constant. If the docker client returns this constant, we always match 24 // even if the container doesn't have the correct image name. 25 const MagicTestContainerID = "tilt-testcontainer" 26 27 var _ Client = &FakeK8sClient{} 28 29 // For keying PodLogsByPodAndContainer 30 type PodAndCName struct { 31 PID PodID 32 CName container.Name 33 } 34 35 type FakeK8sClient struct { 36 FakePortForwardClient 37 38 Yaml string 39 Lb LoadBalancerSpec 40 41 DeletedYaml string 42 DeleteError error 43 44 LastPodQueryNamespace Namespace 45 LastPodQueryImage reference.NamedTagged 46 47 PodLogsByPodAndContainer map[PodAndCName]BufferCloser 48 LastPodLogStartTime time.Time 49 ContainerLogsError error 50 51 podWatcherMu sync.Mutex 52 podWatches []fakePodWatch 53 54 serviceWatcherMu sync.Mutex 55 serviceWatches []fakeServiceWatch 56 57 eventsCh chan *v1.Event 58 EventsWatchErr error 59 60 UpsertError error 61 LastUpsertResult []K8sEntity 62 63 Runtime container.Runtime 64 Registry container.Registry 65 66 entityByName map[string]K8sEntity 67 getByReferenceCallCount int 68 69 ExecCalls []ExecCall 70 ExecErrors []error 71 } 72 73 type ExecCall struct { 74 PID PodID 75 CName container.Name 76 Ns Namespace 77 Cmd []string 78 Stdin []byte 79 } 80 81 type fakeServiceWatch struct { 82 ls labels.Selector 83 ch chan *v1.Service 84 } 85 86 type fakePodWatch struct { 87 ls labels.Selector 88 ch chan *v1.Pod 89 } 90 91 func (c *FakeK8sClient) EmitService(ls labels.Selector, s *v1.Service) { 92 c.podWatcherMu.Lock() 93 defer c.podWatcherMu.Unlock() 94 for _, w := range c.serviceWatches { 95 if SelectorEqual(ls, w.ls) { 96 w.ch <- s 97 } 98 } 99 } 100 101 func (c *FakeK8sClient) WatchServices(ctx context.Context, ls labels.Selector) (<-chan *v1.Service, error) { 102 c.serviceWatcherMu.Lock() 103 ch := make(chan *v1.Service, 20) 104 c.serviceWatches = append(c.serviceWatches, fakeServiceWatch{ls, ch}) 105 c.serviceWatcherMu.Unlock() 106 107 go func() { 108 // when ctx is canceled, remove the label selector from the list of watched label selectors 109 <-ctx.Done() 110 c.serviceWatcherMu.Lock() 111 var newWatches []fakeServiceWatch 112 for _, e := range c.serviceWatches { 113 if !SelectorEqual(e.ls, ls) { 114 newWatches = append(newWatches, e) 115 } 116 } 117 c.serviceWatches = newWatches 118 c.serviceWatcherMu.Unlock() 119 }() 120 return ch, nil 121 } 122 123 func (c *FakeK8sClient) WatchEvents(ctx context.Context) (<-chan *v1.Event, error) { 124 if c.EventsWatchErr != nil { 125 err := c.EventsWatchErr 126 c.EventsWatchErr = nil 127 return nil, err 128 } 129 130 return c.eventsCh, nil 131 } 132 133 func (c *FakeK8sClient) EmitEvent(ctx context.Context, evt *v1.Event) { 134 c.eventsCh <- evt 135 } 136 137 func (c *FakeK8sClient) WatchedSelectors() []labels.Selector { 138 c.podWatcherMu.Lock() 139 defer c.podWatcherMu.Unlock() 140 var ret []labels.Selector 141 for _, w := range c.podWatches { 142 ret = append(ret, w.ls) 143 } 144 return ret 145 } 146 147 func (c *FakeK8sClient) EmitPod(ls labels.Selector, p *v1.Pod) { 148 c.podWatcherMu.Lock() 149 defer c.podWatcherMu.Unlock() 150 for _, w := range c.podWatches { 151 if SelectorEqual(ls, w.ls) { 152 w.ch <- p 153 } 154 } 155 } 156 157 func (c *FakeK8sClient) WatchPods(ctx context.Context, ls labels.Selector) (<-chan *v1.Pod, error) { 158 c.podWatcherMu.Lock() 159 ch := make(chan *v1.Pod, 20) 160 c.podWatches = append(c.podWatches, fakePodWatch{ls, ch}) 161 c.podWatcherMu.Unlock() 162 163 go func() { 164 // when ctx is canceled, remove the label selector from the list of watched label selectors 165 <-ctx.Done() 166 c.podWatcherMu.Lock() 167 var newWatches []fakePodWatch 168 for _, e := range c.podWatches { 169 if !SelectorEqual(e.ls, ls) { 170 newWatches = append(newWatches, e) 171 } 172 } 173 c.podWatches = newWatches 174 c.podWatcherMu.Unlock() 175 }() 176 return ch, nil 177 } 178 179 func NewFakeK8sClient() *FakeK8sClient { 180 return &FakeK8sClient{ 181 PodLogsByPodAndContainer: make(map[PodAndCName]BufferCloser), 182 eventsCh: make(chan *v1.Event, 10), 183 } 184 } 185 186 func (c *FakeK8sClient) TearDown() { 187 if c.eventsCh != nil { 188 close(c.eventsCh) 189 } 190 } 191 192 func (c *FakeK8sClient) ConnectedToCluster(ctx context.Context) error { 193 return nil 194 } 195 196 func (c *FakeK8sClient) Upsert(ctx context.Context, entities []K8sEntity) ([]K8sEntity, error) { 197 if c.UpsertError != nil { 198 return nil, c.UpsertError 199 } 200 yaml, err := SerializeSpecYAML(entities) 201 if err != nil { 202 return nil, errors.Wrap(err, "kubectl apply") 203 } 204 c.Yaml = yaml 205 206 result := make([]K8sEntity, 0, len(entities)) 207 208 for _, e := range entities { 209 clone := e.DeepCopy() 210 err = SetUID(&clone, uuid.New().String()) 211 if err != nil { 212 return nil, errors.Wrap(err, "Upsert: generating UUID") 213 } 214 result = append(result, clone) 215 } 216 217 c.LastUpsertResult = result 218 return result, nil 219 } 220 221 func (c *FakeK8sClient) Delete(ctx context.Context, entities []K8sEntity) error { 222 if c.DeleteError != nil { 223 err := c.DeleteError 224 c.DeleteError = nil 225 return err 226 } 227 228 yaml, err := SerializeSpecYAML(entities) 229 if err != nil { 230 return errors.Wrap(err, "kubectl delete") 231 } 232 c.DeletedYaml = yaml 233 return nil 234 } 235 236 func (c *FakeK8sClient) InjectEntityByName(entities ...K8sEntity) { 237 if c.entityByName == nil { 238 c.entityByName = make(map[string]K8sEntity) 239 } 240 for _, entity := range entities { 241 c.entityByName[entity.Name()] = entity 242 } 243 } 244 245 func (c *FakeK8sClient) GetByReference(ctx context.Context, ref v1.ObjectReference) (K8sEntity, error) { 246 c.getByReferenceCallCount++ 247 resp, ok := c.entityByName[ref.Name] 248 if !ok { 249 logger.Get(ctx).Infof("FakeK8sClient.GetByReference: resource not found: %s", ref.Name) 250 return K8sEntity{}, apierrors.NewNotFound(v1.Resource(ref.Kind), ref.Name) 251 } 252 return resp, nil 253 } 254 255 func (c *FakeK8sClient) WatchPod(ctx context.Context, pod *v1.Pod) (watch.Interface, error) { 256 return watch.NewEmptyWatch(), nil 257 } 258 259 func (c *FakeK8sClient) SetLogsForPodContainer(pID PodID, cName container.Name, logs string) { 260 c.PodLogsByPodAndContainer[PodAndCName{pID, cName}] = BufferCloser{Buffer: bytes.NewBufferString(logs)} 261 } 262 263 func (c *FakeK8sClient) ContainerLogs(ctx context.Context, pID PodID, cName container.Name, n Namespace, startTime time.Time) (io.ReadCloser, error) { 264 if c.ContainerLogsError != nil { 265 return nil, c.ContainerLogsError 266 } 267 268 // If we have specific logs for this pod/container combo, return those 269 c.LastPodLogStartTime = startTime 270 if buf, ok := c.PodLogsByPodAndContainer[PodAndCName{pID, cName}]; ok { 271 return buf, nil 272 } 273 274 return BufferCloser{Buffer: bytes.NewBuffer(nil)}, nil 275 } 276 277 func (c *FakeK8sClient) PodByID(ctx context.Context, pID PodID, n Namespace) (*v1.Pod, error) { 278 return nil, nil 279 } 280 281 func FakePodStatus(image reference.NamedTagged, phase string) v1.PodStatus { 282 return v1.PodStatus{ 283 Phase: v1.PodPhase(phase), 284 ContainerStatuses: []v1.ContainerStatus{ 285 { 286 Name: "main", 287 ContainerID: "docker://" + MagicTestContainerID, 288 Image: image.String(), 289 Ready: true, 290 }, 291 { 292 Name: "tilt-synclet", 293 ContainerID: "docker://tilt-testsynclet", 294 // can't use the constants in synclet because that would create a dep cycle 295 Image: "gcr.io/windmill-public-containers/tilt-synclet:latest", 296 Ready: true, 297 }, 298 }, 299 } 300 } 301 302 func FakePodSpec(image reference.NamedTagged) v1.PodSpec { 303 return v1.PodSpec{ 304 Containers: []v1.Container{ 305 { 306 Name: "main", 307 Image: image.String(), 308 Ports: []v1.ContainerPort{ 309 { 310 ContainerPort: 8080, 311 }, 312 }, 313 }, 314 { 315 Name: "tilt-synclet", 316 Image: "gcr.io/windmill-public-containers/tilt-synclet:latest", 317 }, 318 }, 319 } 320 } 321 322 func (c *FakeK8sClient) applyWasCalled() bool { 323 return c.Yaml != "" 324 } 325 326 func (c *FakeK8sClient) CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, optionalLocalPort, remotePort int, host string) (PortForwarder, error) { 327 pfc := &(c.FakePortForwardClient) 328 return pfc.CreatePortForwarder(ctx, namespace, podID, optionalLocalPort, remotePort, host) 329 } 330 331 func (c *FakeK8sClient) ContainerRuntime(ctx context.Context) container.Runtime { 332 if c.Runtime != "" { 333 return c.Runtime 334 } 335 return container.RuntimeDocker 336 } 337 338 func (c *FakeK8sClient) PrivateRegistry(ctx context.Context) container.Registry { 339 return c.Registry 340 } 341 342 func (c *FakeK8sClient) Exec(ctx context.Context, podID PodID, cName container.Name, n Namespace, cmd []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 343 var stdinBytes []byte 344 var err error 345 if stdin != nil { 346 stdinBytes, err = ioutil.ReadAll(stdin) 347 if err != nil { 348 return errors.Wrap(err, "reading Exec stdin") 349 } 350 } 351 352 c.ExecCalls = append(c.ExecCalls, ExecCall{ 353 PID: podID, 354 CName: cName, 355 Ns: n, 356 Cmd: cmd, 357 Stdin: stdinBytes, 358 }) 359 360 if len(c.ExecErrors) > 0 { 361 err = c.ExecErrors[0] 362 c.ExecErrors = c.ExecErrors[1:] 363 return err 364 } 365 return nil 366 } 367 368 type BufferCloser struct { 369 *bytes.Buffer 370 } 371 372 func (b BufferCloser) Close() error { 373 return nil 374 } 375 376 var _ io.ReadCloser = BufferCloser{} 377 378 type FakePortForwarder struct { 379 localPort int 380 ctx context.Context 381 Done chan error 382 } 383 384 func (pf FakePortForwarder) LocalPort() int { 385 return pf.localPort 386 } 387 388 func (pf FakePortForwarder) ForwardPorts() error { 389 select { 390 case <-pf.ctx.Done(): 391 return pf.ctx.Err() 392 case <-pf.Done: 393 return nil 394 } 395 } 396 397 type FakePortForwardClient struct { 398 CreatePortForwardCallCount int 399 LastForwardPortPodID PodID 400 LastForwardPortRemotePort int 401 LastForwardPortHost string 402 LastForwarder FakePortForwarder 403 LastForwardContext context.Context 404 } 405 406 func (c *FakePortForwardClient) CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, optionalLocalPort, remotePort int, host string) (PortForwarder, error) { 407 c.CreatePortForwardCallCount++ 408 c.LastForwardContext = ctx 409 c.LastForwardPortPodID = podID 410 c.LastForwardPortRemotePort = remotePort 411 c.LastForwardPortHost = host 412 413 result := FakePortForwarder{ 414 localPort: optionalLocalPort, 415 ctx: ctx, 416 Done: make(chan error), 417 } 418 c.LastForwarder = result 419 return result, nil 420 }