package generate import ( "context" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "github.com/databricks/cli/bundle/config/generate" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/dyn/yamlsaver" "github.com/databricks/cli/libs/filer" "github.com/databricks/cli/libs/textutil" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) func NewGenerateAppCommand() *cobra.Command { var configDir string var sourceDir string var appName string var force bool cmd := &cobra.Command{ Use: "app", Short: "Generate bundle configuration for a Databricks app", } cmd.Flags().StringVar(&appName, "existing-app-name", "", `App name to generate config for`) cmd.MarkFlagRequired("existing-app-name") wd, err := os.Getwd() if err != nil { wd = "." } cmd.Flags().StringVarP(&configDir, "config-dir", "d", filepath.Join(wd, "resources"), `Directory path where the output bundle config will be stored`) cmd.Flags().StringVarP(&sourceDir, "source-dir", "s", filepath.Join(wd, "src", "app"), `Directory path where the app files will be stored`) cmd.Flags().BoolVarP(&force, "force", "f", false, `Force overwrite existing files in the output directory`) cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() b, diags := root.MustConfigureBundle(cmd) if err := diags.Error(); err != nil { return diags.Error() } w := b.WorkspaceClient() cmdio.LogString(ctx, fmt.Sprintf("Loading app '%s' configuration", appName)) app, err := w.Apps.Get(ctx, apps.GetAppRequest{Name: appName}) if err != nil { return err } downloader := newDownloader(w, sourceDir, configDir) sourceCodePath := app.DefaultSourceCodePath downloader.markDirectoryForDownload(ctx, &sourceCodePath) appConfig, err := getAppConfig(ctx, app, w) if err != nil { return fmt.Errorf("failed to get app config: %w", err) } v, err := generate.ConvertAppToValue(app, sourceCodePath, appConfig) if err != nil { return err } appKey := cmd.Flag("key").Value.String() if appKey == "" { appKey = textutil.NormalizeString(app.Name) } result := map[string]dyn.Value{ "resources": dyn.V(map[string]dyn.Value{ "apps": dyn.V(map[string]dyn.Value{ appKey: v, }), }), } // If there are app.yaml or app.yml files in the source code path, they will be downloaded but we don't want to include them in the bundle. // We include this configuration inline, so we need to remove these files. for _, configFile := range []string{"app.yml", "app.yaml"} { delete(downloader.files, filepath.Join(sourceDir, configFile)) } err = downloader.FlushToDisk(ctx, force) if err != nil { return err } filename := filepath.Join(configDir, fmt.Sprintf("%s.app.yml", appKey)) saver := yamlsaver.NewSaver() err = saver.SaveAsYAML(result, filename, force) if err != nil { return err } cmdio.LogString(ctx, fmt.Sprintf("App configuration successfully saved to %s", filename)) return nil } return cmd } func getAppConfig(ctx context.Context, app *apps.App, w *databricks.WorkspaceClient) (map[string]interface{}, error) { sourceCodePath := app.DefaultSourceCodePath f, err := filer.NewWorkspaceFilesClient(w, sourceCodePath) if err != nil { return nil, err } // The app config is stored in app.yml or app.yaml file in the source code path. configFileNames := []string{"app.yml", "app.yaml"} for _, configFile := range configFileNames { r, err := f.Read(ctx, configFile) if err != nil { if errors.Is(err, fs.ErrNotExist) { continue } return nil, err } defer r.Close() cmdio.LogString(ctx, fmt.Sprintf("Reading app configuration from %s", configFile)) content, err := io.ReadAll(r) var appConfig map[string]interface{} err = yaml.Unmarshal(content, &appConfig) if err != nil { cmdio.LogString(ctx, fmt.Sprintf("Failed to parse app configuration:\n%s\nerr: %v", string(content), err)) return nil, nil } return appConfig, nil } return nil, nil }