-
Notifications
You must be signed in to change notification settings - Fork 141
support for 'compilation' of new dashboards-as-code format via elastic-package build
#3531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
2b2b931
ff79937
d656ec3
bd992b6
4771503
ec0cee9
caf0257
29e55a6
a48735e
b7fabed
63f2e3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| // 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 builder | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/Masterminds/semver/v3" | ||
|
|
||
| "github.com/elastic/elastic-package/internal/export" | ||
| "github.com/elastic/elastic-package/internal/kibana" | ||
| "github.com/elastic/elastic-package/internal/logger" | ||
| "github.com/elastic/elastic-package/internal/packages" | ||
| ) | ||
|
|
||
| const dashboardsAsCodeDir = "_dev/build/dashboards_as_code" | ||
|
|
||
| // minDashboardsAsCodeKibanaVersion is the first Kibana version that supports | ||
| // the dashboards-as-code import API (POST /api/dashboards). | ||
| var minDashboardsAsCodeKibanaVersion = semver.MustParse("9.4.0") | ||
|
|
||
| // compileDashboardsAsCode compiles each *.json file under | ||
| // <sourcePackageRoot>/_dev/dashboards_as_code/ into a saved-object dashboard | ||
| // under <sourcePackageRoot>/kibana/dashboard/. Each source file is imported | ||
| // into the connected Kibana via POST /api/dashboards, the resulting dashboard | ||
| // is exported back through the standard dashboards export pipeline, and the | ||
| // imported saved object is then deleted from Kibana. | ||
| // | ||
| // If the source directory does not exist or contains no JSON files, this | ||
| // function is a no-op and no Kibana connection is attempted. When source | ||
| // files are present and kibanaClient is nil, returns an error. | ||
| func compileDashboardsAsCode(ctx context.Context, kibanaClient *kibana.Client, sourcePackageRoot string) error { | ||
| sourceDir := filepath.Join(sourcePackageRoot, dashboardsAsCodeDir) | ||
| files, err := filepath.Glob(filepath.Join(sourceDir, "*.json")) | ||
| if err != nil { | ||
| return fmt.Errorf("listing dashboards-as-code sources failed: %w", err) | ||
| } | ||
| if len(files) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| if kibanaClient == nil { | ||
| return fmt.Errorf("package contains %s but no Kibana client is configured; "+ | ||
| "set ELASTIC_PACKAGE_KIBANA_HOST or run 'elastic-package stack up' first", dashboardsAsCodeDir) | ||
| } | ||
|
|
||
| versionInfo, err := kibanaClient.Version() | ||
| if err != nil { | ||
| return fmt.Errorf("getting Kibana version information: %w", err) | ||
| } | ||
| if err := checkDashboardsAsCodeKibanaVersion(versionInfo); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this not checked by the package validation? when the version of a package is set with its kibana version?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the intention here is just to check if the stack being used to build the package supports the API. it won't check vs the manifest version because existing check/build doesn't require a stack at all. |
||
| return err | ||
| } | ||
|
|
||
| manifest, err := packages.ReadPackageManifestFromPackageRoot(sourcePackageRoot) | ||
| if err != nil { | ||
| return fmt.Errorf("reading package manifest failed (path: %s): %w", sourcePackageRoot, err) | ||
| } | ||
|
|
||
| for _, file := range files { | ||
| if err := compileDashboardAsCodeFile(ctx, kibanaClient, manifest.Name, sourcePackageRoot, file); err != nil { | ||
| return fmt.Errorf("compiling dashboards-as-code file %s: %w", file, err) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func checkDashboardsAsCodeKibanaVersion(info kibana.VersionInfo) error { | ||
| v, err := semver.NewVersion(info.Number) | ||
| if err != nil { | ||
| return fmt.Errorf("cannot parse Kibana version %s: %w", info.Number, err) | ||
| } | ||
| if v.LessThan(minDashboardsAsCodeKibanaVersion) { | ||
| return fmt.Errorf("dashboards-as-code requires Kibana %s or later (got %s); the import API at POST /api/dashboards is not available in this version", | ||
| minDashboardsAsCodeKibanaVersion, info.Number) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func compileDashboardAsCodeFile(ctx context.Context, kibanaClient *kibana.Client, packageName, sourcePackageRoot, file string) error { | ||
| logger.Debugf("Compiling dashboards-as-code file: %s", file) | ||
|
|
||
| body, err := os.ReadFile(file) | ||
| if err != nil { | ||
| return fmt.Errorf("reading dashboards-as-code source failed: %w", err) | ||
| } | ||
|
|
||
| // Use the source filename (without extension) as the saved-object id so the | ||
| // compiled output is deterministic. standardizeObjectID will then prefix it | ||
| // with the package name during export. | ||
| id := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) | ||
| id, err = kibanaClient.ImportDashboardAsCode(ctx, id, body) | ||
| if err != nil { | ||
| return fmt.Errorf("importing dashboards-as-code failed: %w", err) | ||
| } | ||
|
|
||
| // Best-effort cleanup of the imported dashboard, regardless of how the | ||
| // rest of this function completes. Use a fresh context so cleanup runs | ||
| // even if ctx has been cancelled by the time we reach this point. | ||
| defer func() { | ||
| if cleanupErr := kibanaClient.DeleteDashboard(context.Background(), id); cleanupErr != nil { | ||
| if errors.Is(cleanupErr, context.Canceled) { | ||
|
tommyers-elastic marked this conversation as resolved.
Outdated
|
||
| return | ||
| } | ||
| logger.Debugf("Failed to delete imported dashboard %s during cleanup: %v", id, cleanupErr) | ||
| } | ||
| }() | ||
|
|
||
| objects, err := kibanaClient.Export(ctx, []string{id}) | ||
| if err != nil { | ||
| return fmt.Errorf("exporting dashboard %s failed: %w", id, err) | ||
| } | ||
|
|
||
| if err := export.TransformAndWriteDashboards(sourcePackageRoot, packageName, objects); err != nil { | ||
| return fmt.Errorf("writing exported dashboard %s failed: %w", id, err) | ||
| } | ||
| return nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.