package generate import ( "context" "errors" "fmt" "io" "io/fs" "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") cmd.Flags().StringVarP(&configDir, "config-dir", "d", filepath.Join("resources"), `Directory path where the output bundle config will be stored`) cmd.Flags().StringVarP(&sourceDir, "source-dir", "s", filepath.Join("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 } // Making sure the config directory and source directory are absolute paths. if !filepath.IsAbs(configDir) { configDir = filepath.Join(b.BundleRootPath, configDir) } if !filepath.IsAbs(sourceDir) { sourceDir = filepath.Join(b.BundleRootPath, sourceDir) } downloader := newDownloader(w, sourceDir, configDir) sourceCodePath := app.DefaultSourceCodePath err = downloader.markDirectoryForDownload(ctx, &sourceCodePath) if err != nil { return err } appConfig, err := getAppConfig(ctx, app, w) if err != nil { return fmt.Errorf("failed to get app config: %w", err) } // Making sure the source code path is relative to the config directory. rel, err := filepath.Rel(configDir, sourceDir) if err != nil { return err } v, err := generate.ConvertAppToValue(app, filepath.ToSlash(rel), 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, appKey+".app.yml") saver := yamlsaver.NewSaver() err = saver.SaveAsYAML(result, filename, force) if err != nil { return err } cmdio.LogString(ctx, "App configuration successfully saved to "+filename) return nil } return cmd } func getAppConfig(ctx context.Context, app *apps.App, w *databricks.WorkspaceClient) (map[string]any, 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, "Reading app configuration from "+configFile) content, err := io.ReadAll(r) if err != nil { return nil, err } var appConfig map[string]any 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 }