Skip to content
2 changes: 1 addition & 1 deletion internal/elasticsearch/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ type Info struct {
Version struct {
Number string `json:"number"`
BuildFlavor string `json:"build_flavor"`
} `json:"version`
} `json:"version"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed while trying to get and use the Elastic stack subscription.

}

// Info gets cluster information and metadata.
Expand Down
44 changes: 44 additions & 0 deletions internal/kibana/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ 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)
Expand All @@ -27,6 +30,47 @@ func (c *Client) InstallPackage(ctx context.Context, name, version string) ([]pa
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit hacky and would be prone to fail if this API would change, maybe add a comment mentioning that is only intended to be used between 8.7.0 and 8.8.2, and is only safe to be used on these versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment here 4da2bd9

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)
Expand Down
40 changes: 34 additions & 6 deletions internal/packages/installer/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import (
"github.com/elastic/elastic-package/internal/validation"
)

var semver8_7_0 = semver.MustParse("8.7.0")
var (
semver8_7_0 = semver.MustParse("8.7.0")
semver8_8_2 = semver.MustParse("8.8.2")
)

// Installer is responsible for installation/uninstallation of the package.
type Installer interface {
Expand Down Expand Up @@ -54,11 +57,15 @@ func NewForPackage(options Options) (Installer, error) {
return nil, fmt.Errorf("failed to get kibana version: %w", err)
}

supportsZip := !version.LessThan(semver8_7_0)
supportsUploadZip, reason, err := isAllowedInstallationViaApi(context.TODO(), options.Kibana, version)
if err != nil {
return nil, fmt.Errorf("failed to validate whether or not it can be used upload API: %w", err)
}
if options.ZipPath != "" {
if !supportsZip {
return nil, fmt.Errorf("not supported uploading zip packages in Kibana %s (%s required)", version, semver8_7_0)
if !supportsUploadZip {
return nil, errors.New(reason)
}

if !options.SkipValidation {
logger.Debugf("Validating built .zip package (path: %s)", options.ZipPath)
errs, skipped := validation.ValidateAndFilterFromZip(options.ZipPath)
Expand All @@ -75,20 +82,41 @@ func NewForPackage(options Options) (Installer, error) {

target, err := builder.BuildPackage(builder.BuildOptions{
PackageRoot: options.RootPath,
CreateZip: supportsZip,
CreateZip: supportsUploadZip,
SignPackage: false,
SkipValidation: options.SkipValidation,
})
if err != nil {
return nil, fmt.Errorf("failed to build package: %v", err)
}

if supportsZip {
if supportsUploadZip {
return CreateForZip(options.Kibana, target)
}
return CreateForManifest(options.Kibana, target)
}

func isAllowedInstallationViaApi(ctx context.Context, kbnClient *kibana.Client, kibanaVersion *semver.Version) (bool, string, error) {
reason := ""
if kibanaVersion.LessThan(semver8_7_0) {
reason = fmt.Sprintf("not supported uploading zip packages in Kibana %s (%s required)", kibanaVersion, semver8_7_0)
return false, reason, nil
}

if kibanaVersion.LessThan(semver8_8_2) {
err := kbnClient.EnsureZipPackageCanBeInstalled(ctx)
if errors.Is(err, kibana.ErrNotSupported) {
reason = fmt.Sprintf("not supported uploading zip packages in Kibana %s (%s required or Enteprise license)", kibanaVersion, semver8_8_2)
return false, reason, nil
}
if err != nil {
return false, "", err
}
}

return true, "", nil
}

func kibanaVersion(kibana *kibana.Client) (*semver.Version, error) {
version, err := kibana.Version()
if err != nil {
Expand Down