-
Notifications
You must be signed in to change notification settings - Fork 129
Add edit dashboards command #1573
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
Changes from all commits
ca210ab
b655a3d
53ae2c2
922be01
12bb1fd
f4c656b
d2daf9e
5321b60
7b37e97
fb5feff
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 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,155 @@ | ||||||||||||||
| // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||||||||||||||
| // or more contributor license agreements. Licensed under the Elastic License; | ||||||||||||||
| // you may not use this file except in compliance with the Elastic License. | ||||||||||||||
|
|
||||||||||||||
| package cmd | ||||||||||||||
|
|
||||||||||||||
| import ( | ||||||||||||||
| "errors" | ||||||||||||||
| "fmt" | ||||||||||||||
| "net/url" | ||||||||||||||
| "strings" | ||||||||||||||
|
|
||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||
|
|
||||||||||||||
| "github.com/elastic/elastic-package/internal/cobraext" | ||||||||||||||
| "github.com/elastic/elastic-package/internal/common" | ||||||||||||||
| "github.com/elastic/elastic-package/internal/install" | ||||||||||||||
| "github.com/elastic/elastic-package/internal/kibana" | ||||||||||||||
| "github.com/elastic/elastic-package/internal/stack" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| const editLongDescription = `Use this command to edit assets relevant for the package, e.g. Kibana dashboards.` | ||||||||||||||
|
|
||||||||||||||
| const editDashboardsLongDescription = `Use this command to make dashboards editable. | ||||||||||||||
|
|
||||||||||||||
| Pass a comma-separated list of dashboard ids with -d or use the interactive prompt to make managed dashboards editable in Kibana.` | ||||||||||||||
|
|
||||||||||||||
| func setupEditCommand() *cobraext.Command { | ||||||||||||||
| editDashboardsCmd := &cobra.Command{ | ||||||||||||||
| Use: "dashboards", | ||||||||||||||
| Short: "Make dashboards editable in Kibana", | ||||||||||||||
| Long: editDashboardsLongDescription, | ||||||||||||||
| Args: cobra.NoArgs, | ||||||||||||||
| RunE: editDashboardsCmd, | ||||||||||||||
| } | ||||||||||||||
| editDashboardsCmd.Flags().StringSliceP(cobraext.DashboardIDsFlagName, "d", nil, cobraext.DashboardIDsFlagDescription) | ||||||||||||||
| editDashboardsCmd.Flags().Bool(cobraext.TLSSkipVerifyFlagName, false, cobraext.TLSSkipVerifyFlagDescription) | ||||||||||||||
| editDashboardsCmd.Flags().Bool(cobraext.AllowSnapshotFlagName, false, cobraext.AllowSnapshotDescription) | ||||||||||||||
|
|
||||||||||||||
| cmd := &cobra.Command{ | ||||||||||||||
| Use: "edit", | ||||||||||||||
| Short: "Edit package assets", | ||||||||||||||
| Long: editLongDescription, | ||||||||||||||
| } | ||||||||||||||
| cmd.AddCommand(editDashboardsCmd) | ||||||||||||||
| cmd.PersistentFlags().StringP(cobraext.ProfileFlagName, "p", "", fmt.Sprintf(cobraext.ProfileFlagDescription, install.ProfileNameEnvVar)) | ||||||||||||||
|
|
||||||||||||||
| return cobraext.NewCommand(cmd, cobraext.ContextPackage) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func editDashboardsCmd(cmd *cobra.Command, args []string) error { | ||||||||||||||
| cmd.Println("Make Kibana dashboards editable") | ||||||||||||||
|
|
||||||||||||||
| dashboardIDs, err := cmd.Flags().GetStringSlice(cobraext.DashboardIDsFlagName) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return cobraext.FlagParsingError(err, cobraext.DashboardIDsFlagName) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| common.TrimStringSlice(dashboardIDs) | ||||||||||||||
|
|
||||||||||||||
| var opts []kibana.ClientOption | ||||||||||||||
| tlsSkipVerify, _ := cmd.Flags().GetBool(cobraext.TLSSkipVerifyFlagName) | ||||||||||||||
| if tlsSkipVerify { | ||||||||||||||
| opts = append(opts, kibana.TLSSkipVerify()) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| allowSnapshot, _ := cmd.Flags().GetBool(cobraext.AllowSnapshotFlagName) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return cobraext.FlagParsingError(err, cobraext.AllowSnapshotFlagName) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| profile, err := cobraext.GetProfileFlag(cmd) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return err | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| kibanaClient, err := stack.NewKibanaClientFromProfile(profile, opts...) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return fmt.Errorf("can't create Kibana client: %w", err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| kibanaVersion, err := kibanaClient.Version() | ||||||||||||||
| if err != nil { | ||||||||||||||
| return fmt.Errorf("can't get Kibana status information: %w", err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if kibanaVersion.IsSnapshot() { | ||||||||||||||
| message := fmt.Sprintf("editing dashboards from a SNAPSHOT version of Kibana (%s) is discouraged. It could lead to invalid dashboards (for example if they use features that are reverted or modified before the final release)", kibanaVersion.Version()) | ||||||||||||||
| if !allowSnapshot { | ||||||||||||||
| return fmt.Errorf("%s. --%s flag can be used to ignore this error", message, cobraext.AllowSnapshotFlagName) | ||||||||||||||
| } | ||||||||||||||
| fmt.Printf("Warning: %s\n", message) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if len(dashboardIDs) == 0 { | ||||||||||||||
| dashboardIDs, err = promptDashboardIDs(kibanaClient) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return fmt.Errorf("prompt for dashboard selection failed: %w", err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if len(dashboardIDs) == 0 { | ||||||||||||||
| fmt.Println("No dashboards were found in Kibana.") | ||||||||||||||
| return nil | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| updatedDashboardIDs := make([]string, 0, len(dashboardIDs)) | ||||||||||||||
| failedDashboardUpdates := make(map[string]error, len(dashboardIDs)) | ||||||||||||||
| for _, dashboardID := range dashboardIDs { | ||||||||||||||
| err = kibanaClient.SetManagedSavedObject("dashboard", dashboardID, false) | ||||||||||||||
| if err != nil { | ||||||||||||||
| failedDashboardUpdates[dashboardID] = err | ||||||||||||||
| } else { | ||||||||||||||
| updatedDashboardIDs = append(updatedDashboardIDs, dashboardID) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if len(updatedDashboardIDs) > 0 { | ||||||||||||||
| urls, err := dashboardURLs(*kibanaClient, updatedDashboardIDs) | ||||||||||||||
| if err != nil { | ||||||||||||||
| cmd.Println(fmt.Sprintf("\nFailed to retrieve dashboard URLS: %s", err.Error())) | ||||||||||||||
| cmd.Println(fmt.Sprintf("The following dashboards are now editable in Kibana:\n%s", strings.Join(updatedDashboardIDs, "\n"))) | ||||||||||||||
|
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 will probably never happen, because we were using the URL before. But good to handle the error just in case 👍 |
||||||||||||||
| } else { | ||||||||||||||
| cmd.Println(fmt.Sprintf("\nThe following dashboards are now editable in Kibana:%s\n\nRemember to export modified dashboards with elastic-package export dashboards", urls)) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if len(failedDashboardUpdates) > 0 { | ||||||||||||||
| var combinedErr error | ||||||||||||||
| for _, err := range failedDashboardUpdates { | ||||||||||||||
| combinedErr = errors.Join(combinedErr, err) | ||||||||||||||
| } | ||||||||||||||
|
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. I wonder if |
||||||||||||||
| fmt.Println("") | ||||||||||||||
| return fmt.Errorf("failed to make one or more dashboards editable: %s", combinedErr.Error()) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| fmt.Println("\nDone") | ||||||||||||||
| return nil | ||||||||||||||
jsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func dashboardURLs(kibanaClient kibana.Client, dashboardIDs []string) (string, error) { | ||||||||||||||
| kibanaHost := kibanaClient.Address() | ||||||||||||||
| kibanaURL, err := url.Parse(kibanaHost) | ||||||||||||||
| if err != nil { | ||||||||||||||
| return "", fmt.Errorf("failed to retrieve Kibana URL: %w", err) | ||||||||||||||
| } | ||||||||||||||
|
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. I am getting this output: I think we are missing to add the
Suggested change
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. Yep, sorry, just realised that too, pushed a fix. |
||||||||||||||
| var urls strings.Builder | ||||||||||||||
| for _, dashboardID := range dashboardIDs { | ||||||||||||||
| dashboardURL := *kibanaURL | ||||||||||||||
| dashboardURL.Path = "app/dashboards" | ||||||||||||||
| dashboardURL.Fragment = "/view/" + dashboardID | ||||||||||||||
| fmt.Fprintf(&urls, "\n%s", dashboardURL.String()) | ||||||||||||||
| } | ||||||||||||||
| return urls.String(), nil | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # HOWTO: Make dashboards editable in Kibana | ||
|
|
||
| ## Introduction | ||
|
|
||
| As of 8.11, managed assets, including dashboards, are read-only in Kibana. This change was introduced to prevent users from losing changes on package upgrades. Integrations authors, however, need the ability to edit assets in order to adopt new features. | ||
|
|
||
| ## Making a dashboard editable | ||
|
|
||
| Dashboards can be made editable in Kibana by using the `elastic-package edit dashboards` command. This command can either be run interactively, allowing manual selection of dashboards, or be passed a comma-separated list of dashboard ids. | ||
|
|
||
| NB: after modifying dashboards, these need to be exported using `elastic-package export dashboards`. | ||
|
|
||
| ### Using the interactive dashboard selection prompt | ||
|
|
||
| Run the following command: | ||
| ``` | ||
| elastic-package edit dashboards | ||
| ``` | ||
|
|
||
| Use the interactive dashboard selection prompt to select the dashboard(s) that should be made editable. | ||
|
|
||
| ### Using a comma-separated list of dashboard ids | ||
|
|
||
| Pass the list with the `-d` flag: | ||
| ``` | ||
| elastic-package edit dashboards -d 123,456,789 | ||
| ``` | ||
|
|
||
| Each dashboard id will be processed and the outcome of the updates will be listed in the command's final output. | ||
|
|
||
| ### Command output | ||
|
|
||
| The final output will provide the outcome (success or failure) of the update for each dashboard. | ||
|
|
||
| For example, assuming the following command: | ||
| ``` | ||
| elastic-package edit dashboards -d 123,456,789 | ||
| ``` | ||
|
|
||
| #### Success | ||
|
|
||
| Assuming '123', '456' and '789' are valid dashboard ids and all three updates succeed, the output will be successful and report the URLs of the updated dashboards: | ||
| ``` | ||
| Make Kibana dashboards editable | ||
|
|
||
| The following dashboards are now editable in Kibana: | ||
| https://<kibanaURL>/app/dashboards#/view/123 | ||
| https://<kibanaURL>/app/dashboards#/view/456 | ||
| https://<kibanaURL>/app/dashboards#/view/789 | ||
|
|
||
| Done | ||
| ``` | ||
|
|
||
| #### Partial failure | ||
|
|
||
| Assuming that `456` is an invalid dashboard id and that the update is successful for ids `123` and `789`, the output will report the URLs of the updated dashboards as well as an error listing the failures: | ||
| ``` | ||
| Make Kibana dashboards editable | ||
|
|
||
| The following dashboards are now editable in Kibana: | ||
| https://<kibanaURL>/app/dashboards#/view/123 | ||
| https://<kibanaURL>/app/dashboards#/view/789 | ||
|
|
||
| Error: failed to make one or more dashboards editable: failed to export dashboard 456: could not export saved objects; API status code = 400; response body = {"statusCode":400,"error":"Bad Request","message":"Error fetching objects to export","attributes":{"objects":[{"id":"456","type":"dashboard","error":{"statusCode":404,"error":"Not Found","message":"Saved object [dashboard/456] not found"}}]}} | ||
| ``` | ||
|
|
||
|
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. Maybe we have to remember that after modifying the dashboard, it should be exported. 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. Do you mean that this error message could be confusing to the user? 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. Ah no, the error is fine, probably I put my comment in the wrong place. |
||
| ### Optional flags | ||
|
|
||
| * `allow-snapshot`: to allow exporting dashboards from a Elastic stack SNAPSHOT version | ||
| * `tls-skip-verify`: to skip TLS verify | ||
Uh oh!
There was an error while loading. Please reload this page.