pkg/driver/helmdriver.go (234 lines of code) (raw):
package driver
import (
"context"
"errors"
"fmt"
"os"
"reflect"
"strings"
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/storage/driver"
api "github.com/aws/eks-anywhere-packages/api/v1alpha1"
auth "github.com/aws/eks-anywhere-packages/pkg/authenticator"
packagesRegistry "github.com/aws/eks-anywhere-packages/pkg/registry"
)
const (
varHelmUpgradeMaxHistory = 2
)
// helmDriver implements PackageDriver to install packages from Helm charts.
type helmDriver struct {
cfg *action.Configuration
secretAuth auth.Authenticator
tcc auth.TargetClusterClient
log logr.Logger
settings *cli.EnvSettings
}
var _ PackageDriver = (*helmDriver)(nil)
func NewHelm(log logr.Logger, secretAuth auth.Authenticator, tcc auth.TargetClusterClient) *helmDriver {
return &helmDriver{
secretAuth: secretAuth,
tcc: tcc,
log: log,
}
}
func (d *helmDriver) Initialize(ctx context.Context, clusterName string) (err error) {
err = d.secretAuth.Initialize(clusterName)
if err != nil {
d.log.Info("failed to change target cluster for secrets")
}
err = d.tcc.Initialize(ctx, clusterName)
if err != nil {
return fmt.Errorf("initialiing target cluster %s client for helm driver: %w", clusterName, err)
}
d.settings = cli.New()
insecure := packagesRegistry.GetRegistryInsecure(clusterName)
caFile := packagesRegistry.GetClusterCertificateFileName(clusterName)
client, err := newRegistryClient("", "", caFile, insecure, d.settings)
if err != nil {
return fmt.Errorf("creating registry client for helm driver: %w", err)
}
d.cfg = &action.Configuration{RegistryClient: client}
err = d.cfg.Init(d.tcc, d.settings.Namespace(), os.Getenv("HELM_DRIVER"), helmLog(d.log))
if err != nil {
return fmt.Errorf("initializing helm driver: %w", err)
}
return nil
}
func (d *helmDriver) Install(ctx context.Context,
name, namespace string, createNamespace bool, source api.PackageOCISource, values map[string]interface{},
) error {
var err error
install := action.NewInstall(d.cfg)
install.Version = source.Version
install.ReleaseName = name
install.CreateNamespace = createNamespace
helmChart, err := d.getChart(install, source)
if err != nil {
return fmt.Errorf("loading helm chart %s: %w", name, err)
}
// If no target namespace provided read chart values to find namespace
if namespace == "" {
if chartNS, ok := helmChart.Values["defaultNamespace"]; ok {
namespace = chartNS.(string)
} else {
// Fall back case of assuming its default
namespace = "default"
}
}
install.Namespace = namespace
// Update values with imagePullSecrets
// If no secret values we should still continue as it could be case of public registry or local registry
secretvals, err := d.secretAuth.GetSecretValues(ctx, namespace)
if err != nil {
secretvals = nil
// Continue as its possible that a private registry is being used here and thus no data necessary
}
for key, val := range secretvals {
values[key] = val
}
// Check if there exists a matching helm release.
get := action.NewGet(d.cfg)
_, err = get.Run(name)
if err != nil {
if errors.Is(err, driver.ErrReleaseNotFound) {
// Trigger configmap updates and namespace before trying to install charts
if err := d.secretAuth.AddToConfigMap(ctx, name, namespace); err != nil {
d.log.Info("failed to Update ConfigMap with installed namespace", "error", err)
}
if err := d.secretAuth.AddSecretToAllNamespace(ctx); err != nil {
d.log.Info("failed to Update Secret in all namespaces", "error", err)
}
err = d.createRelease(ctx, install, helmChart, values)
if err != nil {
err1 := d.secretAuth.DelFromConfigMap(ctx, name, namespace)
if err1 != nil {
d.log.Info("failed to remove namespace from configmap")
}
return err
}
// Failsafe in event namespace is created via the charts
if err := d.secretAuth.AddSecretToAllNamespace(ctx); err != nil {
d.log.Info("failed to Update Secret in all namespaces", "error", err)
}
return nil
}
return fmt.Errorf("getting helm release %s: %w", name, err)
}
err = d.upgradeRelease(ctx, name, helmChart, values)
if err != nil {
return fmt.Errorf("upgrading helm chart %s: %w", name, err)
}
// Update installed-namespaces on successful install
err = d.secretAuth.AddToConfigMap(ctx, name, namespace)
if err != nil {
d.log.Info("failed to Update ConfigMap with installed namespace", "error", err)
}
if err := d.secretAuth.AddSecretToAllNamespace(ctx); err != nil {
d.log.Info("failed to Update Secret in all namespaces", "error", err)
}
err = d.upgradeRelease(ctx, name, helmChart, values)
if err != nil {
return fmt.Errorf("upgrading helm chart %s: %w", name, err)
}
if err := d.secretAuth.AddSecretToAllNamespace(ctx); err != nil {
d.log.Info("failed to Update Secret in all namespaces", "error", err)
}
return nil
}
func (d *helmDriver) getChart(install *action.Install, source api.PackageOCISource) (*chart.Chart, error) {
url := source.GetChartUri()
chartPath, err := install.LocateChart(url, d.settings)
if err != nil {
return nil, fmt.Errorf("locating helm chart %s tag %s: %w", url, source.Digest, err)
}
return loader.Load(chartPath)
}
func (d *helmDriver) createRelease(ctx context.Context,
install *action.Install, helmChart *chart.Chart, values map[string]interface{},
) error {
_, err := install.RunWithContext(ctx, helmChart, values)
if err != nil {
return fmt.Errorf("installing helm chart %s: %w", install.ReleaseName, err)
}
return nil
}
// helmChartURLIsPrefixed detects if the given URL has an acceptable scheme
// prefix.
func helmChartURLIsPrefixed(url string) bool {
return strings.HasPrefix(url, "https://") ||
strings.HasPrefix(url, "http://") ||
strings.HasPrefix(url, "oci://")
}
// upgradeRelease instructs helm to upgrade a release.
func (d *helmDriver) upgradeRelease(ctx context.Context, name string,
helmChart *chart.Chart, values map[string]interface{},
) (err error) {
// upgrade unless changes in the values are detected. For POC, run helm
// every time and rely on its idempotency.
upgrade := action.NewUpgrade(d.cfg)
// Limit history saved as secret for resource limit
upgrade.MaxHistory = varHelmUpgradeMaxHistory
_, err = upgrade.RunWithContext(ctx, name, helmChart, values)
if err != nil {
return fmt.Errorf("upgrading helm release %s: %w", name, err)
}
return nil
}
func (d *helmDriver) Uninstall(ctx context.Context, name string) (err error) {
uninstall := action.NewUninstall(d.cfg)
rel, err := uninstall.Run(name)
if err != nil {
if errors.Is(err, driver.ErrReleaseNotFound) {
return nil
}
return fmt.Errorf("uninstalling helm chart %s: %w", name, err)
}
err = d.secretAuth.DelFromConfigMap(ctx, name, rel.Release.Namespace)
if err != nil {
d.log.Info("failed to remove namespace from configmap")
}
return nil
}
// helmLog wraps logr.Logger to make it compatible with helm's DebugLog.
func helmLog(log logr.Logger) action.DebugLog {
return func(template string, args ...interface{}) {
log.Info(fmt.Sprintf(template, args...))
}
}
func (d *helmDriver) IsConfigChanged(_ context.Context, name string, values map[string]interface{}) (bool, error) {
get := action.NewGet(d.cfg)
rel, err := get.Run(name)
if err != nil {
d.log.Info("Installation not found %q: %w", name, err)
return true, nil
}
// Check imagePullSecret not defined in config
if _, exist := values["imagePullSecrets"]; !exist {
// Check if imagePullSecrets was added by driver
if val, ok := rel.Config["imagePullSecrets"]; ok {
values["imagePullSecrets"] = val
}
}
return !reflect.DeepEqual(values, rel.Config), nil
}
func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify bool, settings *cli.EnvSettings) (*registry.Client, error) {
if certFile != "" && keyFile != "" || caFile != "" || !insecureSkipTLSverify {
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify, settings)
if err != nil {
return nil, err
}
return registryClient, nil
}
registryClient, err := newDefaultRegistryClient(settings)
if err != nil {
return nil, err
}
return registryClient, nil
}
func newDefaultRegistryClient(settings *cli.EnvSettings) (*registry.Client, error) {
// Create a new registry client
registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug),
registry.ClientOptEnableCache(false),
registry.ClientOptWriter(os.Stderr),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil {
return nil, err
}
return registryClient, nil
}
func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool, settings *cli.EnvSettings) (*registry.Client, error) {
// Create a new registry client
registryClient, err := registry.NewRegistryClientWithTLS(os.Stderr, certFile, keyFile, caFile, insecureSkipTLSverify,
settings.RegistryConfig, settings.Debug,
)
if err != nil {
return nil, err
}
return registryClient, nil
}