internal/packages/status/serverless.go (209 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 status
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/elastic/go-ucfg"
"github.com/elastic/go-ucfg/yaml"
"github.com/elastic/elastic-package/internal/configuration/locations"
"github.com/elastic/elastic-package/internal/logger"
)
var serverlessProjectTypes = []serverlessProjectType{
{
Name: "observability",
ConfigURL: "https://raw.githubusercontent.com/elastic/kibana/main/config/serverless.oblt.yml",
Fallback: serverlessProjectTypeFallback{
Capabilities: []string{
"apm",
"observability",
},
SpecMin: "3.0",
SpecMax: "3.0",
},
},
{
Name: "security",
ConfigURL: "https://raw.githubusercontent.com/elastic/kibana/main/config/serverless.security.yml",
Fallback: serverlessProjectTypeFallback{
Capabilities: []string{
"security",
},
SpecMin: "3.0",
SpecMax: "3.0",
},
},
{
Name: "elasticsearch",
ConfigURL: "https://raw.githubusercontent.com/elastic/kibana/main/config/serverless.es.yml",
Fallback: serverlessProjectTypeFallback{
FleetDisabled: true,
Capabilities: []string{},
},
},
}
type ServerlessProjectType struct {
Name string
Capabilities []string
ExcludePackages []string
SpecMax string
SpecMin string
}
func GetServerlessProjectTypes(client *http.Client) []ServerlessProjectType {
cache, valid, err := loadCachedServerlessProjectTypes()
if err == nil && valid {
return cache
}
if err != nil {
logger.Debugf("failed to load serverless config cache: %v", err)
}
var projectTypes []ServerlessProjectType
for _, projectType := range serverlessProjectTypes {
config, err := requestServerlessKibanaConfig(client, projectType.ConfigURL)
if err != nil {
logger.Debugf("failed to get serverless project type configuration from %q: %v", projectType.ConfigURL, err)
if !projectType.Fallback.FleetDisabled {
projectTypes = append(projectTypes, ServerlessProjectType{
Name: projectType.Name,
Capabilities: projectType.Fallback.Capabilities,
SpecMax: projectType.Fallback.SpecMax,
SpecMin: projectType.Fallback.SpecMin,
})
}
continue
}
if enabled := config.XPack.Fleet.Enabled; enabled != nil && !*enabled {
continue
}
projectTypes = append(projectTypes, ServerlessProjectType{
Name: projectType.Name,
Capabilities: config.XPack.Fleet.Internal.Registry.Capabilities,
ExcludePackages: config.XPack.Fleet.Internal.Registry.ExcludePackages,
SpecMax: config.XPack.Fleet.Internal.Registry.Spec.Max,
SpecMin: config.XPack.Fleet.Internal.Registry.Spec.Min,
})
}
if len(projectTypes) > 0 {
err := saveCachedServerlessProjectTypes(projectTypes)
if err != nil {
logger.Debugf("failed to save serverless config cache: %v", err)
}
}
return projectTypes
}
type serverlessProjectType struct {
Name string
ConfigURL string
Fallback serverlessProjectTypeFallback
}
type serverlessProjectTypeFallback struct {
FleetDisabled bool
Capabilities []string
SpecMax string
SpecMin string
}
type kibanaConfig struct {
XPack struct {
Fleet struct {
Enabled *bool `config:"enabled"`
Internal struct {
Registry struct {
Capabilities []string `config:"capabilities"`
ExcludePackages []string `config:"excludePackages"`
Spec struct {
Max string `config:"max"`
Min string `config:"min"`
} `config:"spec"`
} `config:"registry"`
} `config:"internal"`
} `config:"fleet"`
} `config:"xpack"`
}
func parseServerlessKibanaConfig(r io.Reader) (*kibanaConfig, error) {
d, err := io.ReadAll(r)
if err != nil {
return nil, err
}
config, err := yaml.NewConfig(d, ucfg.PathSep("."))
if err != nil {
return nil, fmt.Errorf("failed to parse kibana configuration: %w", err)
}
var kibanaConfig kibanaConfig
err = config.Unpack(&kibanaConfig, ucfg.PathSep("."))
if err != nil {
return nil, fmt.Errorf("failed to unpack kibana configuration: %w", err)
}
return &kibanaConfig, nil
}
func requestServerlessKibanaConfig(client *http.Client, configURL string) (*kibanaConfig, error) {
resp, err := client.Get(configURL)
if err != nil {
return nil, fmt.Errorf("failed to get kibana configuration: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("received status code %d when requesting kibana configuration", resp.StatusCode)
}
return parseServerlessKibanaConfig(resp.Body)
}
const (
cachedServerlessProjectTypesFileName = "serverless.json"
cachedServerlessProjectTypesCacheExpiration = 1 * time.Hour
)
type cachedServerlessProjectTypes struct {
Timestamp time.Time
ProjectTypes []ServerlessProjectType
}
func loadCachedServerlessProjectTypes() ([]ServerlessProjectType, bool, error) {
locationManager, err := locations.NewLocationManager()
if err != nil {
return nil, false, err
}
cacheDir := locationManager.CacheDir(locations.KibanaConfigCacheName)
cacheFilePath := filepath.Join(cacheDir, cachedServerlessProjectTypesFileName)
d, err := os.ReadFile(cacheFilePath)
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var cache cachedServerlessProjectTypes
err = json.Unmarshal(d, &cache)
if err != nil {
return nil, false, err
}
if time.Now().After(cache.Timestamp.Add(cachedServerlessProjectTypesCacheExpiration)) {
return cache.ProjectTypes, false, nil
}
return cache.ProjectTypes, true, nil
}
func saveCachedServerlessProjectTypes(projectTypes []ServerlessProjectType) error {
locationManager, err := locations.NewLocationManager()
if err != nil {
return err
}
cacheDir := locationManager.CacheDir(locations.KibanaConfigCacheName)
err = os.MkdirAll(cacheDir, 0755)
if err != nil {
return err
}
cache := cachedServerlessProjectTypes{
Timestamp: time.Now(),
ProjectTypes: projectTypes,
}
d, err := json.Marshal(cache)
if err != nil {
return err
}
cacheFilePath := filepath.Join(cacheDir, cachedServerlessProjectTypesFileName)
err = os.WriteFile(cacheFilePath, d, 0644)
if err != nil {
return err
}
return nil
}