cmd/edit.go (126 lines of code) (raw):
// 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(cmd.Context(), 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(cmd.Context(), "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")))
} 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)
}
fmt.Println("")
return fmt.Errorf("failed to make one or more dashboards editable: %s", combinedErr.Error())
}
fmt.Println("\nDone")
return nil
}
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)
}
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
}