Skip to content

Commit 24ecc28

Browse files
authored
Add Support for ES v8.2.0 dashboard backup and restore (#1459)
* Add elastic dashboard backup and restore support Signed-off-by: Md. Ishtiaq Islam <ishtiaq@appscode.com> * Add os dashboard backup and restore support Signed-off-by: Md. Ishtiaq Islam <ishtiaq@appscode.com> --------- Signed-off-by: Md. Ishtiaq Islam <ishtiaq@appscode.com>
1 parent feb7c4a commit 24ecc28

File tree

700 files changed

+421265
-225
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

700 files changed

+421265
-225
lines changed

go.mod

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,36 @@ require (
1313
k8s.io/apimachinery v0.29.0
1414
k8s.io/client-go v0.29.0
1515
k8s.io/klog/v2 v2.110.1
16-
kmodules.xyz/client-go v0.29.4
16+
kmodules.xyz/client-go v0.29.6
1717
kmodules.xyz/custom-resources v0.29.0
1818
kmodules.xyz/offshoot-api v0.29.0
19-
stash.appscode.dev/apimachinery v0.32.1-0.20240101013736-ef308633d8b2
19+
kubedb.dev/apimachinery v0.41.0-beta.1.0.20240124061503-ce4799bb0e5c
20+
kubedb.dev/db-client-go v0.0.9-0.20240126103627-22edae9f6b92
21+
sigs.k8s.io/controller-runtime v0.16.3
22+
stash.appscode.dev/apimachinery v0.32.1-0.20240118085630-4c06ed8c04a7
2023
)
2124

2225
require (
2326
github.com/Masterminds/semver/v3 v3.2.1 // indirect
2427
github.com/PuerkitoBio/purell v1.2.0 // indirect
2528
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
29+
github.com/beorn7/perks v1.0.1 // indirect
30+
github.com/cert-manager/cert-manager v1.13.3 // indirect
31+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
2632
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
2733
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2834
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
2935
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
3036
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
3137
github.com/fatih/structs v1.1.0 // indirect
32-
github.com/go-logr/logr v1.3.0 // indirect
38+
github.com/fsnotify/fsnotify v1.7.0 // indirect
39+
github.com/go-logr/logr v1.4.1 // indirect
3340
github.com/go-openapi/jsonpointer v0.20.0 // indirect
3441
github.com/go-openapi/jsonreference v0.20.2 // indirect
3542
github.com/go-openapi/swag v0.22.4 // indirect
43+
github.com/go-resty/resty/v2 v2.11.0 // indirect
3644
github.com/gogo/protobuf v1.3.2 // indirect
45+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
3746
github.com/golang/protobuf v1.5.3 // indirect
3847
github.com/google/gnostic-models v0.6.8 // indirect
3948
github.com/google/go-cmp v0.6.0 // indirect
@@ -44,15 +53,18 @@ require (
4453
github.com/josharian/intern v1.0.0 // indirect
4554
github.com/json-iterator/go v1.1.12 // indirect
4655
github.com/mailru/easyjson v0.7.7 // indirect
47-
github.com/mattn/go-isatty v0.0.18 // indirect
56+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
4857
github.com/mitchellh/mapstructure v1.5.0 // indirect
4958
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5059
github.com/modern-go/reflect2 v1.0.2 // indirect
5160
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
5261
github.com/onsi/gomega v1.30.0 // indirect
5362
github.com/pkg/errors v0.9.1 // indirect
54-
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
55-
github.com/rogpeppe/go-internal v1.11.0 // indirect
63+
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.70.0 // indirect
64+
github.com/prometheus/client_golang v1.18.0 // indirect
65+
github.com/prometheus/client_model v0.5.0 // indirect
66+
github.com/prometheus/common v0.45.0 // indirect
67+
github.com/prometheus/procfs v0.12.0 // indirect
5668
github.com/sergi/go-diff v1.2.0 // indirect
5769
github.com/spf13/pflag v1.0.5 // indirect
5870
github.com/yudai/gojsondiff v1.0.0 // indirect
@@ -79,13 +91,15 @@ require (
7991
gopkg.in/yaml.v3 v3.0.1 // indirect
8092
k8s.io/apiextensions-apiserver v0.29.0 // indirect
8193
k8s.io/apiserver v0.29.0 // indirect
94+
k8s.io/component-base v0.29.0 // indirect
8295
k8s.io/kube-aggregator v0.29.0 // indirect
8396
k8s.io/kube-openapi v0.0.0-20231129212854-f0671cc7e66a // indirect
8497
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
8598
kmodules.xyz/apiversion v0.2.0 // indirect
99+
kmodules.xyz/monitoring-agent-api v0.29.0 // indirect
86100
kmodules.xyz/objectstore-api v0.29.0 // indirect
87101
kmodules.xyz/prober v0.29.0 // indirect
88-
sigs.k8s.io/controller-runtime v0.16.3 // indirect
102+
sigs.k8s.io/gateway-api v0.8.0 // indirect
89103
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
90104
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
91105
sigs.k8s.io/yaml v1.4.0 // indirect

go.sum

Lines changed: 51 additions & 15 deletions
Large diffs are not rendered by default.

pkg/backup.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package pkg
1919
import (
2020
"context"
2121
"fmt"
22+
"io"
23+
"net/http"
2224
"os"
2325
"path/filepath"
2426
"time"
@@ -140,6 +142,7 @@ func NewCmdBackup() *cobra.Command {
140142

141143
cmd.Flags().StringVar(&opt.backupOptions.Host, "hostname", opt.backupOptions.Host, "Name of the host machine")
142144
cmd.Flags().StringVar(&opt.interimDataDir, "interim-data-dir", opt.interimDataDir, "Directory where the targeted data will be stored temporarily before uploading to the backend.")
145+
cmd.Flags().BoolVar(&opt.enableDashboard, "enable-dashboard-backup", opt.enableDashboard, "Specify whether to enable kibana dashboard backup")
143146

144147
cmd.Flags().Int64Var(&opt.backupOptions.RetentionPolicy.KeepLast, "retention-keep-last", opt.backupOptions.RetentionPolicy.KeepLast, "Specify value for retention strategy")
145148
cmd.Flags().Int64Var(&opt.backupOptions.RetentionPolicy.KeepHourly, "retention-keep-hourly", opt.backupOptions.RetentionPolicy.KeepHourly, "Specify value for retention strategy")
@@ -242,6 +245,11 @@ func (opt *esOptions) backupElasticsearch(targetRef api_v1beta1.TargetRef) (*res
242245
if err := opt.dumpPVCStorageLimit(targetRef); err != nil {
243246
return nil, fmt.Errorf("failed to dump pvc storage limit info %w", err)
244247
}
248+
249+
if err = opt.dumpDashboardObjects(appBinding); err != nil {
250+
return nil, fmt.Errorf("failed to dump dashboard objects %w", err)
251+
}
252+
245253
// dumped data has been stored in the interim data dir. Now, we will backup this directory using Stash.
246254
opt.backupOptions.BackupPaths = []string{opt.interimDataDir}
247255

@@ -288,3 +296,30 @@ func (opt *esOptions) dumpPVCStorageLimit(targetRef api_v1beta1.TargetRef) error
288296
func targetMatched(tref api_v1beta1.TargetRef, expectedKind, expectedName, expectedNamespace string) bool {
289297
return tref.Kind == expectedKind && tref.Namespace == expectedNamespace && tref.Name == expectedName
290298
}
299+
300+
func (opt *esOptions) dumpDashboardObjects(appBinding *appcatalog.AppBinding) error {
301+
if !opt.enableDashboard {
302+
return nil
303+
}
304+
305+
dashboardClient, err := opt.getDashboardClient(appBinding)
306+
if err != nil {
307+
return err
308+
}
309+
310+
response, err := dashboardClient.ExportSavedObjects()
311+
if err != nil {
312+
return err
313+
}
314+
315+
body, err := io.ReadAll(response.Body)
316+
if err != nil {
317+
return err
318+
}
319+
320+
if response.Code != http.StatusOK {
321+
return fmt.Errorf("failed to export dashboard saved objects %s", string(body))
322+
}
323+
324+
return os.WriteFile(filepath.Join(opt.interimDataDir, DashboardObjectsFile), body, os.ModePerm)
325+
}

pkg/restore.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package pkg
1919
import (
2020
"context"
2121
"fmt"
22+
"io"
23+
"net/http"
2224
"os"
2325
"path/filepath"
2426

@@ -131,6 +133,7 @@ func NewCmdRestore() *cobra.Command {
131133
cmd.Flags().StringVar(&opt.restoreOptions.SourceHost, "source-hostname", opt.restoreOptions.SourceHost, "Name of the host whose data will be restored")
132134
cmd.Flags().StringSliceVar(&opt.restoreOptions.Snapshots, "snapshot", opt.restoreOptions.Snapshots, "Snapshots to restore")
133135
cmd.Flags().StringVar(&opt.interimDataDir, "interim-data-dir", opt.interimDataDir, "Directory where the restored data will be stored temporarily before injecting into the desired database.")
136+
cmd.Flags().BoolVar(&opt.enableDashboard, "enable-dashboard-restore", opt.enableDashboard, "Specify whether to enable kibana dashboard restore")
134137

135138
cmd.Flags().StringVar(&opt.outputDir, "output-dir", opt.outputDir, "Directory where output.json file will be written (keep empty if you don't need to write output in file)")
136139

@@ -218,6 +221,15 @@ func (opt *esOptions) restoreElasticsearch(targetRef api_v1beta1.TargetRef) (*re
218221
return nil, err
219222
}
220223

224+
if err = opt.restoreDashboardObjects(appBinding); err != nil {
225+
return nil, fmt.Errorf("failed to restore dashboard objects %w", err)
226+
}
227+
228+
// delete the metadata file as it is not required for restoring the dumps
229+
if err := clearFile(filepath.Join(opt.interimDataDir, DashboardObjectsFile)); err != nil {
230+
return nil, err
231+
}
232+
221233
// run separate shell to restore indices
222234
// klog.Infoln("Performing multielasticdump on", hostname)
223235
session.sh.ShowCMD = false
@@ -238,3 +250,30 @@ func clearFile(filepath string) error {
238250
}
239251
return nil
240252
}
253+
254+
func (opt *esOptions) restoreDashboardObjects(appBinding *appcatalog.AppBinding) error {
255+
if !opt.enableDashboard {
256+
return nil
257+
}
258+
259+
dashboardClient, err := opt.getDashboardClient(appBinding)
260+
if err != nil {
261+
return err
262+
}
263+
264+
response, err := dashboardClient.ImportSavedObjects(filepath.Join(opt.interimDataDir, DashboardObjectsFile))
265+
if err != nil {
266+
return err
267+
}
268+
269+
body, err := io.ReadAll(response.Body)
270+
if err != nil {
271+
return err
272+
}
273+
274+
if response.Code != http.StatusOK {
275+
return fmt.Errorf("failed to import dashboard saved objects %s", string(body))
276+
}
277+
278+
return nil
279+
}

pkg/utils.go

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,31 @@ import (
3131
shell "gomodules.xyz/go-sh"
3232
core "k8s.io/api/core/v1"
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/runtime"
35+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3436
"k8s.io/client-go/kubernetes"
37+
"k8s.io/client-go/rest"
3538
restclient "k8s.io/client-go/rest"
3639
"k8s.io/klog/v2"
3740
kmapi "kmodules.xyz/client-go/api/v1"
3841
meta_util "kmodules.xyz/client-go/meta"
3942
appcatalog "kmodules.xyz/custom-resources/apis/appcatalog/v1alpha1"
4043
appcatalog_cs "kmodules.xyz/custom-resources/client/clientset/versioned"
44+
catalog "kubedb.dev/apimachinery/apis/catalog/v1alpha1"
45+
esapi "kubedb.dev/apimachinery/apis/elasticsearch/v1alpha1"
46+
kubedbapi "kubedb.dev/apimachinery/apis/kubedb/v1alpha2"
47+
es_dashboard "kubedb.dev/db-client-go/elasticsearchdashboard"
48+
"sigs.k8s.io/controller-runtime/pkg/client"
49+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
4150
)
4251

4352
const (
44-
ESUser = "ADMIN_USERNAME"
45-
ESPassword = "ADMIN_PASSWORD"
46-
MultiElasticDumpCMD = "multielasticdump"
47-
ESCACertFile = "root.pem"
48-
ESAuthFile = "auth.txt"
53+
ESUser = "ADMIN_USERNAME"
54+
ESPassword = "ADMIN_PASSWORD"
55+
MultiElasticDumpCMD = "multielasticdump"
56+
ESCACertFile = "root.pem"
57+
ESAuthFile = "auth.txt"
58+
DashboardObjectsFile = "dashboard.ndjson"
4959
)
5060

5161
type esOptions struct {
@@ -59,6 +69,7 @@ type esOptions struct {
5969
appBindingNamespace string
6070
esArgs string
6171
interimDataDir string
72+
enableDashboard bool
6273
outputDir string
6374
storageSecret kmapi.ObjectReference
6475
waitTimeout int32
@@ -166,3 +177,119 @@ func writeAuthFile(filename string, cred *core.Secret) error {
166177
)
167178
return os.WriteFile(filename, []byte(authKeys), 0o400) // only readable to owner
168179
}
180+
181+
func newRuntimeClient(cfg *rest.Config) (client.Client, error) {
182+
scheme := runtime.NewScheme()
183+
utilruntime.Must(core.AddToScheme(scheme))
184+
utilruntime.Must(esapi.AddToScheme(scheme))
185+
utilruntime.Must(kubedbapi.AddToScheme(scheme))
186+
187+
hc, err := rest.HTTPClientFor(cfg)
188+
if err != nil {
189+
return nil, err
190+
}
191+
mapper, err := apiutil.NewDynamicRESTMapper(cfg, hc)
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
return client.New(cfg, client.Options{
197+
Scheme: scheme,
198+
Mapper: mapper,
199+
})
200+
}
201+
202+
func getElasticSearchDashboard(klient client.Client, appBinding *appcatalog.AppBinding) (*esapi.ElasticsearchDashboard, error) {
203+
dashboards := &esapi.ElasticsearchDashboardList{}
204+
opts := []client.ListOption{client.InNamespace(appBinding.Namespace)}
205+
if err := klient.List(context.TODO(), dashboards, opts...); err != nil {
206+
return nil, err
207+
}
208+
209+
for _, dashboard := range dashboards.Items {
210+
if dashboard.Spec.DatabaseRef != nil {
211+
if dashboard.Spec.DatabaseRef.Name == appBinding.Name {
212+
return &dashboard, nil
213+
}
214+
}
215+
}
216+
217+
return nil, fmt.Errorf("no elasticsearch dashboard found")
218+
}
219+
220+
func getElasticSearch(klient client.Client, appBinding *appcatalog.AppBinding) (*kubedbapi.Elasticsearch, error) {
221+
es := &kubedbapi.Elasticsearch{
222+
ObjectMeta: metav1.ObjectMeta{
223+
Name: appBinding.Name,
224+
Namespace: appBinding.Namespace,
225+
},
226+
}
227+
228+
if err := klient.Get(context.TODO(), client.ObjectKeyFromObject(es), es); err != nil {
229+
return nil, err
230+
}
231+
232+
return es, nil
233+
}
234+
235+
func getSecret(klient client.Client, obj kmapi.ObjectReference) (*core.Secret, error) {
236+
sec := &core.Secret{
237+
ObjectMeta: metav1.ObjectMeta{
238+
Name: obj.Name,
239+
Namespace: obj.Namespace,
240+
},
241+
}
242+
243+
if err := klient.Get(context.TODO(), client.ObjectKeyFromObject(sec), sec); err != nil {
244+
return nil, err
245+
}
246+
247+
return sec, nil
248+
}
249+
250+
func getVersionInfo(es *kubedbapi.Elasticsearch, appBinding *appcatalog.AppBinding) *es_dashboard.DbVersionInfo {
251+
authPlugin := catalog.ElasticsearchAuthPluginOpenSearch
252+
segments := strings.Split(es.Spec.Version, "-")
253+
if segments[0] == "xpack" {
254+
authPlugin = catalog.ElasticsearchAuthPluginXpack
255+
}
256+
return &es_dashboard.DbVersionInfo{
257+
Name: es.Spec.Version,
258+
Version: appBinding.Spec.Version,
259+
AuthPlugin: authPlugin,
260+
}
261+
}
262+
263+
func (opt esOptions) getDashboardClient(appBinding *appcatalog.AppBinding) (*es_dashboard.Client, error) {
264+
klient, err := newRuntimeClient(opt.config)
265+
if err != nil {
266+
return nil, err
267+
}
268+
269+
esDashboard, err := getElasticSearchDashboard(klient, appBinding)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
es, err := getElasticSearch(klient, appBinding)
275+
if err != nil {
276+
return nil, err
277+
}
278+
279+
sec, err := getSecret(klient, kmapi.ObjectReference{
280+
Name: es.Spec.AuthSecret.Name,
281+
Namespace: es.Namespace,
282+
})
283+
if err != nil {
284+
return nil, err
285+
}
286+
287+
versionInfo := getVersionInfo(es, appBinding)
288+
289+
return es_dashboard.NewKubeDBClientBuilder(klient, esDashboard).
290+
WithContext(context.TODO()).
291+
WithDatabaseRef(es).
292+
WithAuthSecret(sec).
293+
WithDbVersionInfo(versionInfo).
294+
GetElasticsearchDashboardClient()
295+
}

vendor/github.com/beorn7/perks/LICENSE

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)