internal/kibana/packages.go (164 lines of code) (raw):
// 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
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"github.com/elastic/elastic-package/internal/packages"
)
var ErrNotSupported error = errors.New("not supported")
// InstallPackage installs the given package in Fleet.
func (c *Client) InstallPackage(ctx context.Context, name, version string) ([]packages.Asset, error) {
path := c.epmPackageUrl(name, version)
reqBody := []byte(`{"force":true}`) // allows installing older versions of the package being tested
statusCode, respBody, err := c.post(ctx, path, reqBody)
if err != nil {
return nil, fmt.Errorf("could not install package: %w", err)
}
return processResults("install", statusCode, respBody)
}
// EnsureZipPackageCanBeInstalled checks whether or not it can be installed a package using the upload API.
// This is intened to be used between 8.7.0 and 8.8.2 stack versions, and it is only safe to be run in those
// stack versions.
func (c *Client) EnsureZipPackageCanBeInstalled(ctx context.Context) error {
path := fmt.Sprintf("%s/epm/packages", FleetAPI)
req, err := c.newRequest(ctx, http.MethodPost, path, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/zip")
req.Header.Add("elastic-api-version", "2023-10-31")
statusCode, respBody, err := c.doRequest(req)
if err != nil {
return fmt.Errorf("could not install zip package: %w", err)
}
switch statusCode {
case http.StatusBadRequest:
// If the stack allows to use the upload API, the response is like this one:
// {
// "statusCode":400,
// "error":"Bad Request",
// "message":"Error during extraction of package: Error: end of central directory record signature not found. Assumed content type was application/zip, check if this matches the archive type."
// }
return nil
case http.StatusForbidden:
var resp struct {
Message string `json:"message"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return fmt.Errorf("could not unmarhsall response to JSON: %w", err)
}
if resp.Message == "Requires Enterprise license" {
return ErrNotSupported
}
}
return fmt.Errorf("unexpected response (status code %d): %s", statusCode, string(respBody))
}
// InstallZipPackage installs the local zip package in Fleet.
func (c *Client) InstallZipPackage(ctx context.Context, zipFile string) ([]packages.Asset, error) {
path := fmt.Sprintf("%s/epm/packages", FleetAPI)
body, err := os.Open(zipFile)
if err != nil {
return nil, fmt.Errorf("failed to read zip file: %w", err)
}
defer body.Close()
req, err := c.newRequest(ctx, http.MethodPost, path, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/zip")
req.Header.Add("elastic-api-version", "2023-10-31")
statusCode, respBody, err := c.doRequest(req)
if err != nil {
return nil, fmt.Errorf("could not install zip package: %w", err)
}
return processResults("zip-install", statusCode, respBody)
}
// RemovePackage removes the given package from Fleet.
func (c *Client) RemovePackage(ctx context.Context, name, version string) ([]packages.Asset, error) {
path := c.epmPackageUrl(name, version)
statusCode, respBody, err := c.delete(ctx, path)
if err != nil {
return nil, fmt.Errorf("could not delete package: %w", err)
}
return processResults("remove", statusCode, respBody)
}
// FleetPackage contains information about a package in Fleet.
type FleetPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Status string `json:"status"`
SavedObject *struct {
Attributes struct {
InstalledElasticsearchAssets []packages.Asset `json:"installed_es"`
InstalledKibanaAssets []packages.Asset `json:"installed_kibana"`
PackageAssets []packages.Asset `json:"package_assets"`
} `json:"attributes"`
} `json:"savedObject"`
InstallationInfo *struct {
InstalledElasticsearchAssets []packages.Asset `json:"installed_es"`
InstalledKibanaAssets []packages.Asset `json:"installed_kibana"`
} `json:"installationInfo"`
}
func (p *FleetPackage) Assets() []packages.Asset {
var assets []packages.Asset
if p.SavedObject != nil {
assets = append(assets, p.SavedObject.Attributes.InstalledElasticsearchAssets...)
assets = append(assets, p.SavedObject.Attributes.InstalledKibanaAssets...)
return assets
}
// starting in 9.0.0 "savedObject" fields does not exist in the API response
assets = append(assets, p.InstallationInfo.InstalledElasticsearchAssets...)
assets = append(assets, p.InstallationInfo.InstalledKibanaAssets...)
return assets
}
type ErrPackageNotFound struct {
name string
}
func (e *ErrPackageNotFound) Error() string {
return fmt.Sprintf("package %s not found", e.name)
}
// GetPackage obtains information about a package from Fleet.
func (c *Client) GetPackage(ctx context.Context, name string) (*FleetPackage, error) {
path := c.epmPackageUrl(name, "")
statusCode, respBody, err := c.get(ctx, path)
if err != nil {
return nil, fmt.Errorf("could not get package: %w", err)
}
if statusCode == http.StatusNotFound {
return nil, &ErrPackageNotFound{name: name}
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get package; API status code = %d; response body = %s", statusCode, string(respBody))
}
var response struct {
// Response is here when old packages API is used (before 8.0)
Response *FleetPackage `json:"response"`
// Response is here when new packages API is used (since 8.0)
Item *FleetPackage `json:"item"`
}
err = json.Unmarshal(respBody, &response)
switch {
case err != nil:
return nil, fmt.Errorf("failed to decode package response: %w", err)
case response.Response != nil:
return response.Response, nil
case response.Item != nil:
return response.Item, nil
default:
return nil, fmt.Errorf("package %s not found in response: %s", name, string(respBody))
}
}
func (c *Client) epmPackageUrl(name, version string) string {
if version == "" {
return fmt.Sprintf("%s/epm/packages/%s", FleetAPI, name)
}
switch {
case c.semver != nil && c.semver.Major() < 8:
return fmt.Sprintf("%s/epm/packages/%s-%s", FleetAPI, name, version)
default:
return fmt.Sprintf("%s/epm/packages/%s/%s", FleetAPI, name, version)
}
}
func processResults(action string, statusCode int, respBody []byte) ([]packages.Asset, error) {
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not %s package; API status code = %d; response body = %s", action, statusCode, respBody)
}
var resp struct {
// Assets are here when old packages API is used (with hyphen, before 8.0).
Response []packages.Asset `json:"response"`
// Assets are here when new packages API is used (with slash, since 8.0).
Items []packages.Asset `json:"items"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert %s package (response) to JSON: %w", action, err)
}
if len(resp.Response) > 0 {
return resp.Response, nil
}
return resp.Items, nil
}