Skip to content

Commit

Permalink
Add method make managed saved objects unmanaged (#1565)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsoriano authored Nov 27, 2023
1 parent 4db685f commit a0f350f
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 2 deletions.
149 changes: 147 additions & 2 deletions internal/kibana/saved_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
package kibana

import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"sort"
"strings"
Expand Down Expand Up @@ -93,11 +95,11 @@ func (c *Client) findDashboardsNextPage(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(path)
if err != nil {
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s: %w", statusCode, respBody, err)
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, respBody)
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s", statusCode, string(respBody))
}

var r savedObjectsResponse
Expand All @@ -107,3 +109,146 @@ func (c *Client) findDashboardsNextPage(page int) (*savedObjectsResponse, error)
}
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(savedObjectType string, id string, managed bool) error {
exportRequest := ExportSavedObjectsRequest{
ExcludeExportDetails: true,
IncludeReferencesDeep: false,
Objects: []ExportSavedObjectsRequestObject{
{
ID: id,
Type: savedObjectType,
},
},
}
objects, err := c.ExportSavedObjects(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(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(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(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(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)
}
}
multipartWriter.Close()

path := SavedObjectsAPI + "/_import"
request, err := c.newRequest(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
}
84 changes: 84 additions & 0 deletions internal/kibana/saved_objects_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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_test

import (
"errors"
"net/http"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/elastic-package/internal/kibana"
"github.com/elastic/elastic-package/internal/stack"
)

func TestSetManagedSavedObject(t *testing.T) {
// TODO: Use kibana test client when we support recording POST requests.
client, err := stack.NewKibanaClient(kibana.RetryMax(0))
var undefinedEnvError *stack.ErrUndefinedEnv
if errors.As(err, &undefinedEnvError) {
t.Skip("Kibana host required:", err)
}
require.NoError(t, err)

id := preloadDashboard(t, client)
require.True(t, getManagedSavedObject(t, client, "dashboard", id))

err = client.SetManagedSavedObject("dashboard", id, false)
require.NoError(t, err)
assert.False(t, getManagedSavedObject(t, client, "dashboard", id))
}

func preloadDashboard(t *testing.T, client *kibana.Client) string {
id := uuid.New().String()
importRequest := kibana.ImportSavedObjectsRequest{
Overwrite: false, // Highly unlikely, but avoid overwriting existing objects.
Objects: []map[string]any{
{
"attributes": map[string]any{
"title": "Empty Dashboard",
},
"managed": true,
"type": "dashboard",
"id": id,
},
},
}
_, err := client.ImportSavedObjects(importRequest)
require.NoError(t, err)

t.Cleanup(func() {
statusCode, _, err := client.SendRequest(http.MethodDelete, kibana.SavedObjectsAPI+"/dashboard/"+id, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)
})

return id
}

func getManagedSavedObject(t *testing.T, client *kibana.Client, savedObjectType string, id string) bool {
exportRequest := kibana.ExportSavedObjectsRequest{
ExcludeExportDetails: true,
Objects: []kibana.ExportSavedObjectsRequestObject{
{
ID: id,
Type: "dashboard",
},
},
}
export, err := client.ExportSavedObjects(exportRequest)
require.NoError(t, err)
require.Len(t, export, 1)

managed, found := export[0]["managed"]
if !found {
return false
}

return managed.(bool)
}

0 comments on commit a0f350f

Please sign in to comment.