-
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 all commits
dc54003
2fa7ec3
66a1770
54be65a
d843e20
7d6211b
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 = "New dashboard" | ||
) | ||
|
||
func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | ||
cmd.Println("Export Kibana dashboards") | ||
|
||
|
@@ -67,16 +73,17 @@ 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) | ||
dashboardIDs, err = selectDashboardIds(cmd, kibanaClient, kibanaVersion) | ||
if err != nil { | ||
return fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
return fmt.Errorf("selecting dashboard IDs failed: %w", err) | ||
} | ||
} | ||
|
||
if len(dashboardIDs) == 0 { | ||
fmt.Println("No dashboards were found in Kibana.") | ||
return nil | ||
} | ||
if len(dashboardIDs) == 0 { | ||
fmt.Println("No dashboards were found in Kibana.") | ||
return nil | ||
} | ||
|
||
err = export.Dashboards(cmd.Context(), kibanaClient, dashboardIDs) | ||
|
@@ -88,6 +95,154 @@ func exportDashboardsCmd(cmd *cobra.Command, args []string) error { | |
return nil | ||
} | ||
|
||
// 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(cmd *cobra.Command, kibanaClient *kibana.Client, kibanaVersion kibana.VersionInfo) ([]string, error) { | ||
if 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 := promptDashboardIDs(cmd.Context(), kibanaClient) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
|
||
installedPackage, err := promptPackagesInstalled(cmd.Context(), kibanaClient) | ||
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 := promptDashboardIDs(cmd.Context(), kibanaClient) | ||
if err != nil { | ||
return nil, fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||
} | ||
return dashboardIDs, nil | ||
} | ||
Comment on lines
+124
to
+130
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. Used here the same method as in local instances to show all dashboards in the selector. This function uses |
||
|
||
// 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(cmd.Context(), 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) (string, error) { | ||
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("reading package manifest failed (path: %s): %w", packageRoot, err) | ||
} | ||
|
||
installedPackages, err := kibanaClient.FindInstalledPackages(ctx) | ||
if err != nil { | ||
return "", fmt.Errorf("finding installed packages failed: %w", err) | ||
} | ||
|
||
// It always shows an option to export dashboards not related to any package. | ||
options := []string{newDashboardOption} | ||
|
||
options = append(options, installedPackages.Strings()...) | ||
defaultPackage := "" | ||
for _, ip := range installedPackages { | ||
if ip.Name == m.Name { | ||
// set default package to the one matching the package in the current directory | ||
defaultPackage = ip.String() | ||
break | ||
} | ||
} | ||
|
||
packagesPrompt := tui.NewSelect("Which packages would you like to export dashboards from?", options, defaultPackage) | ||
|
||
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 promptDashboardIDs(ctx context.Context, kibanaClient *kibana.Client) ([]string, error) { | ||
savedDashboards, err := kibanaClient.FindDashboards(ctx) | ||
Comment on lines
246
to
247
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.
|
||
if err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,19 @@ func (c *Client) Export(ctx context.Context, dashboardIDs []string) ([]common.Ma | |
return c.exportWithSavedObjectsAPI(ctx, dashboardIDs) | ||
} | ||
|
||
// exportAllDashboards method exports all dashboards using the Kibana APIs without any export details nor including references. | ||
func (c *Client) exportAllDashboards(ctx context.Context) ([]common.MapStr, error) { | ||
logger.Debug("Export dashboards using the Kibana Saved Objects Export API") | ||
|
||
request := ExportSavedObjectsRequest{ | ||
ExcludeExportDetails: true, | ||
IncludeReferencesDeep: false, | ||
Type: "dashboard", | ||
} | ||
|
||
return c.ExportSavedObjects(ctx, request) | ||
} | ||
Comment on lines
+36
to
+46
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. This method would return all the dashboards available in the Kibana Instance. According to the documentation, the number of assets that will be exported depend on
By default that setting is 10000 elements (savedObjects.maxImportExportSize setting):
|
||
|
||
func (c *Client) exportWithSavedObjectsAPI(ctx context.Context, dashboardIDs []string) ([]common.MapStr, error) { | ||
logger.Debug("Export dashboards using the Kibana Saved Objects Export API") | ||
|
||
|
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.
Title for the option to be selected in order to show all the dashboards from the Serverless instance.
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.
This option is shown the first one in the list.
Example: