internal/kibana/savedobjects.go (206 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 kibana import ( "bytes" "context" "encoding/json" "fmt" "mime/multipart" "net/http" "sort" "strings" "github.com/elastic/elastic-package/internal/logger" ) const findDashboardsPerPage = 100 // DashboardSavedObject corresponds to the Kibana dashboard saved object type DashboardSavedObject struct { ID string Title string } // DashboardSavedObjects is an array of DashboardSavedObject type DashboardSavedObjects []DashboardSavedObject type savedObjectsResponse struct { Total int SavedObjects []savedObjectResponse `json:"saved_objects"` Error string Message string } type savedObjectResponse struct { ID string Attributes struct { Title string } } // Strings method returns string representation for a set of saved objects. func (dsos DashboardSavedObjects) Strings() []string { var entries []string for _, dso := range dsos { entries = append(entries, dso.String()) } return entries } // String method returns a string representation for Kibana dashboard saved object. func (dso *DashboardSavedObject) String() string { return fmt.Sprintf("%s (ID: %s)", dso.Title, dso.ID) } // FindDashboards method returns dashboards available in the Kibana instance. func (c *Client) FindDashboards(ctx context.Context) (DashboardSavedObjects, error) { logger.Debug("Find dashboards using the Saved Objects API") var foundObjects DashboardSavedObjects page := 1 for { r, err := c.findDashboardsNextPage(ctx, page) if err != nil { return nil, fmt.Errorf("can't fetch page with results: %w", err) } if r.Error != "" { return nil, fmt.Errorf("%s: %s", r.Error, r.Message) } for _, savedObject := range r.SavedObjects { foundObjects = append(foundObjects, DashboardSavedObject{ ID: savedObject.ID, Title: savedObject.Attributes.Title, }) } if r.Total <= len(foundObjects) { break } page++ } sort.Slice(foundObjects, func(i, j int) bool { return sort.StringsAreSorted([]string{strings.ToLower(foundObjects[i].Title), strings.ToLower(foundObjects[j].Title)}) }) return foundObjects, nil } func (c *Client) findDashboardsNextPage(ctx context.Context, page int) (*savedObjectsResponse, error) { path := fmt.Sprintf("%s/_find?type=dashboard&fields=title&per_page=%d&page=%d", SavedObjectsAPI, findDashboardsPerPage, page) statusCode, respBody, err := c.get(ctx, path) if err != nil { return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s: %w", statusCode, string(respBody), err) } if statusCode != http.StatusOK { return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s", statusCode, string(respBody)) } var r savedObjectsResponse err = json.Unmarshal(respBody, &r) if err != nil { return nil, fmt.Errorf("unmarshalling response failed: %w", err) } return &r, nil } // SetManagedSavedObject method sets the managed property in a saved object. // For example managed dashboards cannot be edited, and setting managed to false will // allow to edit them. // Managed property cannot be directly changed, so we modify it by exporting the // saved object and importing it again, overwriting the original one. func (c *Client) SetManagedSavedObject(ctx context.Context, savedObjectType string, id string, managed bool) error { exportRequest := ExportSavedObjectsRequest{ ExcludeExportDetails: true, IncludeReferencesDeep: false, Objects: []ExportSavedObjectsRequestObject{ { ID: id, Type: savedObjectType, }, }, } objects, err := c.ExportSavedObjects(ctx, exportRequest) if err != nil { return fmt.Errorf("failed to export %s %s: %w", savedObjectType, id, err) } for _, o := range objects { o["managed"] = managed } importRequest := ImportSavedObjectsRequest{ Overwrite: true, Objects: objects, } _, err = c.ImportSavedObjects(ctx, importRequest) if err != nil { return fmt.Errorf("failed to import %s %s: %w", savedObjectType, id, err) } return nil } type ExportSavedObjectsRequest struct { ExcludeExportDetails bool `json:"excludeExportDetails"` IncludeReferencesDeep bool `json:"includeReferencesDeep"` Objects []ExportSavedObjectsRequestObject `json:"objects"` } type ExportSavedObjectsRequestObject struct { ID string `json:"id"` Type string `json:"type"` } func (c *Client) ExportSavedObjects(ctx context.Context, request ExportSavedObjectsRequest) ([]map[string]any, error) { body, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("failed to encode request: %w", err) } path := SavedObjectsAPI + "/_export" statusCode, respBody, err := c.SendRequest(ctx, http.MethodPost, path, body) if err != nil { return nil, fmt.Errorf("could not export saved objects; API status code = %d; response body = %s: %w", statusCode, string(respBody), err) } if statusCode != http.StatusOK { return nil, fmt.Errorf("could not export saved objects; API status code = %d; response body = %s", statusCode, string(respBody)) } var objects []map[string]any decoder := json.NewDecoder(bytes.NewReader(respBody)) for decoder.More() { var object map[string]any err := decoder.Decode(&object) if err != nil { return nil, fmt.Errorf("unmarshalling response failed (body: \n%s): %w", string(respBody), err) } objects = append(objects, object) } return objects, nil } type ImportSavedObjectsRequest struct { Overwrite bool Objects []map[string]any } type ImportSavedObjectsResponse struct { Success bool `json:"success"` Count int `json:"successCount"` Results []ImportResult `json:"successResults"` Errors []ImportResult `json:"errors"` } type ImportResult struct { ID string `json:"id"` Type string `json:"type"` Title string `json:"title"` Error map[string]any `json:"error"` Meta map[string]any `json:"meta"` } func (c *Client) ImportSavedObjects(ctx context.Context, importRequest ImportSavedObjectsRequest) (*ImportSavedObjectsResponse, error) { var body bytes.Buffer multipartWriter := multipart.NewWriter(&body) fileWriter, err := multipartWriter.CreateFormFile("file", "file.ndjson") if err != nil { return nil, fmt.Errorf("failed to create multipart form file: %w", err) } enc := json.NewEncoder(fileWriter) for _, object := range importRequest.Objects { // Encode includes the newline delimiter. err := enc.Encode(object) if err != nil { return nil, fmt.Errorf("failed to encode object as json: %w", err) } } err = multipartWriter.Close() if err != nil { return nil, fmt.Errorf("failed to finalize multipart message: %w", err) } path := SavedObjectsAPI + "/_import" request, err := c.newRequest(ctx, http.MethodPost, path, &body) if err != nil { return nil, fmt.Errorf("cannot create new request: %w", err) } request.Header.Set("Content-Type", multipartWriter.FormDataContentType()) if importRequest.Overwrite { q := request.URL.Query() q.Set("overwrite", "true") request.URL.RawQuery = q.Encode() } statusCode, respBody, err := c.doRequest(request) if err != nil { return nil, fmt.Errorf("could not import saved objects; API status code = %d; response body = %s: %w", statusCode, string(respBody), err) } if statusCode != http.StatusOK { return nil, fmt.Errorf("could not import saved objects; API status code = %d; response body = %s", statusCode, string(respBody)) } var results ImportSavedObjectsResponse err = json.Unmarshal(respBody, &results) if err != nil { return nil, fmt.Errorf("could not decode response; response body: %s: %w", respBody, err) } return &results, nil }