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

astronomer / astro-cli / 609b6753-92a4-41d8-923c-0f23002261ab

10 Apr 2026 04:52PM UTC coverage: 39.387% (+0.04%) from 39.344%
609b6753-92a4-41d8-923c-0f23002261ab

Pull #2077

circleci

ashb
fixup! Store auth credentials in the OS keychain (macOS + Linux)
Pull Request #2077: Store auth credentials in the OS keychain (macOS + Linux)

366 of 500 new or added lines in 43 files covered. (73.2%)

43 existing lines in 7 files now uncovered.

24949 of 63343 relevant lines covered (39.39%)

9.5 hits per line

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

88.8
/software/deploy/deploy.go
1
package deploy
2

3
import (
4
        "errors"
5
        "fmt"
6
        neturl "net/url"
7
        "os"
8
        "path/filepath"
9
        "strconv"
10
        "strings"
11
        "time"
12

13
        "github.com/docker/docker/api/types/versions"
14

15
        "github.com/astronomer/astro-cli/airflow"
16
        "github.com/astronomer/astro-cli/airflow/types"
17
        "github.com/astronomer/astro-cli/config"
18
        "github.com/astronomer/astro-cli/context"
19
        "github.com/astronomer/astro-cli/docker"
20
        "github.com/astronomer/astro-cli/houston"
21
        "github.com/astronomer/astro-cli/pkg/fileutil"
22
        "github.com/astronomer/astro-cli/pkg/input"
23
        "github.com/astronomer/astro-cli/pkg/keychain"
24
        "github.com/astronomer/astro-cli/pkg/logger"
25
        "github.com/astronomer/astro-cli/pkg/printutil"
26
        "github.com/astronomer/astro-cli/software/auth"
27
)
28

29
var (
30
        // this is used to monkey patch the function in order to write unit test cases
31
        imageHandlerInit = airflow.ImageHandlerInit
32

33
        dockerfile = "Dockerfile"
34

35
        deployImagePlatformSupport = []string{"linux/amd64"}
36

37
        gzipFile = fileutil.GzipFile
38

39
        getDeploymentIDForCurrentCommandVar = getDeploymentIDForCurrentCommand
40
)
41

42
var (
43
        ErrNoWorkspaceID                         = errors.New("no workspace id provided")
44
        errNoDomainSet                           = errors.New("no domain set, re-authenticate")
45
        errInvalidDeploymentID                   = errors.New("please specify a valid deployment ID")
46
        errDeploymentNotFound                    = errors.New("no airflow deployments found")
47
        errInvalidDeploymentSelected             = errors.New("invalid deployment selection\n") //nolint
48
        ErrDagOnlyDeployDisabledInConfig         = errors.New("to perform this operation, set both deployments.dagOnlyDeployment and deployments.configureDagDeployment to true in your Astronomer cluster")
49
        ErrDagOnlyDeployNotEnabledForDeployment  = errors.New("to perform this operation, first set the Deployment type to 'dag_deploy' via the UI or the API or the CLI")
50
        ErrEmptyDagFolderUserCancelledOperation  = errors.New("no DAGs found in the dags folder. User canceled the operation")
51
        ErrBYORegistryDomainNotSet               = errors.New("Custom registry host is not set in config. It can be set at astronomer.houston.config.deployments.registry.protectedCustomRegistry.updateRegistry.host") //nolint
52
        ErrDeploymentTypeIncorrectForImageOnly   = errors.New("--image only works for Dag-only, Git-sync-based and NFS-based deployments")
53
        WarningInvalidImageNameMsg               = "WARNING! The image in your Dockerfile '%s' is not based on Astro Runtime and is not supported. Change your Dockerfile with an image that pulls from 'quay.io/astronomer/astro-runtime' to proceed.\n"
54
        ErrNoRuntimeLabelOnCustomImage           = errors.New("the image should have label io.astronomer.docker.runtime.version")
55
        ErrRuntimeVersionNotPassedForRemoteImage = errors.New("if --image-name and --remote is passed, it's mandatory to pass --runtime-version")
56
)
57

58
const (
59
        houstonDeploymentHeader       = "Authenticated to %s \n\n"
60
        houstonSelectDeploymentPrompt = "Select which airflow deployment you want to deploy to:"
61
        houstonDeploymentPrompt       = "Deploying: %s\n"
62

63
        imageBuildingPrompt = "Building image..."
64

65
        warningInvalidImageName                   = "WARNING! The image in your Dockerfile is pulling from '%s', which is not supported. We strongly recommend that you use Astronomer Certified or Runtime images that pull from 'astronomerinc/ap-airflow', 'quay.io/astronomer/ap-airflow' or 'quay.io/astronomer/astro-runtime'. If you're running a custom image, you can override this. Are you sure you want to continue?\n"
66
        warningInvalidNameTag                     = "WARNING! You are about to push an image using the '%s' tag. This is not recommended.\nPlease use one of the following tags: %s.\nAre you sure you want to continue?"
67
        warningInvalidNameTagEmptyRecommendations = "WARNING! You are about to push an image using the '%s' tag. This is not recommended.\nAre you sure you want to continue?"
68

69
        registryDomainPrefix              = "registry."
70
        runtimeImageLabel                 = "io.astronomer.docker.runtime.version"
71
        airflowImageLabel                 = "io.astronomer.docker.airflow.version"
72
        composeSkipImageBuildingPromptMsg = "Skipping building image since --image-name flag is used..."
73
)
74

75
var tab = printutil.Table{
76
        Padding:        []int{5, 30, 30, 50},
77
        DynamicPadding: true,
78
        Header:         []string{"#", "LABEL", "DEPLOYMENT NAME", "WORKSPACE", "DEPLOYMENT ID"},
79
}
80

81
func Airflow(houstonClient houston.ClientInterface, store keychain.SecureStore, path, deploymentID, wsID string, ignoreCacheDeploy, prompt bool, description string, isImageOnlyDeploy bool, imageName string) (string, error) {
16✔
82
        deploymentID, deployments, err := getDeploymentIDForCurrentCommand(houstonClient, wsID, deploymentID, prompt)
16✔
83
        if err != nil {
23✔
84
                return deploymentID, err
7✔
85
        }
7✔
86

87
        c, _ := config.GetCurrentContext()
9✔
88
        cloudDomain := c.Domain
9✔
89
        nextTag := ""
9✔
90
        releaseName := ""
9✔
91
        for i := range deployments {
18✔
92
                deployment := deployments[i]
9✔
93
                if deployment.ID == deploymentID {
18✔
94
                        nextTag = deployment.DeploymentInfo.NextCli
9✔
95
                        releaseName = deployment.ReleaseName
9✔
96
                }
9✔
97
        }
98

99
        deploymentInfo, err := houston.Call(houstonClient.GetDeployment)(deploymentID)
9✔
100
        if err != nil {
10✔
101
                return deploymentID, fmt.Errorf("failed to get deployment info: %w", err)
1✔
102
        }
1✔
103

104
        appConfig, err := houston.Call(houstonClient.GetAppConfig)(deploymentInfo.ClusterID)
8✔
105
        if err != nil {
8✔
106
                return deploymentID, fmt.Errorf("failed to get app config: %w", err)
×
107
        }
×
108

109
        byoRegistryDomain := ""
8✔
110
        byoRegistryEnabled := appConfig != nil && appConfig.Flags.BYORegistryEnabled
8✔
111
        if byoRegistryEnabled {
10✔
112
                // updating nextTag logic for private registry, since houston won't maintain next tag in case of BYO registry
2✔
113
                nextTag = "deploy-" + time.Now().UTC().Format("2006-01-02T15-04")
2✔
114
                byoRegistryDomain = appConfig.BYORegistryDomain
2✔
115
                if byoRegistryDomain == "" {
3✔
116
                        return deploymentID, ErrBYORegistryDomainNotSet
1✔
117
                }
1✔
118
        }
119

120
        // isImageOnlyDeploy is not valid for image-based deployments since image-based deployments inherently mean that the image itself contains dags.
121
        // If we deploy only the image, the deployment will not have any dags for image-based deployments.
122
        // Even on astro, image-based deployments are not allowed to be deployed with --image flag.
123
        if isImageOnlyDeploy && deploymentInfo.DagDeployment.Type == houston.ImageDeploymentType {
8✔
124
                return "", ErrDeploymentTypeIncorrectForImageOnly
1✔
125
        }
1✔
126
        // We don't need to exclude the dags from the image because the dags present in the image are not respected anyways for non-image based deployments
127

128
        fmt.Printf(houstonDeploymentPrompt, releaseName)
6✔
129

6✔
130
        // Build the image to deploy
6✔
131
        err = buildPushDockerImage(houstonClient, store, deploymentInfo, releaseName, path, nextTag, cloudDomain, byoRegistryDomain, ignoreCacheDeploy, byoRegistryEnabled, description, imageName)
6✔
132
        if err != nil {
8✔
133
                return deploymentID, err
2✔
134
        }
2✔
135

136
        deploymentLink := getAirflowUILink(deploymentID, deploymentInfo.Urls)
4✔
137
        fmt.Printf("Successfully pushed Docker image to Astronomer registry, it can take a few minutes to update the deployment with the new image. Navigate to the Astronomer UI to confirm the state of your deployment (%s).\n", deploymentLink)
4✔
138

4✔
139
        return deploymentID, nil
4✔
140
}
141

142
// Find deployment ID in deployments slice
143
func deploymentExists(deploymentID string, deployments []houston.Deployment) bool {
12✔
144
        for idx := range deployments {
24✔
145
                deployment := deployments[idx]
12✔
146
                if deployment.ID == deploymentID {
22✔
147
                        return true
10✔
148
                }
10✔
149
        }
150
        return false
2✔
151
}
152

153
func validateRuntimeVersion(houstonClient houston.ClientInterface, tag string, deploymentInfo *houston.Deployment) error {
12✔
154
        // Get valid image tags for platform using Deployment Info request
12✔
155
        deploymentConfig, err := houston.Call(houstonClient.GetDeploymentConfig)(nil)
12✔
156
        if err != nil {
13✔
157
                return err
1✔
158
        }
1✔
159
        vars := make(map[string]interface{})
11✔
160
        vars["clusterId"] = deploymentInfo.ClusterID
11✔
161
        // ignoring the error as user can be connected to platform where runtime is not enabled
11✔
162
        runtimeReleases, _ := houston.Call(houstonClient.GetRuntimeReleases)(vars)
11✔
163
        var validTags string
11✔
164
        if config.CFG.ShowWarnings.GetBool() && deploymentInfo.DesiredAirflowVersion != "" && !deploymentConfig.IsValidTag(tag) {
13✔
165
                validTags = strings.Join(deploymentConfig.GetValidTags(tag), ", ")
2✔
166
        }
2✔
167
        if config.CFG.ShowWarnings.GetBool() && deploymentInfo.DesiredRuntimeVersion != "" && !runtimeReleases.IsValidVersion(tag) {
11✔
168
                validTags = strings.Join(runtimeReleases.GreaterVersions(tag), ", ")
×
169
        }
×
170
        if validTags != "" {
11✔
171
                validTags := strings.Join(deploymentConfig.GetValidTags(tag), ", ")
×
172

×
173
                msg := fmt.Sprintf(warningInvalidNameTag, tag, validTags)
×
174
                if validTags == "" {
×
175
                        msg = fmt.Sprintf(warningInvalidNameTagEmptyRecommendations, tag)
×
176
                }
×
177

178
                i, _ := input.Confirm(msg)
×
179
                if !i {
×
180
                        fmt.Println("Canceling deploy...")
×
181
                        os.Exit(1)
×
182
                }
×
183
        }
184
        return nil
11✔
185
}
186

187
func UpdateDeploymentImage(houstonClient houston.ClientInterface, deploymentID, wsID, runtimeVersion, imageName string) (string, error) {
5✔
188
        if runtimeVersion == "" {
6✔
189
                return "", ErrRuntimeVersionNotPassedForRemoteImage
1✔
190
        }
1✔
191
        deploymentID, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
4✔
192
        if err != nil {
5✔
193
                return "", err
1✔
194
        }
1✔
195
        if deploymentID == "" {
3✔
196
                return "", errInvalidDeploymentID
×
197
        }
×
198
        deploymentInfo, err := houston.Call(houstonClient.GetDeployment)(deploymentID)
3✔
199
        if err != nil {
4✔
200
                return "", fmt.Errorf("failed to get deployment info: %w", err)
1✔
201
        }
1✔
202
        fmt.Println("Skipping building the image since --image-name flag is used...")
2✔
203
        req := houston.UpdateDeploymentImageRequest{ReleaseName: deploymentInfo.ReleaseName, Image: imageName, AirflowVersion: "", RuntimeVersion: runtimeVersion}
2✔
204
        _, err = houston.Call(houstonClient.UpdateDeploymentImage)(req)
2✔
205
        fmt.Println("Image successfully updated")
2✔
206
        return deploymentID, err
2✔
207
}
208

209
func pushDockerImage(byoRegistryEnabled bool, deploymentInfo *houston.Deployment, byoRegistryDomain, name, nextTag, cloudDomain string, imageHandler airflow.ImageHandler, houstonClient houston.ClientInterface, store keychain.SecureStore, customImageName string) error {
10✔
210
        var registry, remoteImage, token string
10✔
211
        if byoRegistryEnabled {
14✔
212
                registry = byoRegistryDomain
4✔
213
                remoteImage = fmt.Sprintf("%s:%s", registry, fmt.Sprintf("%s-%s", name, nextTag))
4✔
214
        } else {
10✔
215
                if ctx, err := context.GetCurrentContext(); err == nil {
12✔
216
                        if creds, err := store.GetCredentials(ctx.Domain); err == nil {
6✔
NEW
217
                                token = creds.Token
×
NEW
218
                        }
×
219
                }
220
                platformVersion, _ := houstonClient.GetPlatformVersion(nil)
6✔
221
                if versions.GreaterThanOrEqualTo(platformVersion, "1.0.0") {
12✔
222
                        var err error
6✔
223
                        registry, err = getDeploymentRegistryURL(deploymentInfo.Urls)
6✔
224
                        if err != nil {
6✔
225
                                return err
×
226
                        }
×
227
                        // Switch to per deployment registry login
228
                        err = auth.RegistryAuth(houstonClient, os.Stdout, registry, token)
6✔
229
                        if err != nil {
6✔
230
                                logger.Debugf("There was an error logging into registry: %s", err.Error())
×
231
                                return err
×
232
                        }
×
233
                        remoteImage = fmt.Sprintf("%s/%s", registry, airflow.ImageName(name, nextTag))
6✔
234
                } else {
×
235
                        registry = registryDomainPrefix + cloudDomain
×
236
                        remoteImage = fmt.Sprintf("%s/%s", registry, airflow.ImageName(name, nextTag))
×
237
                }
×
238
        }
239
        if customImageName != "" {
12✔
240
                if tagFromImageName := getGetTagFromImageName(customImageName); tagFromImageName != "" {
3✔
241
                        remoteImage = fmt.Sprintf("%s:%s", registry, tagFromImageName)
1✔
242
                }
1✔
243
        }
244
        useShaAsTag := config.CFG.ShaAsTag.GetBool()
10✔
245
        fmt.Println("Pushing image to configured registry")
10✔
246
        sha, err := imageHandler.Push(remoteImage, "", token, useShaAsTag)
10✔
247
        if err != nil {
11✔
248
                return err
1✔
249
        }
1✔
250
        if byoRegistryEnabled {
13✔
251
                if useShaAsTag {
5✔
252
                        remoteImage = fmt.Sprintf("%s@%s", registry, sha)
1✔
253
                }
1✔
254
                runtimeVersion, _ := imageHandler.GetLabel("", runtimeImageLabel)
4✔
255
                airflowVersion, _ := imageHandler.GetLabel("", airflowImageLabel)
4✔
256
                req := houston.UpdateDeploymentImageRequest{ReleaseName: name, Image: remoteImage, AirflowVersion: airflowVersion, RuntimeVersion: runtimeVersion}
4✔
257
                _, err = houston.Call(houstonClient.UpdateDeploymentImage)(req)
4✔
258
                return err
4✔
259
        }
260
        return nil
5✔
261
}
262

263
func buildDockerImageForCustomImage(imageHandler airflow.ImageHandler, customImageName string, deploymentInfo *houston.Deployment, houstonClient houston.ClientInterface) error {
3✔
264
        fmt.Println(composeSkipImageBuildingPromptMsg)
3✔
265
        err := imageHandler.TagLocalImage(customImageName)
3✔
266
        if err != nil {
3✔
267
                return err
×
268
        }
×
269
        runtimeLabel, err := imageHandler.GetLabel("", airflow.RuntimeImageLabel)
3✔
270
        if err != nil {
3✔
271
                fmt.Println("unable get runtime version from image")
×
272
                return err
×
273
        }
×
274
        if runtimeLabel == "" {
4✔
275
                return ErrNoRuntimeLabelOnCustomImage
1✔
276
        }
1✔
277
        err = validateRuntimeVersion(houstonClient, runtimeLabel, deploymentInfo)
2✔
278
        return err
2✔
279
}
280

281
func buildDockerImageFromWorkingDir(path string, imageHandler airflow.ImageHandler, houstonClient houston.ClientInterface, deploymentInfo *houston.Deployment, ignoreCacheDeploy bool, description string) error {
12✔
282
        // all these checks inside Dockerfile should happen only when no image-name is provided
12✔
283
        // parse dockerfile
12✔
284
        cmds, err := docker.ParseFile(filepath.Join(path, dockerfile))
12✔
285
        if err != nil {
14✔
286
                return fmt.Errorf("failed to parse dockerfile: %s: %w", filepath.Join(path, dockerfile), err)
2✔
287
        }
2✔
288

289
        _, tag := docker.GetImageTagFromParsedFile(cmds)
10✔
290

10✔
291
        // Get valid image tags for platform using Deployment Info request
10✔
292
        err = validateRuntimeVersion(houstonClient, tag, deploymentInfo)
10✔
293
        if err != nil {
11✔
294
                return err
1✔
295
        }
1✔
296
        // Build our image
297
        fmt.Println(imageBuildingPrompt)
9✔
298
        deployLabels := []string{"io.astronomer.skip.revision=true"}
9✔
299
        if description != "" {
18✔
300
                deployLabels = append(deployLabels, "io.astronomer.deploy.revision.description="+description)
9✔
301
        }
9✔
302
        buildConfig := types.ImageBuildConfig{
9✔
303
                Path:            config.WorkingPath,
9✔
304
                NoCache:         ignoreCacheDeploy,
9✔
305
                TargetPlatforms: deployImagePlatformSupport,
9✔
306
                Labels:          deployLabels,
9✔
307
        }
9✔
308

9✔
309
        err = imageHandler.Build("", "", buildConfig)
9✔
310
        return err
9✔
311
}
312

313
func buildDockerImage(ignoreCacheDeploy bool, deploymentInfo *houston.Deployment, customImageName, path string, imageHandler airflow.ImageHandler, houstonClient houston.ClientInterface, description string) error {
15✔
314
        if customImageName == "" {
27✔
315
                return buildDockerImageFromWorkingDir(path, imageHandler, houstonClient, deploymentInfo, ignoreCacheDeploy, description)
12✔
316
        }
12✔
317
        return buildDockerImageForCustomImage(imageHandler, customImageName, deploymentInfo, houstonClient)
3✔
318
}
319

320
func getGetTagFromImageName(imageName string) string {
2✔
321
        parts := strings.Split(imageName, ":")
2✔
322
        if len(parts) == 2 {
3✔
323
                return parts[1]
1✔
324
        }
1✔
325
        return ""
1✔
326
}
327

328
func buildPushDockerImage(houstonClient houston.ClientInterface, store keychain.SecureStore, deploymentInfo *houston.Deployment, name, path, nextTag, cloudDomain, byoRegistryDomain string, ignoreCacheDeploy, byoRegistryEnabled bool, description, customImageName string) error {
15✔
329
        imageName := airflow.ImageName(name, "latest")
15✔
330
        imageHandler := imageHandlerInit(imageName)
15✔
331
        err := buildDockerImage(ignoreCacheDeploy, deploymentInfo, customImageName, path, imageHandler, houstonClient, description)
15✔
332
        if err != nil {
20✔
333
                return err
5✔
334
        }
5✔
335
        return pushDockerImage(byoRegistryEnabled, deploymentInfo, byoRegistryDomain, name, nextTag, cloudDomain, imageHandler, houstonClient, store, customImageName)
10✔
336
}
337

338
func getAirflowUILink(deploymentID string, deploymentURLs []houston.DeploymentURL) string {
7✔
339
        if deploymentID == "" {
8✔
340
                return ""
1✔
341
        }
1✔
342

343
        for _, url := range deploymentURLs {
11✔
344
                if url.Type == houston.AirflowURLType {
10✔
345
                        return url.URL
5✔
346
                }
5✔
347
        }
348
        return ""
1✔
349
}
350

351
func getDeploymentRegistryURL(deploymentURLs []houston.DeploymentURL) (string, error) {
8✔
352
        for _, url := range deploymentURLs {
31✔
353
                if url.Type == houston.RegistryURLType {
30✔
354
                        return url.URL, nil
7✔
355
                }
7✔
356
        }
357
        return "", errors.New("no valid registry url found failed to push")
1✔
358
}
359

360
func getDeploymentIDForCurrentCommand(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
16✔
361
        if wsID == "" {
17✔
362
                return deploymentID, []houston.Deployment{}, ErrNoWorkspaceID
1✔
363
        }
1✔
364

365
        // Validate workspace
366
        currentWorkspace, err := houston.Call(houstonClient.GetWorkspace)(wsID)
15✔
367
        if err != nil {
16✔
368
                return deploymentID, []houston.Deployment{}, err
1✔
369
        }
1✔
370

371
        // Get Deployments from workspace ID
372
        request := houston.ListDeploymentsRequest{
14✔
373
                WorkspaceID: currentWorkspace.ID,
14✔
374
        }
14✔
375
        deployments, err := houston.Call(houstonClient.ListDeployments)(request)
14✔
376
        if err != nil {
15✔
377
                return deploymentID, deployments, err
1✔
378
        }
1✔
379

380
        c, err := config.GetCurrentContext()
13✔
381
        if err != nil {
14✔
382
                return deploymentID, deployments, err
1✔
383
        }
1✔
384

385
        cloudDomain := c.Domain
12✔
386
        if cloudDomain == "" {
12✔
387
                return deploymentID, deployments, errNoDomainSet
×
388
        }
×
389

390
        // Use config deployment if provided
391
        if deploymentID == "" {
14✔
392
                deploymentID = config.CFG.ProjectDeployment.GetProjectString()
2✔
393
        }
2✔
394

395
        if deploymentID != "" && !deploymentExists(deploymentID, deployments) {
13✔
396
                return deploymentID, deployments, errInvalidDeploymentID
1✔
397
        }
1✔
398

399
        // Prompt user for deployment if no deployment passed in
400
        if deploymentID == "" || prompt {
13✔
401
                if len(deployments) == 0 {
3✔
402
                        return deploymentID, deployments, errDeploymentNotFound
1✔
403
                }
1✔
404

405
                fmt.Printf(houstonDeploymentHeader, cloudDomain)
1✔
406
                fmt.Println(houstonSelectDeploymentPrompt)
1✔
407

1✔
408
                deployMap := map[string]houston.Deployment{}
1✔
409
                for i := range deployments {
2✔
410
                        deployment := deployments[i]
1✔
411
                        index := i + 1
1✔
412
                        tab.AddRow([]string{strconv.Itoa(index), deployment.Label, deployment.ReleaseName, currentWorkspace.Label, deployment.ID}, false)
1✔
413

1✔
414
                        deployMap[strconv.Itoa(index)] = deployment
1✔
415
                }
1✔
416

417
                tab.Print(os.Stdout)
1✔
418
                choice := input.Text("\n> ")
1✔
419
                selected, ok := deployMap[choice]
1✔
420
                if !ok {
2✔
421
                        return deploymentID, deployments, errInvalidDeploymentSelected
1✔
422
                }
1✔
423
                deploymentID = selected.ID
×
424
        }
425
        return deploymentID, deployments, nil
9✔
426
}
427

428
func isDagOnlyDeploymentEnabled(appConfig *houston.AppConfig) bool {
10✔
429
        return appConfig != nil && appConfig.Flags.DagOnlyDeployment
10✔
430
}
10✔
431

432
func isDagOnlyDeploymentEnabledForDeployment(deploymentInfo *houston.Deployment) bool {
9✔
433
        return deploymentInfo != nil && deploymentInfo.DagDeployment.Type == houston.DagOnlyDeploymentType
9✔
434
}
9✔
435

436
func validateIfDagDeployURLCanBeConstructed(deploymentInfo *houston.Deployment) error {
5✔
437
        _, err := config.GetCurrentContext()
5✔
438
        if err != nil {
6✔
439
                return fmt.Errorf("could not get current context! Error: %w", err)
1✔
440
        }
1✔
441
        if deploymentInfo == nil || deploymentInfo.ReleaseName == "" {
5✔
442
                return errInvalidDeploymentID
1✔
443
        }
1✔
444
        return nil
3✔
445
}
446

447
func getDagDeployURL(deploymentInfo *houston.Deployment) string {
6✔
448
        // Checks if dagserver URL exists and returns the URL
6✔
449
        for _, url := range deploymentInfo.Urls {
11✔
450
                if url.Type == houston.DagServerURLType {
6✔
451
                        logger.Infof("Using dag deploy URL from dagserver: %s", url.URL)
1✔
452
                        return url.URL
1✔
453
                }
1✔
454
        }
455

456
        // If no dagserver URL is found, we look for airflow URL to detect upload url
457
        for _, url := range deploymentInfo.Urls {
8✔
458
                if url.Type != houston.AirflowURLType {
5✔
459
                        continue
2✔
460
                }
461

462
                parsedAirflowURL, err := neturl.Parse(url.URL)
1✔
463
                if err != nil {
1✔
464
                        logger.Infof("Error parsing airflow URL: %v", err)
×
465
                        break
×
466
                }
467

468
                // Use URL scheme and host from the airflow URL
469
                dagUploadURL := fmt.Sprintf("https://%s/%s/dags/upload", parsedAirflowURL.Host, deploymentInfo.ReleaseName)
1✔
470
                logger.Infof("Generated Dag Upload URL from airflow base URL: %s", dagUploadURL)
1✔
471
                return dagUploadURL
1✔
472
        }
473
        return ""
4✔
474
}
475

476
func DagsOnlyDeploy(houstonClient houston.ClientInterface, store keychain.SecureStore, wsID, deploymentID, dagsParentPath string, dagDeployURL *string, cleanUpFiles bool, description string) error {
12✔
477
        deploymentID, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
12✔
478
        if err != nil {
13✔
479
                return err
1✔
480
        }
1✔
481

482
        if deploymentID == "" {
11✔
483
                return errInvalidDeploymentID
×
484
        }
×
485

486
        // Throw error if the feature is disabled at Deployment level
487
        deploymentInfo, err := houston.Call(houstonClient.GetDeployment)(deploymentID)
11✔
488
        if err != nil {
12✔
489
                return fmt.Errorf("failed to get deployment info: %w", err)
1✔
490
        }
1✔
491
        appConfig, err := houston.Call(houstonClient.GetAppConfig)(deploymentInfo.ClusterID)
10✔
492
        if err != nil {
10✔
493
                return fmt.Errorf("failed to get app config: %w", err)
×
494
        }
×
495
        // Throw error if the feature is disabled at Houston level
496
        if !isDagOnlyDeploymentEnabled(appConfig) {
11✔
497
                return ErrDagOnlyDeployDisabledInConfig
1✔
498
        }
1✔
499
        if !isDagOnlyDeploymentEnabledForDeployment(deploymentInfo) {
10✔
500
                return ErrDagOnlyDeployNotEnabledForDeployment
1✔
501
        }
1✔
502

503
        uploadURL := ""
8✔
504
        if dagDeployURL == nil {
13✔
505
                // Throw error if the upload URL can't be constructed
5✔
506
                err = validateIfDagDeployURLCanBeConstructed(deploymentInfo)
5✔
507
                if err != nil {
7✔
508
                        return err
2✔
509
                }
2✔
510
                uploadURL = getDagDeployURL(deploymentInfo)
3✔
511
        } else {
3✔
512
                uploadURL = *dagDeployURL
3✔
513
        }
3✔
514

515
        dagsPath := filepath.Join(dagsParentPath, "dags")
6✔
516
        dagsTarPath := filepath.Join(dagsParentPath, "dags.tar")
6✔
517
        dagsTarGzPath := dagsTarPath + ".gz"
6✔
518
        dagFiles := fileutil.GetFilesWithSpecificExtension(dagsPath, ".py")
6✔
519

6✔
520
        // Alert the user if dags folder is empty
6✔
521
        if len(dagFiles) == 0 && config.CFG.ShowWarnings.GetBool() {
9✔
522
                i, _ := input.Confirm("Warning: No DAGs found. This will delete any existing DAGs. Are you sure you want to deploy?")
3✔
523
                if !i {
4✔
524
                        return ErrEmptyDagFolderUserCancelledOperation
1✔
525
                }
1✔
526
        }
527

528
        // Generate the dags tar
529
        err = fileutil.Tar(dagsPath, dagsTarPath, true, nil)
5✔
530
        if err != nil {
6✔
531
                return err
1✔
532
        }
1✔
533
        if cleanUpFiles {
5✔
534
                defer os.Remove(dagsTarPath)
1✔
535
        }
1✔
536

537
        // Gzip the tar
538
        err = gzipFile(dagsTarPath, dagsTarGzPath)
4✔
539
        if err != nil {
5✔
540
                return err
1✔
541
        }
1✔
542
        if cleanUpFiles {
4✔
543
                defer os.Remove(dagsTarGzPath)
1✔
544
        }
1✔
545

546
        c, _ := config.GetCurrentContext()
3✔
547

3✔
548
        var token string
3✔
549
        if creds, err := store.GetCredentials(c.Domain); err == nil {
3✔
NEW
550
                token = creds.Token
×
NEW
551
        }
×
552

553
        headers := map[string]string{
3✔
554
                "authorization": token,
3✔
555
        }
3✔
556

3✔
557
        uploadFileArgs := fileutil.UploadFileArguments{
3✔
558
                FilePath:            dagsTarGzPath,
3✔
559
                TargetURL:           uploadURL,
3✔
560
                FormFileFieldName:   "file",
3✔
561
                Headers:             headers,
3✔
562
                Description:         description,
3✔
563
                MaxTries:            8,
3✔
564
                InitialDelayInMS:    1 * 1000,
3✔
565
                BackoffFactor:       2,
3✔
566
                RetryDisplayMessage: "please wait, attempting to upload the dags",
3✔
567
        }
3✔
568
        return fileutil.UploadFile(&uploadFileArgs)
3✔
569
}
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

© 2026 Coveralls, Inc