-
Notifications
You must be signed in to change notification settings - Fork 127
Allow to export dashboards from Serverless environments #3007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 15 commits
dc54003
2fa7ec3
66a1770
54be65a
d843e20
7d6211b
d7b269b
6542265
a8b59fb
b6dccd2
a2b0823
53cdeb3
9cb72c3
14db2d7
243539a
402339c
1069e50
7775a0a
f491e12
1ab5ec3
0609cae
9d4417a
9d9c0e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,9 @@ package cmd | |
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/elastic/elastic-package/internal/packages" | ||
"github.com/elastic/elastic-package/internal/tui" | ||
|
||
"github.com/spf13/cobra" | ||
|
@@ -19,10 +21,14 @@ import ( | |
"github.com/elastic/elastic-package/internal/stack" | ||
) | ||
|
||
const exportDashboardsLongDescription = `Use this command to export dashboards with referenced objects from the Kibana instance. | ||
const ( | ||
exportDashboardsLongDescription = `Use this command to export dashboards with referenced objects from the Kibana instance. | ||
|
||
Use this command to download selected dashboards and other associated saved objects from Kibana. This command adjusts the downloaded saved objects according to package naming conventions (prefixes, unique IDs) and writes them locally into folders corresponding to saved object types (dashboard, visualization, map, etc.).` | ||
|
||
newDashboardOption = "Working on a new dashboard (show all available dashboards)" | ||
|
||
) | ||
|
||
func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | ||
cmd.Println("Export Kibana dashboards") | ||
|
||
|
@@ -67,10 +73,26 @@ func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | |
fmt.Printf("Warning: %s\n", message) | ||
} | ||
|
||
// Just query for dashboards if none were provided as flags | ||
if len(dashboardIDs) == 0 { | ||
dashboardIDs, err = promptDashboardIDs(cmd.Context(), kibanaClient) | ||
packageRoot, err := packages.MustFindPackageRoot() | ||
if err != nil { | ||
return fmt.Errorf("locating package root failed: %w", err) | ||
} | ||
m, err := packages.ReadPackageManifestFromPackageRoot(packageRoot) | ||
if err != nil { | ||
return fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
return fmt.Errorf("reading package manifest failed (path: %s): %w", packageRoot, err) | ||
} | ||
options := selectDashboardOptions{ | ||
ctx: cmd.Context(), | ||
kibanaClient: kibanaClient, | ||
kibanaVersion: kibanaVersion, | ||
defaultPackage: m.Name, | ||
} | ||
|
||
dashboardIDs, err = selectDashboardIDs(options) | ||
if err != nil { | ||
return fmt.Errorf("selecting dashboard IDs failed: %w", err) | ||
} | ||
|
||
if len(dashboardIDs) == 0 { | ||
|
@@ -88,12 +110,171 @@ func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | |
return nil | ||
} | ||
|
||
func promptDashboardIDs(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
type selectDashboardOptions struct { | ||
ctx context.Context | ||
kibanaClient *kibana.Client | ||
kibanaVersion kibana.VersionInfo | ||
defaultPackage string | ||
} | ||
|
||
// selectDashboardIDs prompts the user to select dashboards to export. It handles | ||
// different flows depending on whether the Kibana instance is a Serverless environment or not. | ||
// In non-Serverless environments, it prompts directly for dashboard selection. | ||
// In Serverless environments, it first prompts to select an installed package or choose | ||
// to export new dashboards, and then prompts for dashboard selection accordingly. | ||
func selectDashboardIDs(options selectDashboardOptions) ([]string, error) { | ||
if options.kibanaVersion.BuildFlavor != kibana.ServerlessFlavor { | ||
// This method uses a deprecated API to search for saved objects. | ||
// And this API is not available in Serverless environments. | ||
dashboardIDs, err := promptDashboardIDsNonServerless(options.ctx, options.kibanaClient) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
|
||
installedPackage, err := promptPackagesInstalled(options.ctx, options.kibanaClient, options.defaultPackage) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for package selection failed: %w", err) | ||
} | ||
|
||
if installedPackage == "" { | ||
fmt.Println("No installed packages were found in Kibana.") | ||
return nil, nil | ||
} | ||
|
||
if installedPackage == newDashboardOption { | ||
dashboardIDs, err := promptDashboardIDsServerless(options.ctx, options.kibanaClient) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
|
||
// As it can be installed just one version of a package in Elastic, we can split by '-' to get the name. | ||
// This package name will be used to get the data related to a package (kibana.GetPackage). | ||
installedPackageName, _, found := strings.Cut(installedPackage, "-") | ||
if !found { | ||
return nil, fmt.Errorf("invalid package name: %s", installedPackage) | ||
} | ||
|
||
dashboardIDs, err := promptPackageDashboardIDs(options.ctx, options.kibanaClient, installedPackageName) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for package dashboard selection failed: %w", err) | ||
} | ||
|
||
return dashboardIDs, nil | ||
} | ||
|
||
func promptPackagesInstalled(ctx context.Context, kibanaClient *kibana.Client, defaultPackageName string) (string, error) { | ||
installedPackages, err := kibanaClient.FindInstalledPackages(ctx) | ||
if err != nil { | ||
return "", fmt.Errorf("finding installed packages failed: %w", err) | ||
} | ||
|
||
// First option is always to list all available dashboards even if they are not related | ||
// to any package. This is helpful in case the user is working on a new dashboard. | ||
options := []string{newDashboardOption} | ||
|
||
options = append(options, installedPackages.Strings()...) | ||
defaultOption := "" | ||
for _, ip := range installedPackages { | ||
if ip.Name == defaultPackageName { | ||
// set default package to the one matching the package in the current directory | ||
defaultOption = ip.String() | ||
break | ||
} | ||
} | ||
Comment on lines
+194
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice detail 👍 |
||
|
||
packagesPrompt := tui.NewSelect("Which packages would you like to export dashboards from?", options, defaultOption) | ||
mrodm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
var selectedOption string | ||
err = tui.AskOne(packagesPrompt, &selectedOption, tui.Required) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return selectedOption, nil | ||
} | ||
|
||
// promptPackageDashboardIDs prompts the user to select dashboards from the given package. | ||
// It requires the package name to fetch the installed package information from Kibana. | ||
func promptPackageDashboardIDs(ctx context.Context, kibanaClient *kibana.Client, packageName string) ([]string, error) { | ||
installedPackage, err := kibanaClient.GetPackage(ctx, packageName) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get package status: %w", err) | ||
} | ||
if installedPackage.Status == "not_installed" { | ||
return nil, fmt.Errorf("package %s is not installed", packageName) | ||
} | ||
|
||
// get asset titles from IDs | ||
packageAssets := []packages.Asset{} | ||
for _, asset := range installedPackage.InstallationInfo.InstalledKibanaAssets { | ||
if asset.Type != "dashboard" { | ||
continue | ||
} | ||
|
||
packageAssets = append(packageAssets, packages.Asset{ID: asset.ID, Type: asset.Type}) | ||
} | ||
|
||
assetsResponse, err := kibanaClient.GetDataFromPackageAssetIDs(ctx, packageAssets) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get package assets: %w", err) | ||
} | ||
|
||
dashboardIDOptions := []string{} | ||
for _, asset := range assetsResponse { | ||
if asset.Type != "dashboard" { | ||
continue | ||
} | ||
dashboardIDOptions = append(dashboardIDOptions, asset.String()) | ||
} | ||
|
||
if len(dashboardIDOptions) == 0 { | ||
return nil, fmt.Errorf("no dashboards found for package %s", packageName) | ||
} | ||
|
||
dashboardsPrompt := tui.NewMultiSelect("Which dashboards would you like to export?", dashboardIDOptions, []string{}) | ||
dashboardsPrompt.SetPageSize(100) | ||
|
||
var selectedOptions []string | ||
err = tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var selectedIDs []string | ||
for _, option := range selectedOptions { | ||
for _, asset := range assetsResponse { | ||
if asset.String() == option { | ||
selectedIDs = append(selectedIDs, asset.ID) | ||
} | ||
} | ||
} | ||
|
||
return selectedIDs, nil | ||
} | ||
|
||
func promptDashboardIDsServerless(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
savedDashboards, err := kibanaClient.FindServerlessDashboards(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("finding dashboards failed: %w", err) | ||
} | ||
|
||
return promptDashboardIDs(savedDashboards) | ||
} | ||
|
||
func promptDashboardIDsNonServerless(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
savedDashboards, err := kibanaClient.FindDashboards(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("finding dashboards failed: %w", err) | ||
} | ||
|
||
return promptDashboardIDs(savedDashboards) | ||
} | ||
|
||
func promptDashboardIDs(savedDashboards kibana.DashboardSavedObjects) ([]string, error) { | ||
if len(savedDashboards) == 0 { | ||
return []string{}, nil | ||
} | ||
|
@@ -102,7 +283,7 @@ func promptDashboardIDs(ctx context.Context, kibanaClient *kibana.Client) ([]str | |
dashboardsPrompt.SetPageSize(100) | ||
|
||
var selectedOptions []string | ||
err = tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
err := tui.AskOne(dashboardsPrompt, &selectedOptions, tui.Required) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be helpful for the user to select the package where they are located if any. But this should not raise an error if the command does not run from a package.