-
-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathoperations.go
More file actions
458 lines (409 loc) · 17.6 KB
/
operations.go
File metadata and controls
458 lines (409 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
package devcontainer
import (
"context"
"fmt"
"os"
"strings"
"time"
errUtils "github.com/cloudposse/atmos/errors"
"github.com/cloudposse/atmos/pkg/container"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/ui"
"github.com/cloudposse/atmos/pkg/ui/spinner"
)
const (
// DefaultContainerStopTimeout is the default timeout for stopping containers.
defaultContainerStopTimeout = 10 * time.Second
)
// buildConfigLoadError builds a standardized error for configuration loading failures.
func buildConfigLoadError(err error, name string) error {
return errUtils.Build(err).
WithExplanationf("Failed to load devcontainer configuration for `%s`", name).
WithHintf("Verify that the devcontainer is defined in `atmos.yaml` under `components.devcontainer.%s`", name).
WithHint("Run `atmos devcontainer list` to see all available devcontainers").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/configuration/").
WithContext("devcontainer_name", name).
WithExitCode(2).
Err()
}
// buildRuntimeDetectError builds a standardized error for runtime detection failures.
func buildRuntimeDetectError(err error, name, runtime string) error {
return errUtils.Build(errUtils.ErrRuntimeNotAvailable).
WithCause(err).
WithExplanation("Failed to detect or initialize container runtime").
WithHint("Ensure Docker or Podman is installed and running").
WithHint("Run `docker info` or `podman info` to verify the runtime is accessible").
WithHint("See Docker installation: https://docs.docker.com/get-docker/").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("devcontainer_name", name).
WithContext("runtime", runtime).
WithExitCode(3).
Err()
}
// buildContainerNameError builds a standardized error for container name generation failures.
func buildContainerNameError(err error, name, instance string) error {
return errUtils.Build(err).
WithExplanationf("Failed to generate valid container name from devcontainer `%s` and instance `%s`", name, instance).
WithHint("Container names must be lowercase alphanumeric with hyphens only").
WithHint("Ensure the devcontainer name and instance follow naming conventions").
WithHint("See Docker naming: https://docs.docker.com/engine/reference/commandline/create/#name").
WithContext("devcontainer_name", name).
WithContext("instance", instance).
WithExitCode(2).
Err()
}
// buildContainerListError builds a standardized error for container listing failures.
func buildContainerListError(err error, name, containerName, runtime string) error {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to list containers with name `%s`", containerName).
WithHint("Verify that the container runtime is accessible and running").
WithHint("Run `docker ps -a` or `podman ps -a` to check container status").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("devcontainer_name", name).
WithContext("container_name", containerName).
WithContext("runtime", runtime).
WithExitCode(3).
Err()
}
// buildContainerInspectError builds a standardized error for container inspect failures.
func buildContainerInspectError(err error, name, containerName, runtime string) error {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to inspect container `%s`", containerName).
WithHint("Check that the container runtime daemon is running").
WithHint("Run `docker ps -a` or `podman ps -a` to see all containers").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("devcontainer_name", name).
WithContext("container_name", containerName).
WithContext("runtime", runtime).
WithExitCode(3).
Err()
}
// buildIdentityInjectionError builds a standardized error for identity injection failures.
func buildIdentityInjectionError(err error, name, identityName string) error {
return errUtils.Build(err).
WithExplanationf("Failed to inject identity `%s` into devcontainer environment", identityName).
WithHintf("Verify that the identity `%s` is configured in `atmos.yaml`", identityName).
WithHint("Run `atmos auth identity list` to see available identities").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/auth/auth-identity-configure/").
WithContext("devcontainer_name", name).
WithContext("identity_name", identityName).
WithExitCode(2).
Err()
}
// containerParams holds parameters for container operations.
type containerParams struct {
ctx context.Context
runtime container.Runtime
config *Config
containerName string
name string
instance string
}
// runWithSpinner is a wrapper for spinner.ExecWithSpinner for backwards compatibility.
func runWithSpinner(progressMsg, completedMsg string, operation func() error) error {
return spinner.ExecWithSpinner(progressMsg, completedMsg, operation)
}
// createAndStartNewContainer creates and starts a new container.
func createAndStartNewContainer(params *containerParams) error {
// Build image if build configuration is specified.
if err := buildImageIfNeeded(params.ctx, params.runtime, params.config, params.name); err != nil {
return errUtils.Build(err).
WithContext("devcontainer_name", params.name).
WithContext("container_name", params.containerName).
Err()
}
containerID, err := createContainer(params)
if err != nil {
return errUtils.Build(err).
WithContext("devcontainer_name", params.name).
WithContext("container_name", params.containerName).
Err()
}
if err := startContainer(params.ctx, params.runtime, containerID, params.containerName); err != nil {
return errUtils.Build(err).
WithContext("devcontainer_name", params.name).
WithContext("container_name", params.containerName).
WithContext("container_id", containerID).
Err()
}
// Inspect container to get actual port information after creation.
containerInfo, err := params.runtime.Inspect(params.ctx, containerID)
if err != nil {
log.Warn("Failed to inspect container for port info", "error", err)
displayContainerInfo(params.config, nil)
} else {
displayContainerInfo(params.config, containerInfo)
}
return nil
}
// stopAndRemoveContainer stops and removes a container if it exists.
func stopAndRemoveContainer(ctx context.Context, runtime container.Runtime, containerName string) error {
containerInfo, err := runtime.Inspect(ctx, containerName)
if err != nil {
// Container doesn't exist - nothing to stop/remove.
return nil //nolint:nilerr // intentionally ignoring error when container doesn't exist
}
if err := stopContainerIfRunning(ctx, runtime, containerInfo); err != nil {
return errUtils.Build(err).
WithContext("container_name", containerName).
WithContext("container_id", containerInfo.ID).
Err()
}
return errUtils.Build(removeContainer(ctx, runtime, containerInfo, containerName)).
WithContext("container_name", containerName).
WithContext("container_id", containerInfo.ID).
Err()
}
// stopContainerIfRunning stops a container if it's running.
func stopContainerIfRunning(ctx context.Context, runtime container.Runtime, containerInfo *container.Info) error {
if !isContainerRunning(containerInfo.Status) {
return nil
}
return runWithSpinner(
fmt.Sprintf("Stopping container %s", containerInfo.Name),
fmt.Sprintf("Stopped container %s", containerInfo.Name),
func() error {
if err := runtime.Stop(ctx, containerInfo.ID, defaultContainerStopTimeout); err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to stop container `%s` (ID: %s)", containerInfo.Name, containerInfo.ID).
WithHint("Check that the container runtime daemon is running").
WithHintf("Run `atmos devcontainer logs %s` to check container logs", extractDevcontainerName(containerInfo.Name)).
WithHint("The container may be stuck or require a forced removal").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("container_name", containerInfo.Name).
WithContext("container_id", containerInfo.ID).
WithExitCode(3).
Err()
}
return nil
})
}
// removeContainer removes a container.
func removeContainer(ctx context.Context, runtime container.Runtime, containerInfo *container.Info, containerName string) error {
return runWithSpinner(
fmt.Sprintf("Removing container %s", containerName),
fmt.Sprintf("Removed container %s", containerName),
func() error {
if err := runtime.Remove(ctx, containerInfo.ID, true); err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to remove container `%s` (ID: %s)", containerName, containerInfo.ID).
WithHint("Check that the container runtime daemon is running").
WithHintf("Run `atmos devcontainer logs %s` to check container logs", extractDevcontainerName(containerName)).
WithHint("If the container is running, stop it first with `atmos devcontainer stop`").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("container_name", containerName).
WithContext("container_id", containerInfo.ID).
WithExitCode(3).
Err()
}
return nil
})
}
// pullImageIfNeeded pulls an image unless noPull is true or image is empty.
func pullImageIfNeeded(ctx context.Context, runtime container.Runtime, image string, noPull bool) error {
if noPull || image == "" {
return nil
}
return runWithSpinner(
fmt.Sprintf("Pulling image %s", image),
fmt.Sprintf("Pulled image %s", image),
func() error {
if err := runtime.Pull(ctx, image); err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to pull container image `%s`", image).
WithHintf("Verify that the image name `%s` is correct and accessible", image).
WithHint("Check that you have network connectivity and proper registry credentials").
WithHint("If using a private registry, ensure you are logged in with `docker login` or `podman login`").
WithHint("See Docker Hub: https://hub.docker.com/").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/configuration/").
WithContext("image", image).
WithExitCode(3).
Err()
}
return nil
})
}
// createContainer creates a new container.
func createContainer(params *containerParams) (string, error) {
var containerID string
err := runWithSpinner(
fmt.Sprintf("Creating container %s", params.containerName),
fmt.Sprintf("Created container %s", params.containerName),
func() error {
createConfig := ToCreateConfig(params.config, params.containerName, params.name, params.instance)
id, err := params.runtime.Create(params.ctx, createConfig)
if err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to create container `%s`", params.containerName).
WithHintf("Verify that the image `%s` exists or can be pulled", params.config.Image).
WithHint("Check that the container runtime daemon is running").
WithHint("Run `docker images` or `podman images` to see available images").
WithHint("See DevContainer spec: https://containers.dev/implementors/json_reference/").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/configuration/").
WithContext("container_name", params.containerName).
WithContext("devcontainer_name", params.name).
WithContext("image", params.config.Image).
WithExitCode(3).
Err()
}
containerID = id
return nil
})
return containerID, err
}
// startContainer starts a container.
func startContainer(ctx context.Context, runtime container.Runtime, containerID, containerName string) error {
return runWithSpinner(
fmt.Sprintf("Starting container %s", containerName),
fmt.Sprintf("Started container %s", containerName),
func() error {
if err := runtime.Start(ctx, containerID); err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to start container `%s` (ID: %s)", containerName, containerID).
WithHint("If the container already exists, use `--replace` flag to remove and recreate it").
WithHint("Check that the container runtime daemon is running").
WithHintf("Run `atmos devcontainer config %s` to see devcontainer configuration", extractDevcontainerName(containerName)).
WithHint("The container may have configuration issues preventing startup").
WithHintf("Check container logs with `atmos devcontainer logs %s`", extractDevcontainerName(containerName)).
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/").
WithContext("container_name", containerName).
WithContext("container_id", containerID).
WithExitCode(3).
Err()
}
return nil
})
}
// isContainerRunning checks if a container status indicates it's running.
func isContainerRunning(status string) bool {
// Container status values can be "running", "Running", "Up", etc.
// We check for common running indicators.
return status == "running" || status == "Running" || status == "Up"
}
// buildImageIfNeeded builds a container image if build configuration is specified.
func buildImageIfNeeded(ctx context.Context, runtime container.Runtime, config *Config, devcontainerName string) error {
// If no build config, image must be specified (already validated).
if config.Build == nil {
return nil
}
// Generate image name based on devcontainer name.
imageName := fmt.Sprintf("atmos-devcontainer-%s", devcontainerName)
// Build the image.
return runWithSpinner(
fmt.Sprintf("Building image %s", imageName),
fmt.Sprintf("Built image %s", imageName),
func() error {
buildConfig := &container.BuildConfig{
Context: config.Build.Context,
Dockerfile: config.Build.Dockerfile,
Tags: []string{imageName},
Args: config.Build.Args,
}
if err := runtime.Build(ctx, buildConfig); err != nil {
return errUtils.Build(errUtils.ErrContainerRuntimeOperation).
WithCause(err).
WithExplanationf("Failed to build container image `%s` from Dockerfile", imageName).
WithHintf("Verify that the Dockerfile exists at `%s`", config.Build.Dockerfile).
WithHintf("Verify that the build context path `%s` is correct", config.Build.Context).
WithHint("Check that the container runtime daemon is running").
WithHint("Review the Dockerfile for syntax errors or invalid instructions").
WithHint("See DevContainer build spec: https://containers.dev/implementors/json_reference/#build-properties").
WithHint("See Atmos docs: https://atmos.tools/cli/commands/devcontainer/configuration/").
WithExample(`devcontainer:
my-dev:
spec:
build:
context: .
dockerfile: Dockerfile
args:
VARIANT: "1.24"`).
WithContext("devcontainer_name", devcontainerName).
WithContext("image_name", imageName).
WithContext("dockerfile", config.Build.Dockerfile).
WithContext("context", config.Build.Context).
WithExitCode(3).
Err()
}
// Update config to use the built image name.
config.Image = imageName
return nil
})
}
// displayContainerInfo displays key information about the container in a user-friendly format.
// If containerInfo is provided, shows actual runtime ports; otherwise shows configured ports.
func displayContainerInfo(config *Config, containerInfo *container.Info) {
var info []string
// Show image.
if config.Image != "" {
info = append(info, fmt.Sprintf("**Image:** %s", config.Image))
}
// Show workspace mount.
if workspaceInfo := formatWorkspaceInfo(config.WorkspaceFolder); workspaceInfo != "" {
info = append(info, workspaceInfo)
}
// Show actual runtime ports if available, otherwise show configured ports.
if containerInfo != nil && len(containerInfo.Ports) > 0 {
info = append(info, fmt.Sprintf("**Ports:** %s", FormatPortBindings(containerInfo.Ports)))
} else if portsInfo := formatPortsInfo(config.ForwardPorts); portsInfo != "" {
info = append(info, portsInfo)
}
if len(info) > 0 {
_ = ui.Info(strings.Join(info, "\n"))
}
}
// formatWorkspaceInfo formats the workspace folder information.
func formatWorkspaceInfo(workspaceFolder string) string {
if workspaceFolder == "" {
return ""
}
cwd, _ := os.Getwd()
if cwd != "" {
return fmt.Sprintf("**Workspace:** %s → %s", cwd, workspaceFolder)
}
return fmt.Sprintf("**Workspace folder:** %s", workspaceFolder)
}
// formatPortsInfo formats the forwarded ports information.
func formatPortsInfo(forwardPorts []interface{}) string {
if len(forwardPorts) == 0 {
return ""
}
var ports []string
for _, port := range forwardPorts {
switch v := port.(type) {
case int:
ports = append(ports, fmt.Sprintf("%d", v))
case float64:
ports = append(ports, fmt.Sprintf("%d", int(v)))
case string:
ports = append(ports, v)
}
}
if len(ports) > 0 {
return fmt.Sprintf("**Ports:** %s", strings.Join(ports, ", "))
}
return ""
}
// extractDevcontainerName extracts the devcontainer name from a full container name.
// Container names follow the format: atmos-devcontainer.<name>.<instance>[-<suffix>].
// For example: "atmos-devcontainer.geodesic.default-2" returns "geodesic".
func extractDevcontainerName(containerName string) string {
// Remove "atmos-devcontainer." prefix.
const prefix = "atmos-devcontainer."
if !strings.HasPrefix(containerName, prefix) {
return containerName
}
remainder := strings.TrimPrefix(containerName, prefix)
// Split by "." to get name part.
parts := strings.Split(remainder, ".")
if len(parts) > 0 {
return parts[0]
}
return containerName
}