diff --git a/internal/elasticsearch/client.go b/internal/elasticsearch/client.go index 74cf73d528..2e888d7c79 100644 --- a/internal/elasticsearch/client.go +++ b/internal/elasticsearch/client.go @@ -196,7 +196,7 @@ type Info struct { Version struct { Number string `json:"number"` BuildFlavor string `json:"build_flavor"` - } `json:"version` + } `json:"version"` } // Info gets cluster information and metadata. diff --git a/internal/kibana/packages.go b/internal/kibana/packages.go index 34fcf7a079..3b8950249e 100644 --- a/internal/kibana/packages.go +++ b/internal/kibana/packages.go @@ -7,6 +7,7 @@ package kibana import ( "context" "encoding/json" + "errors" "fmt" "net/http" "os" @@ -14,6 +15,8 @@ import ( "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) @@ -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 { + 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) diff --git a/internal/packages/installer/factory.go b/internal/packages/installer/factory.go index 29819f43a0..dfcacfbae3 100644 --- a/internal/packages/installer/factory.go +++ b/internal/packages/installer/factory.go @@ -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 { @@ -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) @@ -75,7 +82,7 @@ func NewForPackage(options Options) (Installer, error) { target, err := builder.BuildPackage(builder.BuildOptions{ PackageRoot: options.RootPath, - CreateZip: supportsZip, + CreateZip: supportsUploadZip, SignPackage: false, SkipValidation: options.SkipValidation, }) @@ -83,12 +90,33 @@ func NewForPackage(options Options) (Installer, error) { 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 {