internal/profile/profile.go (208 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 profile
import (
"embed"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/elastic/go-resource"
"github.com/elastic/elastic-package/internal/configuration/locations"
"github.com/elastic/elastic-package/internal/files"
)
const (
// PackageProfileMetaFile is the filename of the profile metadata file
PackageProfileMetaFile = "profile.json"
// PackageProfileConfigFile is the filename of the profile configuration file
PackageProfileConfigFile = "config.yml"
// DefaultProfile is the name of the default profile.
DefaultProfile = "default"
// dateFormat is the format of the dates in the profile metadata.
dateFormat = time.RFC3339Nano
)
//go:embed _static
var static embed.FS
var (
staticSource = resource.NewSourceFS(static)
profileResources = []resource.Resource{
&resource.File{
Path: PackageProfileMetaFile,
Content: profileMetadataContent,
},
&resource.File{
Path: PackageProfileConfigFile + ".example",
Content: staticSource.File("_static/config.yml.example"),
},
}
)
type Options struct {
ProfilesDirPath string
Name string
FromProfile string
OverwriteExisting bool
}
func CreateProfile(options Options) error {
if options.ProfilesDirPath == "" {
loc, err := locations.NewLocationManager()
if err != nil {
return fmt.Errorf("error finding profile dir location: %w", err)
}
options.ProfilesDirPath = loc.ProfileDir()
}
if options.Name == "" {
options.Name = DefaultProfile
}
if !options.OverwriteExisting {
_, err := loadProfile(options.ProfilesDirPath, options.Name)
if err == nil {
return fmt.Errorf("profile %q already exists", options.Name)
}
if err != nil && !errors.Is(err, ErrNotAProfile) {
return fmt.Errorf("failed to check if profile %q exists: %w", options.Name, err)
}
}
// If they're creating from Default, assume they want the actual default, and
// not whatever is currently inside default.
if from := options.FromProfile; from != "" && from != DefaultProfile {
return createProfileFrom(options)
}
return createProfile(options, profileResources)
}
func createProfile(options Options, resources []resource.Resource) error {
profileDir := filepath.Join(options.ProfilesDirPath, options.Name)
resourceManager := resource.NewManager()
resourceManager.AddFacter(resource.StaticFacter{
"creation_date": time.Now().UTC().Format(dateFormat),
"profile_name": options.Name,
"profile_path": profileDir,
})
os.MkdirAll(profileDir, 0755)
resourceManager.RegisterProvider("file", &resource.FileProvider{
Prefix: profileDir,
})
results, err := resourceManager.Apply(resources)
if err != nil {
var errors []string
for _, result := range results {
if err := result.Err(); err != nil {
errors = append(errors, err.Error())
}
}
return fmt.Errorf("%w: %s", err, strings.Join(errors, ", "))
}
return nil
}
func createProfileFrom(options Options) error {
from, err := LoadProfile(options.FromProfile)
if err != nil {
return fmt.Errorf("failed to load profile to copy %q: %w", options.FromProfile, err)
}
profileDir := filepath.Join(options.ProfilesDirPath, options.Name)
err = files.CopyAll(from.ProfilePath, profileDir)
if err != nil {
return fmt.Errorf("failed to copy files from profile %q to %q", options.FromProfile, options.Name)
}
overwriteOptions := options
overwriteOptions.OverwriteExisting = true
return createProfile(overwriteOptions, profileResources)
}
// Profile manages a a given user config profile
type Profile struct {
// ProfilePath is the absolute path to the profile
ProfilePath string
ProfileName string
config config
metadata Metadata
overrides map[string]string
}
// Path returns an absolute path to the given file
func (profile Profile) Path(names ...string) string {
elems := append([]string{profile.ProfilePath}, names...)
return filepath.Join(elems...)
}
// Config returns a configuration setting, or its default if setting not found
func (profile Profile) Config(name string, def string) string {
v, found := profile.overrides[name]
if found {
return v
}
v, found = profile.config.get(name)
if found {
return v
}
return def
}
func (profile *Profile) Decode(name string, dst any) error {
return profile.config.Decode(name, dst)
}
// RuntimeOverrides defines configuration overrides for the current session.
func (profile *Profile) RuntimeOverrides(overrides map[string]string) {
profile.overrides = overrides
}
// ErrNotAProfile is returned in cases where we don't have a valid profile directory
var ErrNotAProfile = errors.New("not a profile")
// ComposeEnvVars returns a list of environment variables that can be passed
// to docker-compose for the sake of filling out paths and names in the docker compose file.
func (profile Profile) ComposeEnvVars() []string {
return []string{
fmt.Sprintf("PROFILE_NAME=%s", profile.ProfileName),
}
}
// DeleteProfile deletes a profile from the default elastic-package config dir
func DeleteProfile(profileName string) error {
if profileName == DefaultProfile {
return errors.New("cannot remove default profile")
}
loc, err := locations.NewLocationManager()
if err != nil {
return fmt.Errorf("error finding stack dir location: %w", err)
}
pathToDelete := filepath.Join(loc.ProfileDir(), profileName)
return os.RemoveAll(pathToDelete)
}
// FetchAllProfiles returns a list of profile values
func FetchAllProfiles(profilesDirPath string) ([]Metadata, error) {
dirList, err := os.ReadDir(profilesDirPath)
if errors.Is(err, os.ErrNotExist) {
return []Metadata{}, nil
}
if err != nil {
return []Metadata{}, fmt.Errorf("error reading from directory %s: %w", profilesDirPath, err)
}
var profiles []Metadata
// TODO: this should read a profile.json file or something like that
for _, item := range dirList {
if !item.IsDir() {
continue
}
profile, err := loadProfile(profilesDirPath, item.Name())
if errors.Is(err, ErrNotAProfile) {
continue
}
if err != nil {
return profiles, fmt.Errorf("error loading profile %s: %w", item.Name(), err)
}
profiles = append(profiles, profile.metadata)
}
return profiles, nil
}
// LoadProfile loads an existing profile from the default elastic-package config dir.
func LoadProfile(profileName string) (*Profile, error) {
loc, err := locations.NewLocationManager()
if err != nil {
return nil, fmt.Errorf("error finding stack dir location: %w", err)
}
profile, err := loadProfile(loc.ProfileDir(), profileName)
if err != nil {
return nil, err
}
err = profile.migrate(currentVersion)
if err != nil {
return nil, fmt.Errorf("error migrating profile to version %v: %w", currentVersion, err)
}
return profile, nil
}
// loadProfile loads an existing profile
func loadProfile(profilesDirPath string, profileName string) (*Profile, error) {
profilePath := filepath.Join(profilesDirPath, profileName)
metadata, err := loadProfileMetadata(filepath.Join(profilePath, PackageProfileMetaFile))
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %w", ErrNotAProfile, err)
}
if err != nil {
return nil, fmt.Errorf("error reading profile metadata: %w", err)
}
configPath := filepath.Join(profilePath, PackageProfileConfigFile)
config, err := loadProfileConfig(configPath)
if err != nil {
return nil, fmt.Errorf("error loading configuration for profile %q: %w", profileName, err)
}
profile := Profile{
ProfileName: profileName,
ProfilePath: profilePath,
config: config,
metadata: metadata,
}
return &profile, nil
}