Skip to content

Add a new validation to use or not upload API to install packages #2511

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

Merged
merged 11 commits into from
Apr 8, 2025
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