Skip to content

Commit a0f350f

Browse files
authored
Add method make managed saved objects unmanaged (#1565)
1 parent 4db685f commit a0f350f

File tree

2 files changed

+231
-2
lines changed

2 files changed

+231
-2
lines changed

internal/kibana/saved_objects.go

+147-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package kibana
66

77
import (
8+
"bytes"
89
"encoding/json"
910
"fmt"
11+
"mime/multipart"
1012
"net/http"
1113
"sort"
1214
"strings"
@@ -93,11 +95,11 @@ func (c *Client) findDashboardsNextPage(page int) (*savedObjectsResponse, error)
9395
path := fmt.Sprintf("%s/_find?type=dashboard&fields=title&per_page=%d&page=%d", SavedObjectsAPI, findDashboardsPerPage, page)
9496
statusCode, respBody, err := c.get(path)
9597
if err != nil {
96-
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s: %w", statusCode, respBody, err)
98+
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s: %w", statusCode, string(respBody), err)
9799
}
98100

99101
if statusCode != http.StatusOK {
100-
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s", statusCode, respBody)
102+
return nil, fmt.Errorf("could not find dashboards; API status code = %d; response body = %s", statusCode, string(respBody))
101103
}
102104

103105
var r savedObjectsResponse
@@ -107,3 +109,146 @@ func (c *Client) findDashboardsNextPage(page int) (*savedObjectsResponse, error)
107109
}
108110
return &r, nil
109111
}
112+
113+
// SetManagedSavedObject method sets the managed property in a saved object.
114+
// For example managed dashboards cannot be edited, and setting managed to false will
115+
// allow to edit them.
116+
// Managed property cannot be directly changed, so we modify it by exporting the
117+
// saved object and importing it again, overwriting the original one.
118+
func (c *Client) SetManagedSavedObject(savedObjectType string, id string, managed bool) error {
119+
exportRequest := ExportSavedObjectsRequest{
120+
ExcludeExportDetails: true,
121+
IncludeReferencesDeep: false,
122+
Objects: []ExportSavedObjectsRequestObject{
123+
{
124+
ID: id,
125+
Type: savedObjectType,
126+
},
127+
},
128+
}
129+
objects, err := c.ExportSavedObjects(exportRequest)
130+
if err != nil {
131+
return fmt.Errorf("failed to export %s %s: %w", savedObjectType, id, err)
132+
}
133+
134+
for _, o := range objects {
135+
o["managed"] = managed
136+
}
137+
138+
importRequest := ImportSavedObjectsRequest{
139+
Overwrite: true,
140+
Objects: objects,
141+
}
142+
_, err = c.ImportSavedObjects(importRequest)
143+
if err != nil {
144+
return fmt.Errorf("failed to import %s %s: %w", savedObjectType, id, err)
145+
}
146+
147+
return nil
148+
}
149+
150+
type ExportSavedObjectsRequest struct {
151+
ExcludeExportDetails bool `json:"excludeExportDetails"`
152+
IncludeReferencesDeep bool `json:"includeReferencesDeep"`
153+
Objects []ExportSavedObjectsRequestObject `json:"objects"`
154+
}
155+
156+
type ExportSavedObjectsRequestObject struct {
157+
ID string `json:"id"`
158+
Type string `json:"type"`
159+
}
160+
161+
func (c *Client) ExportSavedObjects(request ExportSavedObjectsRequest) ([]map[string]any, error) {
162+
body, err := json.Marshal(request)
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to encode request: %w", err)
165+
}
166+
167+
path := SavedObjectsAPI + "/_export"
168+
statusCode, respBody, err := c.SendRequest(http.MethodPost, path, body)
169+
if err != nil {
170+
return nil, fmt.Errorf("could not export saved objects; API status code = %d; response body = %s: %w", statusCode, string(respBody), err)
171+
}
172+
if statusCode != http.StatusOK {
173+
return nil, fmt.Errorf("could not export saved objects; API status code = %d; response body = %s", statusCode, string(respBody))
174+
}
175+
176+
var objects []map[string]any
177+
decoder := json.NewDecoder(bytes.NewReader(respBody))
178+
for decoder.More() {
179+
var object map[string]any
180+
err := decoder.Decode(&object)
181+
if err != nil {
182+
return nil, fmt.Errorf("unmarshalling response failed (body: \n%s): %w", string(respBody), err)
183+
}
184+
185+
objects = append(objects, object)
186+
}
187+
188+
return objects, nil
189+
}
190+
191+
type ImportSavedObjectsRequest struct {
192+
Overwrite bool
193+
Objects []map[string]any
194+
}
195+
196+
type ImportSavedObjectsResponse struct {
197+
Success bool `json:"success"`
198+
Count int `json:"successCount"`
199+
Results []ImportResult `json:"successResults"`
200+
Errors []ImportResult `json:"errors"`
201+
}
202+
203+
type ImportResult struct {
204+
ID string `json:"id"`
205+
Type string `json:"type"`
206+
Title string `json:"title"`
207+
Error map[string]any `json:"error"`
208+
Meta map[string]any `json:"meta"`
209+
}
210+
211+
func (c *Client) ImportSavedObjects(importRequest ImportSavedObjectsRequest) (*ImportSavedObjectsResponse, error) {
212+
var body bytes.Buffer
213+
multipartWriter := multipart.NewWriter(&body)
214+
fileWriter, err := multipartWriter.CreateFormFile("file", "file.ndjson")
215+
if err != nil {
216+
return nil, fmt.Errorf("failed to create multipart form file: %w", err)
217+
}
218+
enc := json.NewEncoder(fileWriter)
219+
for _, object := range importRequest.Objects {
220+
// Encode includes the newline delimiter.
221+
err := enc.Encode(object)
222+
if err != nil {
223+
return nil, fmt.Errorf("failed to encode object as json: %w", err)
224+
}
225+
}
226+
multipartWriter.Close()
227+
228+
path := SavedObjectsAPI + "/_import"
229+
request, err := c.newRequest(http.MethodPost, path, &body)
230+
if err != nil {
231+
return nil, fmt.Errorf("cannot create new request: %w", err)
232+
}
233+
request.Header.Set("Content-Type", multipartWriter.FormDataContentType())
234+
if importRequest.Overwrite {
235+
q := request.URL.Query()
236+
q.Set("overwrite", "true")
237+
request.URL.RawQuery = q.Encode()
238+
}
239+
240+
statusCode, respBody, err := c.doRequest(request)
241+
if err != nil {
242+
return nil, fmt.Errorf("could not import saved objects; API status code = %d; response body = %s: %w", statusCode, string(respBody), err)
243+
}
244+
if statusCode != http.StatusOK {
245+
return nil, fmt.Errorf("could not import saved objects; API status code = %d; response body = %s", statusCode, string(respBody))
246+
}
247+
248+
var results ImportSavedObjectsResponse
249+
err = json.Unmarshal(respBody, &results)
250+
if err != nil {
251+
return nil, fmt.Errorf("could not decode response; response body: %s: %w", respBody, err)
252+
}
253+
return &results, nil
254+
}

internal/kibana/saved_objects_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package kibana_test
6+
7+
import (
8+
"errors"
9+
"net/http"
10+
"testing"
11+
12+
"github.com/google/uuid"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/elastic/elastic-package/internal/kibana"
17+
"github.com/elastic/elastic-package/internal/stack"
18+
)
19+
20+
func TestSetManagedSavedObject(t *testing.T) {
21+
// TODO: Use kibana test client when we support recording POST requests.
22+
client, err := stack.NewKibanaClient(kibana.RetryMax(0))
23+
var undefinedEnvError *stack.ErrUndefinedEnv
24+
if errors.As(err, &undefinedEnvError) {
25+
t.Skip("Kibana host required:", err)
26+
}
27+
require.NoError(t, err)
28+
29+
id := preloadDashboard(t, client)
30+
require.True(t, getManagedSavedObject(t, client, "dashboard", id))
31+
32+
err = client.SetManagedSavedObject("dashboard", id, false)
33+
require.NoError(t, err)
34+
assert.False(t, getManagedSavedObject(t, client, "dashboard", id))
35+
}
36+
37+
func preloadDashboard(t *testing.T, client *kibana.Client) string {
38+
id := uuid.New().String()
39+
importRequest := kibana.ImportSavedObjectsRequest{
40+
Overwrite: false, // Highly unlikely, but avoid overwriting existing objects.
41+
Objects: []map[string]any{
42+
{
43+
"attributes": map[string]any{
44+
"title": "Empty Dashboard",
45+
},
46+
"managed": true,
47+
"type": "dashboard",
48+
"id": id,
49+
},
50+
},
51+
}
52+
_, err := client.ImportSavedObjects(importRequest)
53+
require.NoError(t, err)
54+
55+
t.Cleanup(func() {
56+
statusCode, _, err := client.SendRequest(http.MethodDelete, kibana.SavedObjectsAPI+"/dashboard/"+id, nil)
57+
require.NoError(t, err)
58+
require.Equal(t, http.StatusOK, statusCode)
59+
})
60+
61+
return id
62+
}
63+
64+
func getManagedSavedObject(t *testing.T, client *kibana.Client, savedObjectType string, id string) bool {
65+
exportRequest := kibana.ExportSavedObjectsRequest{
66+
ExcludeExportDetails: true,
67+
Objects: []kibana.ExportSavedObjectsRequestObject{
68+
{
69+
ID: id,
70+
Type: "dashboard",
71+
},
72+
},
73+
}
74+
export, err := client.ExportSavedObjects(exportRequest)
75+
require.NoError(t, err)
76+
require.Len(t, export, 1)
77+
78+
managed, found := export[0]["managed"]
79+
if !found {
80+
return false
81+
}
82+
83+
return managed.(bool)
84+
}

0 commit comments

Comments
 (0)