• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

astronomer / astro-cli / 3594e26d-8682-4297-8627-84e8d776b975

27 Oct 2025 08:29PM UTC coverage: 38.52% (+0.01%) from 38.51%
3594e26d-8682-4297-8627-84e8d776b975

Pull #1968

circleci

ronidelacruz-astro
Merge branch 'fix-verbosity-issue' of github.com:astronomer/astro-cli into fix-verbosity-issue
Pull Request #1968: Verbosity flag provides no extra information (#1501)

15 of 20 new or added lines in 3 files covered. (75.0%)

46 existing lines in 2 files now uncovered.

24148 of 62689 relevant lines covered (38.52%)

10.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.48
/airflow/docker_image.go
1
package airflow
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "context"
7
        "encoding/base64"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "io"
12
        "os"
13
        "os/exec"
14
        "strings"
15

16
        "github.com/astronomer/astro-cli/airflow/runtimes"
17
        airflowTypes "github.com/astronomer/astro-cli/airflow/types"
18
        "github.com/astronomer/astro-cli/config"
19
        "github.com/astronomer/astro-cli/pkg/logger"
20
        "github.com/astronomer/astro-cli/pkg/spinner"
21
        "github.com/astronomer/astro-cli/pkg/util"
22
        cliConfig "github.com/docker/cli/cli/config"
23
        cliTypes "github.com/docker/cli/cli/config/types"
24
        "github.com/docker/cli/cli/streams"
25
        "github.com/docker/docker/api/types/image"
26
        "github.com/docker/docker/client"
27
        "github.com/docker/docker/pkg/jsonmessage"
28
        "github.com/sirupsen/logrus"
29
)
30

31
const (
32
        astroRunContainer  = "astro-run"
33
        pullingImagePrompt = "Pulling image from Astronomer registry"
34
        prefix             = "Bearer "
35

36
        // Enhanced error message for 403 authentication issues
37
        imagePush403ErrMsg = `failed to push image due to authentication error (403 Forbidden).
38

39
This commonly occurs due to:
40
1. Invalid cached Docker credentials
41
2. Incompatible containerd snapshotter configuration
42

43
To resolve:
44
• Run 'docker logout' for each Astro registry to clear cached credentials
45
• Ensure containerd snapshotter is disabled (Docker Desktop users)
46
• Try running 'astro deploy' again
47

48
For detailed troubleshooting steps, visit:
49
https://support.astronomer.io/hc/en-us/articles/41427905156243-403-errors-on-image-push`
50
)
51

52
var errGetImageLabel = errors.New("error getting image label")
53

54
var getDockerClient = func() (client.APIClient, error) {
5✔
55
        return client.NewClientWithOpts(client.FromEnv)
5✔
56
}
5✔
57

58
type DockerImage struct {
59
        imageName string
60
}
61

62
func DockerImageInit(imageName string) *DockerImage {
2✔
63
        return &DockerImage{imageName: imageName}
2✔
64
}
2✔
65

66
func shouldAddPullFlag(dockerfilePath string) (bool, error) {
8✔
67
        file, err := os.Open(dockerfilePath)
8✔
68
        if err != nil {
8✔
69
                return false, fmt.Errorf("opening file: %w", err)
×
70
        }
×
71
        defer file.Close()
8✔
72

8✔
73
        // scan the Dockerfile to check if any FROM instructions reference an image other than astro runtime
8✔
74
        scanner := bufio.NewScanner(file)
8✔
75
        for scanner.Scan() {
11✔
76
                line := scanner.Text()
3✔
77
                if strings.HasPrefix(line, "FROM ") && !strings.HasPrefix(line, fmt.Sprintf("FROM %s", QuayBaseImageName)) && !strings.HasPrefix(line, fmt.Sprintf("FROM %s", AstroImageRegistryBaseImageName)) {
5✔
78
                        return false, nil
2✔
79
                }
2✔
80
        }
81
        if err := scanner.Err(); err != nil {
6✔
82
                return false, fmt.Errorf("scanning file: %w", err)
×
83
        }
×
84
        return true, nil
6✔
85
}
86

87
func (d *DockerImage) Build(dockerfilePath, buildSecretString string, buildConfig airflowTypes.ImageBuildConfig) error {
8✔
88
        // Start the spinner.
8✔
89
        s := spinner.NewSpinner("Building project image…")
8✔
90
        if !logger.IsLevelEnabled(logrus.DebugLevel) {
8✔
91
                s.Start()
×
92
                defer s.Stop()
×
93
        }
×
94

95
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
8✔
96
        if err != nil {
8✔
97
                return err
×
98
        }
×
99
        if dockerfilePath == "" {
13✔
100
                dockerfilePath = "Dockerfile"
5✔
101
        }
5✔
102
        args := []string{"build"}
8✔
103
        addPullFlag, err := shouldAddPullFlag(dockerfilePath)
8✔
104
        if err != nil {
8✔
105
                return fmt.Errorf("reading dockerfile: %w", err)
×
106
        }
×
107
        if runtimes.IsPodman(containerRuntime) {
8✔
108
                args = append(args, "--format", "docker")
×
109
        }
×
110
        if addPullFlag {
14✔
111
                args = append(args, "--pull")
6✔
112
        }
6✔
113
        err = os.Chdir(buildConfig.Path)
8✔
114
        if err != nil {
9✔
115
                return err
1✔
116
        }
1✔
117
        args = append(args, []string{
7✔
118
                "-t",
7✔
119
                d.imageName,
7✔
120
                "-f",
7✔
121
                dockerfilePath,
7✔
122
                ".",
7✔
123
        }...)
7✔
124
        if buildConfig.NoCache {
13✔
125
                args = append(args, "--no-cache")
6✔
126
        }
6✔
127

128
        for _, label := range buildConfig.Labels {
9✔
129
                args = append(args, "--label", label)
2✔
130
        }
2✔
131

132
        if len(buildConfig.TargetPlatforms) > 0 {
14✔
133
                args = append(args, fmt.Sprintf("--platform=%s", strings.Join(buildConfig.TargetPlatforms, ",")))
7✔
134
        }
7✔
135
        if buildSecretString != "" {
8✔
136
                buildSecretArgs := []string{
1✔
137
                        "--secret",
1✔
138
                        buildSecretString,
1✔
139
                }
1✔
140
                args = append(args, buildSecretArgs...)
1✔
141
        }
1✔
142

143
        // Route output streams according to verbosity.
144
        var stdout, stderr io.Writer
7✔
145
        var outBuff bytes.Buffer
7✔
146
        if logger.IsLevelEnabled(logrus.DebugLevel) {
14✔
147
                stdout = os.Stdout
7✔
148
                stderr = os.Stderr
7✔
149
        } else {
7✔
150
                stdout = &outBuff
×
151
                stderr = &outBuff
×
152
        }
×
153

154
        // Build the image
155
        err = cmdExec(containerRuntime, stdout, stderr, args...)
7✔
156
        if err != nil {
8✔
157
                s.FinalMSG = ""
1✔
158
                s.Stop()
1✔
159
                fmt.Println(strings.TrimSpace(outBuff.String()) + "\n")
1✔
160
                return errors.New("an error was encountered while building the image, see the build logs for details")
1✔
161
        }
1✔
162

163
        spinner.StopWithCheckmark(s, "Project image has been updated")
6✔
164
        return nil
6✔
165
}
166

167
func (d *DockerImage) Pytest(pytestFile, airflowHome, envFile, testHomeDirectory string, pytestArgs []string, htmlReport bool, buildConfig airflowTypes.ImageBuildConfig) (string, error) {
6✔
168
        // delete container
6✔
169
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
6✔
170
        if err != nil {
6✔
171
                return "", err
×
172
        }
×
173
        err = cmdExec(containerRuntime, nil, nil, "rm", "astro-pytest")
6✔
174
        if err != nil {
8✔
175
                logger.Debug(err)
2✔
176
        }
2✔
177
        // Change to location of Dockerfile
178
        err = os.Chdir(buildConfig.Path)
6✔
179
        if err != nil {
7✔
180
                return "", err
1✔
181
        }
1✔
182
        args := []string{
5✔
183
                "create",
5✔
184
                "-i",
5✔
185
                "--name",
5✔
186
                "astro-pytest",
5✔
187
        }
5✔
188
        fileExist, err := util.Exists(airflowHome + "/" + envFile)
5✔
189
        if err != nil {
5✔
190
                return "", err
×
191
        }
×
192
        if fileExist {
10✔
193
                args = append(args, []string{"--env-file", envFile}...)
5✔
194
        }
5✔
195
        args = append(args, []string{d.imageName, "pytest", pytestFile}...)
5✔
196
        args = append(args, pytestArgs...)
5✔
197
        // run pytest image
5✔
198
        var stdout, stderr io.Writer
5✔
199
        if logger.IsLevelEnabled(logrus.WarnLevel) {
10✔
200
                stdout = os.Stdout
5✔
201
                stderr = os.Stderr
5✔
202
        } else {
5✔
203
                stdout = nil
×
204
                stderr = nil
×
205
        }
×
206

207
        // create pytest container
208
        err = cmdExec(containerRuntime, stdout, stderr, args...)
5✔
209
        if err != nil {
7✔
210
                return "", err
2✔
211
        }
2✔
212

213
        // cp DAGs folder
214
        args = []string{
3✔
215
                "cp",
3✔
216
                airflowHome + "/dags",
3✔
217
                "astro-pytest:/usr/local/airflow/",
3✔
218
        }
3✔
219
        docErr := cmdExec(containerRuntime, stdout, stderr, args...)
3✔
220
        if docErr != nil {
4✔
221
                return "", docErr
1✔
222
        }
1✔
223

224
        // cp .astro folder
225
        // on some machine .astro is being docker ignored, but not
226
        // on every machine, hence to keep behavior consistent
227
        // copying the .astro folder explicitly
228
        args = []string{
2✔
229
                "cp",
2✔
230
                airflowHome + "/.astro",
2✔
231
                "astro-pytest:/usr/local/airflow/",
2✔
232
        }
2✔
233
        docErr = cmdExec(containerRuntime, stdout, stderr, args...)
2✔
234
        if docErr != nil {
2✔
235
                return "", docErr
×
236
        }
×
237

238
        // start pytest container
239
        err = cmdExec(containerRuntime, stdout, stderr, []string{"start", "astro-pytest", "-a"}...)
2✔
240
        if err != nil {
3✔
241
                logger.Debugf("Error starting pytest container: %s", err.Error())
1✔
242
        }
1✔
243

244
        // get exit code
245
        args = []string{
2✔
246
                "inspect",
2✔
247
                "astro-pytest",
2✔
248
                "--format='{{.State.ExitCode}}'",
2✔
249
        }
2✔
250
        var outb bytes.Buffer
2✔
251
        docErr = cmdExec(containerRuntime, &outb, stderr, args...)
2✔
252
        if docErr != nil {
2✔
253
                logger.Debug(docErr)
×
254
        }
×
255

256
        if htmlReport {
4✔
257
                // Copy the dag-test-report.html file from the container to the destination folder
2✔
258
                docErr = cmdExec(containerRuntime, nil, stderr, "cp", "astro-pytest:/usr/local/airflow/dag-test-report.html", "./"+testHomeDirectory)
2✔
259
                if docErr != nil {
2✔
260
                        logger.Debugf("Error copying dag-test-report.html file from the pytest container: %s", docErr.Error())
×
261
                }
×
262
        }
263

264
        // Persist the include folder from the Docker container to local include folder
265
        docErr = cmdExec(containerRuntime, nil, stderr, "cp", "astro-pytest:/usr/local/airflow/include/", ".")
2✔
266
        if docErr != nil {
2✔
267
                logger.Debugf("Error copying include folder from the pytest container: %s", docErr.Error())
×
268
        }
×
269

270
        // delete container
271
        docErr = cmdExec(containerRuntime, nil, stderr, "rm", "astro-pytest")
2✔
272
        if docErr != nil {
2✔
273
                logger.Debugf("Error removing the astro-pytest container: %s", docErr.Error())
×
274
        }
×
275

276
        return outb.String(), err
2✔
277
}
278

279
func (d *DockerImage) CreatePipFreeze(altImageName, pipFreezeFile string) error {
3✔
280
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
3✔
281
        if err != nil {
3✔
282
                return err
×
283
        }
×
284
        // Define the Docker command and arguments
285
        imageName := d.imageName
3✔
286
        if altImageName != "" {
3✔
287
                imageName = altImageName
×
288
        }
×
289
        dockerArgs := []string{"run", "--rm", imageName, "pip", "freeze"}
3✔
290

3✔
291
        // Create a file to store the command output
3✔
292
        file, err := os.Create(pipFreezeFile)
3✔
293
        if err != nil {
4✔
294
                return err
1✔
295
        }
1✔
296
        defer file.Close()
2✔
297

2✔
298
        // Run the Docker command
2✔
299
        err = cmdExec(containerRuntime, file, os.Stderr, dockerArgs...)
2✔
300
        if err != nil {
3✔
301
                return err
1✔
302
        }
1✔
303

304
        return nil
1✔
305
}
306

307
func (d *DockerImage) Push(remoteImage, username, token string, getImageRepoSha bool) (string, error) {
13✔
308
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
13✔
309
        if err != nil {
13✔
310
                return "", err
×
311
        }
×
312
        err = cmdExec(containerRuntime, nil, nil, "tag", d.imageName, remoteImage)
13✔
313
        if err != nil {
14✔
314
                return "", fmt.Errorf("command '%s tag %s %s' failed: %w", containerRuntime, d.imageName, remoteImage, err)
1✔
315
        }
1✔
316

317
        registry, err := d.getRegistryToAuth(remoteImage)
12✔
318
        if err != nil {
12✔
319
                return "", err
×
320
        }
×
321

322
        // Push image to registry
323
        // Note: Caller is responsible for printing appropriate message
324

325
        configFile := cliConfig.LoadDefaultConfigFile(os.Stderr)
12✔
326

12✔
327
        authConfig, err := configFile.GetAuthConfig(registry)
12✔
328
        if err != nil {
12✔
329
                logger.Debugf("Error reading credentials: %v", err)
×
330
                return "", fmt.Errorf("error reading credentials: %w", err)
×
331
        }
×
332

333
        if username == "" && token == "" {
18✔
334
                registryDomain := strings.Split(registry, "/")[0]
6✔
335
                creds := configFile.GetCredentialsStore(registryDomain)
6✔
336
                authConfig, err = creds.Get(registryDomain)
6✔
337
                if err != nil {
6✔
338
                        logger.Debugf("Error reading credentials for domain: %s from %s credentials store: %v", containerRuntime, registryDomain, err)
×
339
                }
×
340
        } else {
6✔
341
                if username != "" {
12✔
342
                        authConfig.Username = username
6✔
343
                }
6✔
344
                authConfig.Password = token
6✔
345
                authConfig.ServerAddress = registry
6✔
346
        }
347

348
        logger.Debugf("Exec Push %s creds %v \n", containerRuntime, authConfig)
12✔
349

12✔
350
        err = d.pushWithClient(&authConfig, remoteImage)
12✔
351
        if err != nil {
18✔
352
                // if it does not work with the go library use bash to run docker commands. Support for (old?) versions of Colima
6✔
353
                err = pushWithBash(&authConfig, remoteImage)
6✔
354
                if err != nil {
9✔
355
                        // Check for 403 errors only after both methods fail
3✔
356
                        if is403Error(err) {
5✔
357
                                return "", errors.New(imagePush403ErrMsg)
2✔
358
                        }
2✔
359
                        return "", err
1✔
360
                }
361
        }
362
        sha := ""
9✔
363
        if getImageRepoSha {
11✔
364
                sha, err = d.GetImageRepoSHA(registry)
2✔
365
                if err != nil {
3✔
366
                        return "", err
1✔
367
                }
1✔
368
        }
369
        // Delete the image tags we just generated
370
        err = cmdExec(containerRuntime, nil, nil, "rmi", remoteImage)
8✔
371
        if err != nil {
8✔
372
                return "", fmt.Errorf("command '%s rmi %s' failed: %w", containerRuntime, remoteImage, err)
×
373
        }
×
374
        return sha, nil
8✔
375
}
376

377
func (d *DockerImage) GetImageRepoSHA(registry string) (string, error) {
2✔
378
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
2✔
379
        if err != nil {
2✔
380
                return "", err
×
381
        }
×
382

383
        // Get all repo digests of the image
384
        out := &bytes.Buffer{}
2✔
385
        err = cmdExec(containerRuntime, out, nil, "inspect", "--format={{json .RepoDigests}}", d.imageName)
2✔
386
        if err != nil {
3✔
387
                return "", fmt.Errorf("failed to get digests for image %s: %w", d.imageName, err)
1✔
388
        }
1✔
389

390
        // Parse and clean the output
391
        var repoDigests []string
1✔
392
        digestOutput := strings.TrimSpace(out.String())
1✔
393
        err = json.Unmarshal([]byte(digestOutput), &repoDigests)
1✔
394
        if err != nil {
1✔
395
                return "", fmt.Errorf("failed to parse digests for image %s: %w", d.imageName, err)
×
396
        }
×
397
        // Filter repo digests based on the provided repo name
398
        for _, digest := range repoDigests {
2✔
399
                parts := strings.Split(digest, "@")
1✔
400
                if len(parts) == 2 && strings.HasPrefix(parts[0], registry) {
2✔
401
                        return parts[1], nil // Return the digest for the matching repo name
1✔
402
                }
1✔
403
        }
404

405
        // If no matching digest is found
406
        return "", fmt.Errorf("no matching digest found for registry %s in image %s", registry, d.imageName)
×
407
}
408

409
func (d *DockerImage) pushWithClient(authConfig *cliTypes.AuthConfig, remoteImage string) error {
12✔
410
        ctx := context.Background()
12✔
411

12✔
412
        cli, err := getDockerClient()
12✔
413
        if err != nil {
13✔
414
                logger.Debugf("Error setting up new Client ops %v", err)
1✔
415
                return err
1✔
416
        }
1✔
417
        cli.NegotiateAPIVersion(ctx)
11✔
418
        buf, err := json.Marshal(authConfig)
11✔
419
        if err != nil {
11✔
420
                logger.Debugf("Error negotiating api version: %v", err)
×
421
                return err
×
422
        }
×
423
        encodedAuth := base64.URLEncoding.EncodeToString(buf)
11✔
424
        responseBody, err := cli.ImagePush(ctx, remoteImage, image.PushOptions{RegistryAuth: encodedAuth})
11✔
425
        if err != nil {
11✔
426
                logger.Debugf("Error pushing image to docker: %v", err)
×
427
                // if NewClientWithOpt does not work use bash to run docker commands
×
428
                return err
×
429
        }
×
430
        defer responseBody.Close()
11✔
431
        return displayJSONMessagesToStream(responseBody, nil)
11✔
432
}
433

434
// Get the registry name to authenticate against
435
func (d *DockerImage) getRegistryToAuth(imageName string) (string, error) {
16✔
436
        domain, err := config.GetCurrentDomain()
16✔
437
        if err != nil {
16✔
438
                return "", err
×
439
        }
×
440

441
        if domain == "localhost" {
25✔
442
                return config.CFG.LocalRegistry.GetString(), nil
9✔
443
        }
9✔
444
        parts := strings.SplitN(imageName, "/", 2)
7✔
445
        if len(parts) != 2 || !strings.Contains(parts[1], "/") {
7✔
UNCOV
446
                // This _should_ be impossible for users to hit
×
UNCOV
447
                return "", fmt.Errorf("internal logic error: unsure how to get registry from image name %q", imageName)
×
UNCOV
448
        }
×
449
        return parts[0], nil
7✔
450
}
451

452
func (d *DockerImage) Pull(remoteImage, username, token string) error {
7✔
453
        if remoteImage == "" {
11✔
454
                remoteImage = d.imageName
4✔
455
        }
4✔
456

457
        // Pulling image to registry
458
        fmt.Println(pullingImagePrompt)
7✔
459
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
7✔
460
        if err != nil {
7✔
UNCOV
461
                return err
×
UNCOV
462
        }
×
463
        if username != "" { // Case for cloud image push where we have both registry user & pass, for software login happens during `astro login` itself
11✔
464
                var registry string
4✔
465
                if registry, err = d.getRegistryToAuth(remoteImage); err != nil {
4✔
UNCOV
466
                        return err
×
UNCOV
467
                }
×
468
                pass := token
4✔
469
                pass = strings.TrimPrefix(pass, prefix)
4✔
470
                cmd := "echo \"" + pass + "\"" + " | " + containerRuntime + " login " + registry + " -u " + username + " --password-stdin"
4✔
471
                err = cmdExec("bash", os.Stdout, os.Stderr, "-c", cmd) // This command will only work on machines that have bash. If users have issues we will revist
4✔
472
        }
473
        if err != nil {
8✔
474
                return err
1✔
475
        }
1✔
476
        // docker pull <image>
477
        err = cmdExec(containerRuntime, os.Stdout, os.Stderr, "pull", remoteImage)
6✔
478
        if err != nil {
7✔
479
                return err
1✔
480
        }
1✔
481

482
        return nil
5✔
483
}
484

UNCOV
485
var displayJSONMessagesToStream = func(responseBody io.ReadCloser, auxCallback func(jsonmessage.JSONMessage)) error {
×
UNCOV
486
        out := streams.NewOut(os.Stdout)
×
UNCOV
487
        err := jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
×
UNCOV
488
        if err != nil {
×
UNCOV
489
                return err
×
UNCOV
490
        }
×
UNCOV
491
        return nil
×
492
}
493

494
func (d *DockerImage) GetLabel(altImageName, labelName string) (string, error) {
3✔
495
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
3✔
496
        if err != nil {
3✔
497
                return "", err
×
498
        }
×
499
        stdout := new(bytes.Buffer)
3✔
500
        stderr := new(bytes.Buffer)
3✔
501

3✔
502
        labelFmt := fmt.Sprintf("{{ index .Config.Labels %q }}", labelName)
3✔
503
        var label string
3✔
504
        imageName := d.imageName
3✔
505
        if altImageName != "" {
3✔
UNCOV
506
                imageName = altImageName
×
UNCOV
507
        }
×
508
        err = cmdExec(containerRuntime, stdout, stderr, "inspect", "--format", labelFmt, imageName)
3✔
509
        if err != nil {
4✔
510
                return label, err
1✔
511
        }
1✔
512
        if execErr := stderr.String(); execErr != "" {
3✔
513
                return label, fmt.Errorf("%s: %w", execErr, errGetImageLabel)
1✔
514
        }
1✔
515
        label = stdout.String()
1✔
516
        label = strings.Trim(label, "\n")
1✔
517
        return label, nil
1✔
518
}
519

520
func (d *DockerImage) DoesImageExist(imageName string) error {
2✔
521
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
2✔
522
        if err != nil {
2✔
UNCOV
523
                return err
×
UNCOV
524
        }
×
525
        stdout := new(bytes.Buffer)
2✔
526
        stderr := new(bytes.Buffer)
2✔
527

2✔
528
        err = cmdExec(containerRuntime, stdout, stderr, "manifest", "inspect", imageName)
2✔
529
        if err != nil {
3✔
530
                return err
1✔
531
        }
1✔
532
        return nil
1✔
533
}
534

535
func (d *DockerImage) ListLabels() (map[string]string, error) {
3✔
536
        var labels map[string]string
3✔
537

3✔
538
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
3✔
539
        if err != nil {
3✔
UNCOV
540
                return labels, err
×
UNCOV
541
        }
×
542

543
        stdout := new(bytes.Buffer)
3✔
544
        stderr := new(bytes.Buffer)
3✔
545

3✔
546
        err = cmdExec(containerRuntime, stdout, stderr, "inspect", "--format", "{{ json .Config.Labels }}", d.imageName)
3✔
547
        if err != nil {
4✔
548
                return labels, err
1✔
549
        }
1✔
550
        if execErr := stderr.String(); execErr != "" {
3✔
551
                return labels, fmt.Errorf("%s: %w", execErr, errGetImageLabel)
1✔
552
        }
1✔
553
        err = json.Unmarshal(stdout.Bytes(), &labels)
1✔
554
        if err != nil {
1✔
UNCOV
555
                return labels, err
×
UNCOV
556
        }
×
557
        return labels, nil
1✔
558
}
559

560
func (d *DockerImage) TagLocalImage(localImage string) error {
2✔
561
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
2✔
562
        if err != nil {
2✔
563
                return err
×
UNCOV
564
        }
×
565

566
        err = cmdExec(containerRuntime, nil, nil, "tag", localImage, d.imageName)
2✔
567
        if err != nil {
3✔
568
                return fmt.Errorf("command '%s tag %s %s' failed: %w", containerRuntime, localImage, d.imageName, err)
1✔
569
        }
1✔
570
        return nil
1✔
571
}
572

573
func (d *DockerImage) RunDAG(dagID, envFile, settingsFile, containerName, dagFile, executionDate string, taskLogs bool) error {
3✔
574
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
3✔
575
        if err != nil {
3✔
UNCOV
576
                return err
×
UNCOV
577
        }
×
578

579
        stdout := os.Stdout
3✔
580
        stderr := os.Stderr
3✔
581
        // delete container
3✔
582
        err = cmdExec(containerRuntime, nil, nil, "rm", astroRunContainer)
3✔
583
        if err != nil {
4✔
584
                logger.Debug(err)
1✔
585
        }
1✔
586
        var args []string
3✔
587
        if containerName != "" {
4✔
588
                args = []string{
1✔
589
                        "exec",
1✔
590
                        "-t",
1✔
591
                        containerName,
1✔
592
                }
1✔
593
        }
1✔
594
        // check if settings file exists
595
        settingsFileExist, err := util.Exists("./" + settingsFile)
3✔
596
        if err != nil {
3✔
UNCOV
597
                logger.Debug(err)
×
UNCOV
598
        }
×
599
        // docker exec
600
        if containerName == "" {
5✔
601
                args = []string{
2✔
602
                        "run",
2✔
603
                        "-t",
2✔
604
                        "--name",
2✔
605
                        astroRunContainer,
2✔
606
                        "-v",
2✔
607
                        config.WorkingPath + "/dags:/usr/local/airflow/dags:rw",
2✔
608
                        "-v",
2✔
609
                        config.WorkingPath + "/plugins:/usr/local/airflow/plugins:rw",
2✔
610
                        "-v",
2✔
611
                        config.WorkingPath + "/include:/usr/local/airflow/include:rw",
2✔
612
                }
2✔
613
                // if settings file exists append it to args
2✔
614
                if settingsFileExist {
4✔
615
                        args = append(args, []string{"-v", config.WorkingPath + "/" + settingsFile + ":/usr/local/airflow/" + settingsFile}...)
2✔
616
                }
2✔
617
                // if env file exists append it to args
618
                fileExist, err := util.Exists(config.WorkingPath + "/" + envFile)
2✔
619
                if err != nil {
2✔
UNCOV
620
                        logger.Debug(err)
×
UNCOV
621
                }
×
622
                if fileExist {
4✔
623
                        args = append(args, []string{"--env-file", envFile}...)
2✔
624
                }
2✔
625
                args = append(args, []string{d.imageName}...)
2✔
626
        }
627
        if !strings.Contains(dagFile, "dags/") {
6✔
628
                dagFile = "./dags/" + dagFile
3✔
629
        }
3✔
630
        cmdArgs := []string{
3✔
631
                "run_dag",
3✔
632
                dagFile,
3✔
633
                dagID,
3✔
634
        }
3✔
635
        // settings file exists append it to args
3✔
636
        if settingsFileExist {
6✔
637
                cmdArgs = append(cmdArgs, []string{"./" + settingsFile}...)
3✔
638
        }
3✔
639

640
        if executionDate != "" {
3✔
UNCOV
641
                cmdArgs = append(cmdArgs, []string{"--execution-date", executionDate}...)
×
UNCOV
642
        }
×
643

644
        if taskLogs {
6✔
645
                cmdArgs = append(cmdArgs, []string{"--verbose"}...)
3✔
646
        }
3✔
647

648
        args = append(args, cmdArgs...)
3✔
649

3✔
650
        fmt.Println("\nStarting a DAG run for " + dagID + "...")
3✔
651
        fmt.Println("\nLoading DAGs...")
3✔
652
        logger.Debug("args passed to docker command:")
3✔
653
        logger.Debug(args)
3✔
654

3✔
655
        cmdErr := cmdExec(containerRuntime, stdout, stderr, args...)
3✔
656
        // add back later fmt.Println("\nSee the output of this command for errors. To view task logs, use the '--task-logs' flag.")
3✔
657
        if cmdErr != nil {
4✔
658
                logger.Debug(cmdErr)
1✔
659
                fmt.Println("\nSee the output of this command for errors.")
1✔
660
                fmt.Println("If you are having an issue with loading your settings file make sure both the 'variables' and 'connections' fields exist and that there are no yaml syntax errors.")
1✔
661
                fmt.Println("If you are getting a missing `airflow_settings.yaml` or `astro-run-dag` error try restarting airflow with `astro dev restart`.")
1✔
662
        }
1✔
663
        if containerName == "" {
5✔
664
                // delete container
2✔
665
                err = cmdExec(containerRuntime, nil, nil, "rm", astroRunContainer)
2✔
666
                if err != nil {
3✔
667
                        logger.Debug(err)
1✔
668
                }
1✔
669
        }
670
        return cmdErr
3✔
671
}
672

UNCOV
673
func (d *DockerImage) RunCommand(args []string, mountDirs map[string]string, stdout, stderr io.Writer) error {
×
UNCOV
674
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
×
UNCOV
675
        if err != nil {
×
UNCOV
676
                return err
×
UNCOV
677
        }
×
678

679
        // Default docker run arguments
680
        dockerArgs := []string{"run", "--rm"}
×
681

×
682
        // Add volume mounts from the map
×
683
        for hostDir, containerDir := range mountDirs {
×
684
                dockerArgs = append(dockerArgs, "-v", hostDir+":"+containerDir)
×
UNCOV
685
        }
×
686

687
        // Add the image name and the remaining arguments
688
        dockerArgs = append(dockerArgs, d.imageName)
×
689
        args = append(dockerArgs, args...)
×
690

×
691
        logger.Debugf("Running command in container: '%s'", strings.Join(args, " "))
×
692

×
UNCOV
693
        return cmdExec(containerRuntime, stdout, stderr, args...)
×
694
}
695

696
// Exec executes a docker command
697
var cmdExec = func(cmd string, stdout, stderr io.Writer, args ...string) error {
2✔
698
        _, lookErr := exec.LookPath(cmd)
2✔
699
        if lookErr != nil {
3✔
700
                return fmt.Errorf("failed to find the %s command: %w", cmd, lookErr)
1✔
701
        }
1✔
702

703
        execCMD := exec.Command(cmd, args...)
1✔
704
        execCMD.Stdin = os.Stdin
1✔
705
        execCMD.Stdout = stdout
1✔
706
        execCMD.Stderr = stderr
1✔
707

1✔
708
        if cmdErr := execCMD.Run(); cmdErr != nil {
1✔
UNCOV
709
                return fmt.Errorf("failed to execute cmd: %w", cmdErr)
×
UNCOV
710
        }
×
711

712
        return nil
1✔
713
}
714

715
// When login and push do not work use bash to run docker commands, this function is for users using colima
716
func pushWithBash(authConfig *cliTypes.AuthConfig, imageName string) error {
9✔
717
        containerRuntime, err := runtimes.GetContainerRuntimeBinary()
9✔
718
        if err != nil {
9✔
UNCOV
719
                return err
×
UNCOV
720
        }
×
721

722
        if authConfig.Username != "" { // Case for cloud image push where we have both registry user & pass, for software login happens during `astro login` itself
13✔
723
                pass := authConfig.Password
4✔
724
                pass = strings.TrimPrefix(pass, prefix)
4✔
725
                cmd := "echo \"" + pass + "\"" + " | " + containerRuntime + " login " + authConfig.ServerAddress + " -u " + authConfig.Username + " --password-stdin"
4✔
726
                err = cmdExec("bash", os.Stdout, os.Stderr, "-c", cmd) // This command will only work on machines that have bash. If users have issues we will revist
4✔
727
                if err != nil {
5✔
728
                        return err
1✔
729
                }
1✔
730
        }
731

732
        // docker push <imageName> - capture stderr to preserve error details
733
        var stderr bytes.Buffer
8✔
734
        err = cmdExec(containerRuntime, os.Stdout, &stderr, "push", imageName)
8✔
735
        if err != nil {
12✔
736
                // Include stderr output in the error so we can detect 403 errors
4✔
737
                stderrOutput := stderr.String()
4✔
738
                if stderrOutput != "" {
4✔
UNCOV
739
                        return fmt.Errorf("%w: %s", err, stderrOutput)
×
UNCOV
740
                }
×
741
                return err
4✔
742
        }
743
        return nil
4✔
744
}
745

746
// is403Error checks if the error is a 403 authentication error
747
func is403Error(err error) bool {
13✔
748
        if err == nil {
14✔
749
                return false
1✔
750
        }
1✔
751

752
        // Check the entire error chain, not just the top-level error
753
        for currentErr := err; currentErr != nil; currentErr = errors.Unwrap(currentErr) {
24✔
754
                errStr := strings.ToLower(currentErr.Error())
12✔
755

12✔
756
                if strings.Contains(errStr, "403") ||
12✔
757
                        strings.Contains(errStr, "forbidden") ||
12✔
758
                        strings.Contains(errStr, "authentication required") {
19✔
759
                        return true
7✔
760
                }
7✔
761
        }
762

763
        return false
5✔
764
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc