github.com/xiaobinqt/libcompose@v1.1.0/docker/container/container.go (about) 1 package container 2 3 import ( 4 "fmt" 5 "io" 6 "math" 7 "os" 8 "strconv" 9 "strings" 10 "time" 11 12 "golang.org/x/net/context" 13 14 "github.com/docker/docker/api/types" 15 "github.com/docker/docker/api/types/container" 16 "github.com/docker/docker/api/types/network" 17 "github.com/docker/docker/client" 18 "github.com/docker/docker/pkg/stdcopy" 19 "github.com/docker/docker/pkg/term" 20 "github.com/docker/go-connections/nat" 21 "github.com/sirupsen/logrus" 22 "github.com/xiaobinqt/libcompose/config" 23 "github.com/xiaobinqt/libcompose/labels" 24 "github.com/xiaobinqt/libcompose/logger" 25 "github.com/xiaobinqt/libcompose/project" 26 ) 27 28 // Container holds information about a docker container and the service it is tied on. 29 type Container struct { 30 client client.ContainerAPIClient 31 id string 32 container *types.ContainerJSON 33 } 34 35 // Create creates a container and return a Container struct (and an error if any) 36 func Create(ctx context.Context, client client.ContainerAPIClient, name string, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) (*Container, error) { 37 container, err := client.ContainerCreate(ctx, config, hostConfig, networkingConfig, name) 38 if err != nil { 39 return nil, err 40 } 41 return New(ctx, client, container.ID) 42 } 43 44 // New creates a container struct with the specified client, id and name 45 func New(ctx context.Context, client client.ContainerAPIClient, id string) (*Container, error) { 46 container, err := Get(ctx, client, id) 47 if err != nil { 48 return nil, err 49 } 50 return &Container{ 51 client: client, 52 id: id, 53 container: container, 54 }, nil 55 } 56 57 // NewInspected creates a container struct from an inspected container 58 func NewInspected(client client.ContainerAPIClient, container *types.ContainerJSON) *Container { 59 return &Container{ 60 client: client, 61 id: container.ID, 62 container: container, 63 } 64 } 65 66 // Info returns info about the container, like name, command, state or ports. 67 func (c *Container) Info(ctx context.Context) (project.Info, error) { 68 infos, err := ListByFilter(ctx, c.client, map[string][]string{ 69 "name": {c.container.Name}, 70 }) 71 if err != nil || len(infos) == 0 { 72 return nil, err 73 } 74 info := infos[0] 75 76 result := project.Info{} 77 result["Id"] = c.container.ID 78 result["Name"] = name(info.Names) 79 result["Command"] = info.Command 80 result["State"] = info.Status 81 result["Ports"] = portString(info.Ports) 82 83 return result, nil 84 } 85 86 func portString(ports []types.Port) string { 87 result := []string{} 88 89 for _, port := range ports { 90 if port.PublicPort > 0 { 91 result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type)) 92 } else { 93 result = append(result, fmt.Sprintf("%d/%s", port.PrivatePort, port.Type)) 94 } 95 } 96 97 return strings.Join(result, ", ") 98 } 99 100 func name(names []string) string { 101 max := math.MaxInt32 102 var current string 103 104 for _, v := range names { 105 if len(v) < max { 106 max = len(v) 107 current = v 108 } 109 } 110 111 return current[1:] 112 } 113 114 // Rename rename the container. 115 func (c *Container) Rename(ctx context.Context, newName string) error { 116 return c.client.ContainerRename(ctx, c.container.ID, newName) 117 } 118 119 // Remove removes the container. 120 func (c *Container) Remove(ctx context.Context, removeVolume bool) error { 121 return c.client.ContainerRemove(ctx, c.container.ID, types.ContainerRemoveOptions{ 122 Force: true, 123 RemoveVolumes: removeVolume, 124 }) 125 } 126 127 // Stop stops the container. 128 func (c *Container) Stop(ctx context.Context, timeout int) error { 129 timeoutDuration := time.Duration(timeout) * time.Second 130 return c.client.ContainerStop(ctx, c.container.ID, &timeoutDuration) 131 } 132 133 // Pause pauses the container. If the containers are already paused, don't fail. 134 func (c *Container) Pause(ctx context.Context) error { 135 if !c.container.State.Paused { 136 if err := c.client.ContainerPause(ctx, c.container.ID); err != nil { 137 return err 138 } 139 return c.updateInnerContainer(ctx) 140 } 141 return nil 142 } 143 144 // Unpause unpauses the container. If the containers are not paused, don't fail. 145 func (c *Container) Unpause(ctx context.Context) error { 146 if c.container.State.Paused { 147 if err := c.client.ContainerUnpause(ctx, c.container.ID); err != nil { 148 return err 149 } 150 return c.updateInnerContainer(ctx) 151 } 152 return nil 153 } 154 155 func (c *Container) updateInnerContainer(ctx context.Context) error { 156 container, err := Get(ctx, c.client, c.container.ID) 157 if err != nil { 158 return err 159 } 160 c.container = container 161 return nil 162 } 163 164 // Kill kill the container. 165 func (c *Container) Kill(ctx context.Context, signal string) error { 166 return c.client.ContainerKill(ctx, c.container.ID, signal) 167 } 168 169 // IsRunning returns the running state of the container. 170 func (c *Container) IsRunning(ctx context.Context) bool { 171 return c.container.State.Running 172 } 173 174 // Run creates, start and attach to the container based on the image name, 175 // the specified configuration. 176 // It will always create a new container. 177 func (c *Container) Run(ctx context.Context, configOverride *config.ServiceConfig) (int, error) { 178 var ( 179 errCh chan error 180 out, stderr io.Writer 181 in io.ReadCloser 182 inFd uintptr 183 ) 184 185 if configOverride.StdinOpen { 186 in = os.Stdin 187 } 188 if configOverride.Tty { 189 out = os.Stdout 190 stderr = os.Stderr 191 } 192 193 options := types.ContainerAttachOptions{ 194 Stream: true, 195 Stdin: configOverride.StdinOpen, 196 Stdout: configOverride.Tty, 197 Stderr: configOverride.Tty, 198 } 199 200 resp, err := c.client.ContainerAttach(ctx, c.container.ID, options) 201 if err != nil { 202 return -1, err 203 } 204 205 if configOverride.StdinOpen { 206 // set raw terminal 207 inFd, _ = term.GetFdInfo(in) 208 state, err := term.SetRawTerminal(inFd) 209 if err != nil { 210 return -1, err 211 } 212 // restore raw terminal 213 defer term.RestoreTerminal(inFd, state) 214 } 215 216 // holdHijackedConnection (in goroutine) 217 errCh = make(chan error, 1) 218 go func() { 219 errCh <- holdHijackedConnection(configOverride.Tty, in, out, stderr, resp) 220 }() 221 222 if err := c.client.ContainerStart(ctx, c.container.ID, types.ContainerStartOptions{}); err != nil { 223 return -1, err 224 } 225 226 if configOverride.Tty { 227 ws, err := term.GetWinsize(inFd) 228 if err != nil { 229 return -1, err 230 } 231 232 resizeOpts := types.ResizeOptions{ 233 Height: uint(ws.Height), 234 Width: uint(ws.Width), 235 } 236 237 if err := c.client.ContainerResize(ctx, c.container.ID, resizeOpts); err != nil { 238 return -1, err 239 } 240 } 241 242 if err := <-errCh; err != nil { 243 logrus.Debugf("Error hijack: %s", err) 244 return -1, err 245 } 246 247 exitedContainer, err := c.client.ContainerInspect(ctx, c.container.ID) 248 if err != nil { 249 return -1, err 250 } 251 252 return exitedContainer.State.ExitCode, nil 253 } 254 255 func holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { 256 var err error 257 receiveStdout := make(chan error, 1) 258 if outputStream != nil || errorStream != nil { 259 go func() { 260 // When TTY is ON, use regular copy 261 if tty && outputStream != nil { 262 _, err = io.Copy(outputStream, resp.Reader) 263 } else { 264 _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) 265 } 266 logrus.Debugf("[hijack] End of stdout") 267 receiveStdout <- err 268 }() 269 } 270 271 stdinDone := make(chan struct{}) 272 go func() { 273 if inputStream != nil { 274 io.Copy(resp.Conn, inputStream) 275 logrus.Debugf("[hijack] End of stdin") 276 } 277 278 if err := resp.CloseWrite(); err != nil { 279 logrus.Debugf("Couldn't send EOF: %s", err) 280 } 281 close(stdinDone) 282 }() 283 284 select { 285 case err := <-receiveStdout: 286 if err != nil { 287 logrus.Debugf("Error receiveStdout: %s", err) 288 return err 289 } 290 case <-stdinDone: 291 if outputStream != nil || errorStream != nil { 292 if err := <-receiveStdout; err != nil { 293 logrus.Debugf("Error receiveStdout: %s", err) 294 return err 295 } 296 } 297 } 298 299 return nil 300 } 301 302 // Start the specified container with the specified host config 303 func (c *Container) Start(ctx context.Context) error { 304 logrus.WithFields(logrus.Fields{"container.ID": c.container.ID, "container.Name": c.container.Name}).Debug("Starting container") 305 if err := c.client.ContainerStart(ctx, c.container.ID, types.ContainerStartOptions{}); err != nil { 306 logrus.WithFields(logrus.Fields{"container.ID": c.container.ID, "container.Name": c.container.Name}).Debug("Failed to start container") 307 return err 308 } 309 return nil 310 } 311 312 // Restart restarts the container if existing, does nothing otherwise. 313 func (c *Container) Restart(ctx context.Context, timeout int) error { 314 timeoutDuration := time.Duration(timeout) * time.Second 315 return c.client.ContainerRestart(ctx, c.container.ID, &timeoutDuration) 316 } 317 318 // Log forwards container logs to the project configured logger. 319 func (c *Container) Log(ctx context.Context, l logger.Logger, follow bool) error { 320 info, err := c.client.ContainerInspect(ctx, c.container.ID) 321 if err != nil { 322 return err 323 } 324 325 options := types.ContainerLogsOptions{ 326 ShowStdout: true, 327 ShowStderr: true, 328 Follow: follow, 329 Tail: "all", 330 } 331 responseBody, err := c.client.ContainerLogs(ctx, c.container.ID, options) 332 if err != nil { 333 return err 334 } 335 defer responseBody.Close() 336 337 if info.Config.Tty { 338 _, err = io.Copy(&logger.Wrapper{Logger: l}, responseBody) 339 } else { 340 _, err = stdcopy.StdCopy(&logger.Wrapper{Logger: l}, &logger.Wrapper{Logger: l, Err: true}, responseBody) 341 } 342 logrus.WithFields(logrus.Fields{"Logger": l, "err": err}).Debug("c.client.Logs() returned error") 343 344 return err 345 } 346 347 // Port returns the host port the specified port is mapped on. 348 func (c *Container) Port(ctx context.Context, port string) (string, error) { 349 if bindings, ok := c.container.NetworkSettings.Ports[nat.Port(port)]; ok { 350 result := []string{} 351 for _, binding := range bindings { 352 result = append(result, binding.HostIP+":"+binding.HostPort) 353 } 354 355 return strings.Join(result, "\n"), nil 356 } 357 return "", nil 358 } 359 360 // Networks returns the containers network 361 func (c *Container) Networks() (map[string]*network.EndpointSettings, error) { 362 return c.container.NetworkSettings.Networks, nil 363 } 364 365 // ID returns the container Id. 366 func (c *Container) ID() string { 367 return c.container.ID 368 } 369 370 // ShortID return the container Id in its short form 371 func (c *Container) ShortID() string { 372 return c.container.ID[:12] 373 } 374 375 // Name returns the container name. 376 func (c *Container) Name() string { 377 return c.container.Name 378 } 379 380 // Image returns the container image. Depending on the engine version its either 381 // the complete id or the digest reference the image. 382 func (c *Container) Image() string { 383 return c.container.Image 384 } 385 386 // ImageConfig returns the container image stored in the config. It's the 387 // human-readable name of the image. 388 func (c *Container) ImageConfig() string { 389 return c.container.Config.Image 390 } 391 392 // Hash returns the container hash stored as label. 393 func (c *Container) Hash() string { 394 return c.container.Config.Labels[labels.HASH.Str()] 395 } 396 397 // Number returns the container number stored as label. 398 func (c *Container) Number() (int, error) { 399 numberStr := c.container.Config.Labels[labels.NUMBER.Str()] 400 return strconv.Atoi(numberStr) 401 }