-
Notifications
You must be signed in to change notification settings - Fork 160
Expand file tree
/
Copy pathbundle_helpers.go
More file actions
232 lines (195 loc) · 6.93 KB
/
bundle_helpers.go
File metadata and controls
232 lines (195 loc) · 6.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package apps
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdctx"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/flags"
"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/spf13/cobra"
)
const defaultAppWaitTimeout = 20 * time.Minute
// makeArgsOptionalWithBundle updates a command to allow optional NAME argument
// when running from a bundle directory.
func makeArgsOptionalWithBundle(cmd *cobra.Command, usage string) {
cmd.Use = usage
cmd.Args = func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
return fmt.Errorf("accepts at most 1 arg(s), received %d", len(args))
}
if !hasBundleConfig() && len(args) != 1 {
return missingAppNameError()
}
return nil
}
}
// missingAppNameError returns an error message that explains what the positional
// argument should be, and attempts to infer a suggestion from the local environment.
func missingAppNameError() error {
hint := inferAppNameHint()
msg := `missing required argument: APP_NAME
Usage: databricks apps <command> APP_NAME
APP_NAME is the name of the Databricks app to operate on.
Alternatively, run this command from a project directory containing
databricks.yml to auto-detect the app name.`
if hint != "" {
msg += "\n\nDid you mean?\n databricks apps deploy " + hint
}
return errors.New(msg)
}
// inferAppNameHint tries to suggest an app name from the local environment.
// Only returns a hint if the current directory looks like a Databricks app
// (contains app.yml or app.yaml), using the directory name as the suggestion.
func inferAppNameHint() string {
wd, err := os.Getwd()
if err != nil {
return ""
}
for _, filename := range []string{"app.yml", "app.yaml"} {
if _, err := os.Stat(filepath.Join(wd, filename)); err == nil {
return filepath.Base(wd)
}
}
return ""
}
// getAppNameFromArgs returns the app name from args or detects it from the bundle.
// Returns (appName, fromBundle, error).
func getAppNameFromArgs(cmd *cobra.Command, args []string) (string, bool, error) {
if len(args) > 0 {
return args[0], false, nil
}
appName := detectAppNameFromBundle(cmd)
if appName != "" {
return appName, true, nil
}
return "", false, errors.New("no app name provided and unable to detect from project configuration")
}
// updateCommandHelp updates the help text for a command to explain bundle behavior.
func updateCommandHelp(cmd *cobra.Command, commandVerb, commandName string) {
cmd.Long = fmt.Sprintf(`%s an app.
When run from a Databricks Apps project directory (containing databricks.yml)
without a NAME argument, this command automatically detects the app name from
the project configuration and %ss it.
When a NAME argument is provided (or when not in a project directory),
%ss the specified app using the API directly.
Arguments:
NAME: The name of the app. Required when not in a project directory.
When provided in a project directory, uses the specified name instead of auto-detection.
Examples:
# %s app from a project directory (auto-detects app name)
databricks apps %s
# %s app from a specific target
databricks apps %s --target prod
# %s a specific app using the API (even from a project directory)
databricks apps %s my-app`,
commandVerb,
commandName,
commandName,
commandVerb,
commandName,
commandVerb,
commandName,
commandVerb,
commandName)
}
// isIdempotencyError checks if an error message indicates the operation is already in the desired state.
func isIdempotencyError(err error, keywords ...string) bool {
if err == nil {
return false
}
errMsg := err.Error()
for _, keyword := range keywords {
if strings.Contains(errMsg, keyword) {
return true
}
}
return false
}
// displayAppURL displays the app URL in a consistent format if available.
func displayAppURL(ctx context.Context, appInfo *apps.App) {
if appInfo != nil && appInfo.Url != "" {
cmdio.LogString(ctx, fmt.Sprintf("\n🔗 %s\n", appInfo.Url))
}
}
// formatAppStatusMessage formats a user-friendly status message for an app.
func formatAppStatusMessage(appInfo *apps.App, appName, verb string) string {
computeState := "unknown"
if appInfo != nil && appInfo.ComputeStatus != nil {
computeState = string(appInfo.ComputeStatus.State)
}
if appInfo != nil && appInfo.AppStatus != nil && appInfo.AppStatus.State == apps.ApplicationStateUnavailable {
return fmt.Sprintf("⚠ App '%s' %s but is unavailable (compute: %s, app: %s)", appName, verb, computeState, appInfo.AppStatus.State)
}
if appInfo != nil && appInfo.ComputeStatus != nil {
state := appInfo.ComputeStatus.State
switch state {
case apps.ComputeStateActive:
if verb == "is deployed" {
return fmt.Sprintf("✔ App '%s' is already running (status: %s)", appName, state)
}
return fmt.Sprintf("✔ App '%s' started successfully (status: %s)", appName, state)
case apps.ComputeStateStarting:
return fmt.Sprintf("⚠ App '%s' is already starting (status: %s)", appName, state)
default:
return fmt.Sprintf("✔ App '%s' status: %s", appName, state)
}
}
return fmt.Sprintf("✔ App '%s' status: unknown", appName)
}
// getWaitTimeout gets the timeout value for app wait operations.
func getWaitTimeout(cmd *cobra.Command) time.Duration {
timeout, _ := cmd.Flags().GetDuration("timeout")
if timeout == 0 {
timeout = defaultAppWaitTimeout
}
return timeout
}
// shouldWaitForCompletion checks if the command should wait for app operation completion.
func shouldWaitForCompletion(cmd *cobra.Command) bool {
skipWait, _ := cmd.Flags().GetBool("no-wait")
return !skipWait
}
// spinnerInterface matches the interface provided by cmdio.NewSpinner.
type spinnerInterface interface {
Update(msg string)
Close()
}
// createAppProgressCallback creates a progress callback for app operations.
func createAppProgressCallback(spinner spinnerInterface) func(*apps.App) {
return func(i *apps.App) {
if i.ComputeStatus == nil {
return
}
statusMessage := i.ComputeStatus.Message
if statusMessage == "" {
statusMessage = fmt.Sprintf("current status: %s", i.ComputeStatus.State)
}
spinner.Update(statusMessage)
}
}
// handleAlreadyInStateError handles idempotency errors and displays appropriate status.
// Returns true if the error was handled (already in desired state), false otherwise.
func handleAlreadyInStateError(ctx context.Context, cmd *cobra.Command, err error, appName string, keywords []string, verb string, wrapError ErrorWrapper) (bool, error) {
if !isIdempotencyError(err, keywords...) {
return false, nil
}
outputFormat := root.OutputType(cmd)
if outputFormat != flags.OutputText {
return true, nil
}
w := cmdctx.WorkspaceClient(ctx)
appInfo, getErr := w.Apps.Get(ctx, apps.GetAppRequest{Name: appName})
if getErr != nil {
return true, wrapError(cmd, appName, getErr)
}
message := formatAppStatusMessage(appInfo, appName, verb)
cmdio.LogString(ctx, message)
displayAppURL(ctx, appInfo)
return true, nil
}