360 lines
8.6 KiB
Go
360 lines
8.6 KiB
Go
// Licensed to Elasticsearch B.V. under one or more contributor
|
|
// license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright
|
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
|
// the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package dashboards
|
|
|
|
import (
|
|
"archive/zip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
errw "github.com/pkg/errors"
|
|
|
|
"github.com/elastic/beats/libbeat/common"
|
|
)
|
|
|
|
// ErrNotFound returned when we cannot find any dashboard to import.
|
|
type ErrNotFound struct {
|
|
ErrorString string
|
|
}
|
|
|
|
// Error returns the human readable error.
|
|
func (e *ErrNotFound) Error() string { return e.ErrorString }
|
|
|
|
func newErrNotFound(s string, a ...interface{}) *ErrNotFound {
|
|
return &ErrNotFound{fmt.Sprintf(s, a...)}
|
|
}
|
|
|
|
// MessageOutputter is a function type for injecting status logging
|
|
// into this module.
|
|
type MessageOutputter func(msg string, a ...interface{})
|
|
|
|
type Importer struct {
|
|
cfg *Config
|
|
version common.Version
|
|
|
|
loader Loader
|
|
}
|
|
|
|
type Loader interface {
|
|
ImportIndex(file string) error
|
|
ImportDashboard(file string) error
|
|
statusMsg(msg string, a ...interface{})
|
|
Close() error
|
|
}
|
|
|
|
func NewImporter(version common.Version, cfg *Config, loader Loader) (*Importer, error) {
|
|
|
|
// Current max version is 6
|
|
if version.Major > 6 {
|
|
version.Major = 6
|
|
}
|
|
|
|
return &Importer{
|
|
cfg: cfg,
|
|
version: version,
|
|
loader: loader,
|
|
}, nil
|
|
}
|
|
|
|
// Import imports the Kibana dashboards according to the configuration options.
|
|
func (imp Importer) Import() error {
|
|
if imp.cfg.URL != "" || imp.cfg.File != "" {
|
|
err := imp.ImportArchive()
|
|
if err != nil {
|
|
return errw.Wrap(err, "Error importing URL/file")
|
|
}
|
|
} else {
|
|
err := imp.ImportKibanaDir(imp.cfg.Dir)
|
|
if err != nil {
|
|
return errw.Wrapf(err, "Error importing directory %s", imp.cfg.Dir)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (imp Importer) ImportDashboard(file string) error {
|
|
imp.loader.statusMsg("Import dashboard %s", file)
|
|
|
|
return imp.loader.ImportDashboard(file)
|
|
}
|
|
|
|
func (imp Importer) ImportFile(fileType string, file string) error {
|
|
imp.loader.statusMsg("Import %s from %s", fileType, file)
|
|
|
|
if fileType == "dashboard" {
|
|
return imp.loader.ImportDashboard(file)
|
|
} else if fileType == "index-pattern" {
|
|
return imp.loader.ImportIndex(file)
|
|
}
|
|
return fmt.Errorf("Unexpected file type %s", fileType)
|
|
}
|
|
|
|
func (imp Importer) ImportDir(dirType string, dir string) error {
|
|
imp.loader.statusMsg("Import directory %s", dir)
|
|
|
|
dir = path.Join(dir, dirType)
|
|
var errors []string
|
|
|
|
files, err := filepath.Glob(path.Join(dir, "*.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read directory %s. Error: %s", dir, err)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return fmt.Errorf("The directory %s is empty, nothing to import", dir)
|
|
}
|
|
for _, file := range files {
|
|
err = imp.ImportFile(dirType, file)
|
|
if err != nil {
|
|
errors = append(errors, fmt.Sprintf(" error loading %s: %s", file, err))
|
|
}
|
|
}
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("Failed to load directory %s:\n%s", dir, strings.Join(errors, "\n"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (imp Importer) unzip(archive, target string) error {
|
|
imp.loader.statusMsg("Unzip archive %s", target)
|
|
|
|
reader, err := zip.OpenReader(archive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Closure to close the files on each iteration
|
|
unzipFile := func(file *zip.File) error {
|
|
filePath := filepath.Join(target, file.Name)
|
|
|
|
// check that the resulting file path is indeed under target
|
|
// Note that Rel calls Clean.
|
|
relPath, err := filepath.Rel(target, filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(filepath.ToSlash(relPath), "../") {
|
|
return fmt.Errorf("Zip file contains files outside of the target directory: %s", relPath)
|
|
}
|
|
|
|
if file.FileInfo().IsDir() {
|
|
return os.MkdirAll(filePath, file.Mode())
|
|
}
|
|
|
|
if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
|
return fmt.Errorf("failed making directory for file %v: %v", filePath, err)
|
|
}
|
|
|
|
fileReader, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
targetFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer targetFile.Close()
|
|
|
|
if _, err := io.Copy(targetFile, fileReader); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, file := range reader.File {
|
|
err := unzipFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (imp Importer) ImportArchive() error {
|
|
var archive string
|
|
|
|
target, err := ioutil.TempDir("", "tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to generate a temporary directory name: %v", err)
|
|
}
|
|
|
|
if err = os.MkdirAll(target, 0755); err != nil {
|
|
return fmt.Errorf("Failed to create a temporary directory %s: %v", target, err)
|
|
}
|
|
|
|
defer os.RemoveAll(target) // clean up
|
|
|
|
imp.loader.statusMsg("Created temporary directory %s", target)
|
|
if imp.cfg.File != "" {
|
|
archive = imp.cfg.File
|
|
} else if imp.cfg.URL != "" {
|
|
archive, err = imp.downloadFile(imp.cfg.URL, target)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to download file: %s. Error: %v", imp.cfg.URL, err)
|
|
}
|
|
} else {
|
|
return errors.New("No archive file or URL is set - please use -file or -url option")
|
|
}
|
|
|
|
err = imp.unzip(archive, target)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to unzip the archive: %s: %v", archive, err)
|
|
}
|
|
dirs, err := getDirectories(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(dirs) != 1 {
|
|
return fmt.Errorf("Too many directories under %s", target)
|
|
}
|
|
|
|
dirs, err = getDirectories(dirs[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
imp.loader.statusMsg("Importing Kibana from %s", dir)
|
|
if imp.cfg.Beat == "" || filepath.Base(dir) == imp.cfg.Beat {
|
|
err = imp.ImportKibanaDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getDirectories(target string) ([]string, error) {
|
|
files, err := ioutil.ReadDir(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var dirs []string
|
|
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
dirs = append(dirs, filepath.Join(target, file.Name()))
|
|
}
|
|
}
|
|
return dirs, nil
|
|
}
|
|
|
|
func (imp Importer) downloadFile(url string, target string) (string, error) {
|
|
fileName := filepath.Base(url)
|
|
targetPath := path.Join(target, fileName)
|
|
imp.loader.statusMsg("Downloading %s", url)
|
|
|
|
// Create the file
|
|
out, err := os.Create(targetPath)
|
|
if err != nil {
|
|
return targetPath, err
|
|
}
|
|
defer out.Close()
|
|
|
|
// Get the data
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return targetPath, err
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return targetPath, fmt.Errorf("Server returned: %s", resp.Status)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Writer the body to file
|
|
_, err = io.Copy(out, resp.Body)
|
|
if err != nil {
|
|
return targetPath, err
|
|
}
|
|
|
|
return targetPath, nil
|
|
}
|
|
|
|
// import Kibana dashboards and index-pattern or only one of these
|
|
func (imp Importer) ImportKibanaDir(dir string) error {
|
|
var err error
|
|
|
|
versionPath := strconv.Itoa(imp.version.Major)
|
|
|
|
dir = path.Join(dir, versionPath)
|
|
|
|
imp.loader.statusMsg("Importing directory %v", dir)
|
|
|
|
if _, err := os.Stat(dir); err != nil {
|
|
return newErrNotFound("No directory %s", dir)
|
|
}
|
|
|
|
check := []string{}
|
|
if !imp.cfg.OnlyDashboards {
|
|
check = append(check, "index-pattern")
|
|
}
|
|
wantDashboards := false
|
|
if !imp.cfg.OnlyIndex {
|
|
check = append(check, "dashboard")
|
|
wantDashboards = true
|
|
}
|
|
|
|
types := []string{}
|
|
for _, c := range check {
|
|
if imp.subdirExists(dir, c) {
|
|
types = append(types, c)
|
|
}
|
|
}
|
|
|
|
if len(types) == 0 {
|
|
return newErrNotFound("The directory %s does not contain the %s subdirectory."+
|
|
" There is nothing to import into Kibana.", dir, strings.Join(check, " or "))
|
|
}
|
|
|
|
importDashboards := false
|
|
for _, t := range types {
|
|
err = imp.ImportDir(t, dir)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to import %s: %v", t, err)
|
|
}
|
|
|
|
if t == "dashboard" {
|
|
importDashboards = true
|
|
}
|
|
}
|
|
|
|
if wantDashboards && !importDashboards {
|
|
return newErrNotFound("No dashboards to import. Please make sure the %s directory "+
|
|
"contains a dashboard directory.", dir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (imp Importer) subdirExists(parent string, child string) bool {
|
|
if _, err := os.Stat(path.Join(parent, child)); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|