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

astronomer / astro-cli / cf96173f-7a71-4719-b6e4-498189ea6de6

08 Oct 2025 05:36PM UTC coverage: 38.492% (+0.05%) from 38.44%
cf96173f-7a71-4719-b6e4-498189ea6de6

Pull #1954

circleci

feluelle
Add client deploy support for RE projects

- add `--client` flag to deploy command
- add `--platform` flag to deploy command
Pull Request #1954: Add client deploy support for RE projects

79 of 90 new or added lines in 3 files covered. (87.78%)

54 existing lines in 5 files now uncovered.

24107 of 62628 relevant lines covered (38.49%)

10.74 hits per line

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

87.53
/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/astronomer/astro-cli/airflow"
14
        "github.com/astronomer/astro-cli/airflow/types"
15
        "github.com/astronomer/astro-cli/config"
16
        "github.com/astronomer/astro-cli/docker"
17
        "github.com/astronomer/astro-cli/houston"
18
        "github.com/astronomer/astro-cli/pkg/fileutil"
19
        "github.com/astronomer/astro-cli/pkg/input"
20
        "github.com/astronomer/astro-cli/pkg/logger"
21
        "github.com/astronomer/astro-cli/pkg/printutil"
22
        "github.com/astronomer/astro-cli/software/auth"
23
        "github.com/docker/docker/api/types/versions"
24
)
25

26
var (
27
        // this is used to monkey patch the function in order to write unit test cases
28
        imageHandlerInit = airflow.ImageHandlerInit
29

30
        dockerfile = "Dockerfile"
31

32
        deployImagePlatformSupport = []string{"linux/amd64"}
33

34
        gzipFile = fileutil.GzipFile
35

36
        getDeploymentIDForCurrentCommandVar = getDeploymentIDForCurrentCommand
37
)
38

39
var (
40
        ErrNoWorkspaceID                         = errors.New("no workspace id provided")
41
        errNoDomainSet                           = errors.New("no domain set, re-authenticate")
42
        errInvalidDeploymentID                   = errors.New("please specify a valid deployment ID")
43
        errDeploymentNotFound                    = errors.New("no airflow deployments found")
44
        errInvalidDeploymentSelected             = errors.New("invalid deployment selection\n") //nolint
45
        ErrDagOnlyDeployDisabledInConfig         = errors.New("to perform this operation, set both deployments.dagOnlyDeployment and deployments.configureDagDeployment to true in your Astronomer cluster")
46
        ErrDagOnlyDeployNotEnabledForDeployment  = errors.New("to perform this operation, first set the Deployment type to 'dag_deploy' via the UI or the API or the CLI")
47
        ErrEmptyDagFolderUserCancelledOperation  = errors.New("no DAGs found in the dags folder. User canceled the operation")
48
        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
49
        ErrDeploymentTypeIncorrectForImageOnly   = errors.New("--image only works for Dag-only, Git-sync-based and NFS-based deployments")
50
        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"
51
        ErrNoRuntimeLabelOnCustomImage           = errors.New("the image should have label io.astronomer.docker.runtime.version")
52
        ErrRuntimeVersionNotPassedForRemoteImage = errors.New("if --image-name and --remote is passed, it's mandatory to pass --runtime-version")
53
)
54

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

60
        imageBuildingPrompt = "Building image..."
61

62
        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"
63
        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?"
64
        warningInvalidNameTagEmptyRecommendations = "WARNING! You are about to push an image using the '%s' tag. This is not recommended.\nAre you sure you want to continue?"
65

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

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

78
func Airflow(houstonClient houston.ClientInterface, path, deploymentID, wsID, byoRegistryDomain string, ignoreCacheDeploy, byoRegistryEnabled, prompt bool, description string, isImageOnlyDeploy bool, imageName string) (string, error) {
14✔
79
        deploymentID, deployments, err := getDeploymentIDForCurrentCommand(houstonClient, wsID, deploymentID, prompt)
14✔
80
        if err != nil {
21✔
81
                return deploymentID, err
7✔
82
        }
7✔
83

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

96
        if byoRegistryEnabled {
7✔
UNCOV
97
                nextTag = "deploy-" + time.Now().UTC().Format("2006-01-02T15-04") // updating nextTag logic for private registry, since houston won't maintain next tag in case of BYO registry
×
UNCOV
98
        }
×
99

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

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

110
        if appConfig != nil && appConfig.Flags.BYORegistryEnabled {
6✔
UNCOV
111
                byoRegistryEnabled = true
×
UNCOV
112
                byoRegistryDomain = appConfig.BYORegistryDomain
×
UNCOV
113
                if byoRegistryDomain == "" {
×
UNCOV
114
                        return deploymentID, ErrBYORegistryDomainNotSet
×
UNCOV
115
                }
×
116
        }
117

118
        // isImageOnlyDeploy is not valid for image-based deployments since image-based deployments inherently mean that the image itself contains dags.
119
        // If we deploy only the image, the deployment will not have any dags for image-based deployments.
120
        // Even on astro, image-based deployments are not allowed to be deployed with --image flag.
121
        if isImageOnlyDeploy && deploymentInfo.DagDeployment.Type == houston.ImageDeploymentType {
7✔
122
                return "", ErrDeploymentTypeIncorrectForImageOnly
1✔
123
        }
1✔
124
        // 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
125

126
        fmt.Printf(houstonDeploymentPrompt, releaseName)
5✔
127

5✔
128
        // Build the image to deploy
5✔
129
        err = buildPushDockerImage(houstonClient, &c, deploymentInfo, releaseName, path, nextTag, cloudDomain, byoRegistryDomain, ignoreCacheDeploy, byoRegistryEnabled, description, imageName)
5✔
130
        if err != nil {
7✔
131
                return deploymentID, err
2✔
132
        }
2✔
133

134
        deploymentLink := getAirflowUILink(deploymentID, deploymentInfo.Urls)
3✔
135
        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)
3✔
136

3✔
137
        return deploymentID, nil
3✔
138
}
139

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

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

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

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

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

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

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

274
func buildDockerImageFromWorkingDir(path string, imageHandler airflow.ImageHandler, houstonClient houston.ClientInterface, deploymentInfo *houston.Deployment, ignoreCacheDeploy bool, description string) error {
11✔
275
        // all these checks inside Dockerfile should happen only when no image-name is provided
11✔
276
        // parse dockerfile
11✔
277
        cmds, err := docker.ParseFile(filepath.Join(path, dockerfile))
11✔
278
        if err != nil {
13✔
279
                return fmt.Errorf("failed to parse dockerfile: %s: %w", filepath.Join(path, dockerfile), err)
2✔
280
        }
2✔
281

282
        _, tag := docker.GetImageTagFromParsedFile(cmds)
9✔
283

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

8✔
302
        err = imageHandler.Build("", "", buildConfig)
8✔
303
        return err
8✔
304
}
305

306
func buildDockerImage(ignoreCacheDeploy bool, deploymentInfo *houston.Deployment, customImageName, path string, imageHandler airflow.ImageHandler, houstonClient houston.ClientInterface, description string) error {
14✔
307
        if customImageName == "" {
25✔
308
                return buildDockerImageFromWorkingDir(path, imageHandler, houstonClient, deploymentInfo, ignoreCacheDeploy, description)
11✔
309
        }
11✔
310
        return buildDockerImageForCustomImage(imageHandler, customImageName, deploymentInfo, houstonClient)
3✔
311
}
312

313
func getGetTagFromImageName(imageName string) string {
2✔
314
        parts := strings.Split(imageName, ":")
2✔
315
        if len(parts) == 2 {
3✔
316
                return parts[1]
1✔
317
        }
1✔
318
        return ""
1✔
319
}
320

321
func buildPushDockerImage(houstonClient houston.ClientInterface, c *config.Context, deploymentInfo *houston.Deployment, name, path, nextTag, cloudDomain, byoRegistryDomain string, ignoreCacheDeploy, byoRegistryEnabled bool, description, customImageName string) error {
14✔
322
        imageName := airflow.ImageName(name, "latest")
14✔
323
        imageHandler := imageHandlerInit(imageName)
14✔
324
        err := buildDockerImage(ignoreCacheDeploy, deploymentInfo, customImageName, path, imageHandler, houstonClient, description)
14✔
325
        if err != nil {
19✔
326
                return err
5✔
327
        }
5✔
328
        return pushDockerImage(byoRegistryEnabled, deploymentInfo, byoRegistryDomain, name, nextTag, cloudDomain, imageHandler, houstonClient, c, customImageName)
9✔
329
}
330

331
func getAirflowUILink(deploymentID string, deploymentURLs []houston.DeploymentURL) string {
6✔
332
        if deploymentID == "" {
7✔
333
                return ""
1✔
334
        }
1✔
335

336
        for _, url := range deploymentURLs {
9✔
337
                if url.Type == houston.AirflowURLType {
8✔
338
                        return url.URL
4✔
339
                }
4✔
340
        }
341
        return ""
1✔
342
}
343

344
func getDeploymentRegistryURL(deploymentURLs []houston.DeploymentURL) (string, error) {
8✔
345
        for _, url := range deploymentURLs {
31✔
346
                if url.Type == houston.RegistryURLType {
30✔
347
                        return url.URL, nil
7✔
348
                }
7✔
349
        }
350
        return "", errors.New("no valid registry url found failed to push")
1✔
351
}
352

353
func getDeploymentIDForCurrentCommand(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
14✔
354
        if wsID == "" {
15✔
355
                return deploymentID, []houston.Deployment{}, ErrNoWorkspaceID
1✔
356
        }
1✔
357

358
        // Validate workspace
359
        currentWorkspace, err := houston.Call(houstonClient.GetWorkspace)(wsID)
13✔
360
        if err != nil {
14✔
361
                return deploymentID, []houston.Deployment{}, err
1✔
362
        }
1✔
363

364
        // Get Deployments from workspace ID
365
        request := houston.ListDeploymentsRequest{
12✔
366
                WorkspaceID: currentWorkspace.ID,
12✔
367
        }
12✔
368
        deployments, err := houston.Call(houstonClient.ListDeployments)(request)
12✔
369
        if err != nil {
13✔
370
                return deploymentID, deployments, err
1✔
371
        }
1✔
372

373
        c, err := config.GetCurrentContext()
11✔
374
        if err != nil {
12✔
375
                return deploymentID, deployments, err
1✔
376
        }
1✔
377

378
        cloudDomain := c.Domain
10✔
379
        if cloudDomain == "" {
10✔
380
                return deploymentID, deployments, errNoDomainSet
×
UNCOV
381
        }
×
382

383
        // Use config deployment if provided
384
        if deploymentID == "" {
12✔
385
                deploymentID = config.CFG.ProjectDeployment.GetProjectString()
2✔
386
        }
2✔
387

388
        if deploymentID != "" && !deploymentExists(deploymentID, deployments) {
11✔
389
                return deploymentID, deployments, errInvalidDeploymentID
1✔
390
        }
1✔
391

392
        // Prompt user for deployment if no deployment passed in
393
        if deploymentID == "" || prompt {
11✔
394
                if len(deployments) == 0 {
3✔
395
                        return deploymentID, deployments, errDeploymentNotFound
1✔
396
                }
1✔
397

398
                fmt.Printf(houstonDeploymentHeader, cloudDomain)
1✔
399
                fmt.Println(houstonSelectDeploymentPrompt)
1✔
400

1✔
401
                deployMap := map[string]houston.Deployment{}
1✔
402
                for i := range deployments {
2✔
403
                        deployment := deployments[i]
1✔
404
                        index := i + 1
1✔
405
                        tab.AddRow([]string{strconv.Itoa(index), deployment.Label, deployment.ReleaseName, currentWorkspace.Label, deployment.ID}, false)
1✔
406

1✔
407
                        deployMap[strconv.Itoa(index)] = deployment
1✔
408
                }
1✔
409

410
                tab.Print(os.Stdout)
1✔
411
                choice := input.Text("\n> ")
1✔
412
                selected, ok := deployMap[choice]
1✔
413
                if !ok {
2✔
414
                        return deploymentID, deployments, errInvalidDeploymentSelected
1✔
415
                }
1✔
UNCOV
416
                deploymentID = selected.ID
×
417
        }
418
        return deploymentID, deployments, nil
7✔
419
}
420

421
func isDagOnlyDeploymentEnabled(appConfig *houston.AppConfig) bool {
10✔
422
        return appConfig != nil && appConfig.Flags.DagOnlyDeployment
10✔
423
}
10✔
424

425
func isDagOnlyDeploymentEnabledForDeployment(deploymentInfo *houston.Deployment) bool {
9✔
426
        return deploymentInfo != nil && deploymentInfo.DagDeployment.Type == houston.DagOnlyDeploymentType
9✔
427
}
9✔
428

429
func validateIfDagDeployURLCanBeConstructed(deploymentInfo *houston.Deployment) error {
5✔
430
        _, err := config.GetCurrentContext()
5✔
431
        if err != nil {
6✔
432
                return fmt.Errorf("could not get current context! Error: %w", err)
1✔
433
        }
1✔
434
        if deploymentInfo == nil || deploymentInfo.ReleaseName == "" {
5✔
435
                return errInvalidDeploymentID
1✔
436
        }
1✔
437
        return nil
3✔
438
}
439

440
func getDagDeployURL(deploymentInfo *houston.Deployment) string {
6✔
441
        // Checks if dagserver URL exists and returns the URL
6✔
442
        for _, url := range deploymentInfo.Urls {
11✔
443
                if url.Type == houston.DagServerURLType {
6✔
444
                        logger.Infof("Using dag deploy URL from dagserver: %s", url.URL)
1✔
445
                        return url.URL
1✔
446
                }
1✔
447
        }
448

449
        // If no dagserver URL is found, we look for airflow URL to detect upload url
450
        for _, url := range deploymentInfo.Urls {
8✔
451
                if url.Type != houston.AirflowURLType {
5✔
452
                        continue
2✔
453
                }
454

455
                parsedAirflowURL, err := neturl.Parse(url.URL)
1✔
456
                if err != nil {
1✔
457
                        logger.Infof("Error parsing airflow URL: %v", err)
×
UNCOV
458
                        break
×
459
                }
460

461
                // Use URL scheme and host from the airflow URL
462
                dagUploadURL := fmt.Sprintf("https://%s/%s/dags/upload", parsedAirflowURL.Host, deploymentInfo.ReleaseName)
1✔
463
                logger.Infof("Generated Dag Upload URL from airflow base URL: %s", dagUploadURL)
1✔
464
                return dagUploadURL
1✔
465
        }
466
        return ""
4✔
467
}
468

469
func DagsOnlyDeploy(houstonClient houston.ClientInterface, wsID, deploymentID, dagsParentPath string, dagDeployURL *string, cleanUpFiles bool, description string) error {
12✔
470
        deploymentID, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
12✔
471
        if err != nil {
13✔
472
                return err
1✔
473
        }
1✔
474

475
        if deploymentID == "" {
11✔
476
                return errInvalidDeploymentID
×
UNCOV
477
        }
×
478

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

496
        uploadURL := ""
8✔
497
        if dagDeployURL == nil {
13✔
498
                // Throw error if the upload URL can't be constructed
5✔
499
                err = validateIfDagDeployURLCanBeConstructed(deploymentInfo)
5✔
500
                if err != nil {
7✔
501
                        return err
2✔
502
                }
2✔
503
                uploadURL = getDagDeployURL(deploymentInfo)
3✔
504
        } else {
3✔
505
                uploadURL = *dagDeployURL
3✔
506
        }
3✔
507

508
        dagsPath := filepath.Join(dagsParentPath, "dags")
6✔
509
        dagsTarPath := filepath.Join(dagsParentPath, "dags.tar")
6✔
510
        dagsTarGzPath := dagsTarPath + ".gz"
6✔
511
        dagFiles := fileutil.GetFilesWithSpecificExtension(dagsPath, ".py")
6✔
512

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

521
        // Generate the dags tar
522
        err = fileutil.Tar(dagsPath, dagsTarPath, true, nil)
5✔
523
        if err != nil {
6✔
524
                return err
1✔
525
        }
1✔
526
        if cleanUpFiles {
5✔
527
                defer os.Remove(dagsTarPath)
1✔
528
        }
1✔
529

530
        // Gzip the tar
531
        err = gzipFile(dagsTarPath, dagsTarGzPath)
4✔
532
        if err != nil {
5✔
533
                return err
1✔
534
        }
1✔
535
        if cleanUpFiles {
4✔
536
                defer os.Remove(dagsTarGzPath)
1✔
537
        }
1✔
538

539
        c, _ := config.GetCurrentContext()
3✔
540

3✔
541
        headers := map[string]string{
3✔
542
                "authorization": c.Token,
3✔
543
        }
3✔
544

3✔
545
        uploadFileArgs := fileutil.UploadFileArguments{
3✔
546
                FilePath:            dagsTarGzPath,
3✔
547
                TargetURL:           uploadURL,
3✔
548
                FormFileFieldName:   "file",
3✔
549
                Headers:             headers,
3✔
550
                Description:         description,
3✔
551
                MaxTries:            8,
3✔
552
                InitialDelayInMS:    1 * 1000,
3✔
553
                BackoffFactor:       2,
3✔
554
                RetryDisplayMessage: "please wait, attempting to upload the dags",
3✔
555
        }
3✔
556
        return fileutil.UploadFile(&uploadFileArgs)
3✔
557
}
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