constellation/cli/internal/helm/client.go
Otto Bittner efcd0337b4
Microservice upgrades (#729)
Run with: constellation upgrade execute --helm.
This will only upgrade the helm charts. No config is needed.

Upgrades are implemented via helm's upgrade action, i.e. they
automatically roll back if something goes wrong. Releases could 
still be managed via helm, even after an upgrade with constellation
has been done.

Currently not user facing as CRD/CR backups are still in progress.
These backups should be automatically created and saved to the 
user's disk as updates may delete CRs. This happens implicitly 
through CRD upgrades, which are part of microservice upgrades.
2022-12-19 16:52:15 +01:00

222 lines
7.3 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package helm
import (
"context"
"fmt"
"strings"
"time"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/cli"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
)
// Client handles interaction with helm.
type Client struct {
config *action.Configuration
crdClient *apiextensionsclient.Clientset
log debugLog
}
// NewClient returns a new initializes client for the namespace Client.
func NewClient(kubeConfigPath, helmNamespace string, client *apiextensionsclient.Clientset, log debugLog) (*Client, error) {
settings := cli.New()
settings.KubeConfig = kubeConfigPath // constants.AdminConfFilename
actionConfig := &action.Configuration{}
if err := actionConfig.Init(settings.RESTClientGetter(), helmNamespace, "secret", log.Debugf); err != nil {
return nil, fmt.Errorf("initializing config: %w", err)
}
return &Client{config: actionConfig, crdClient: client, log: log}, nil
}
// CurrentVersion returns the version of the currently installed helm release.
func (c *Client) CurrentVersion(release string) (string, error) {
client := action.NewList(c.config)
client.Filter = release
rel, err := client.Run()
if err != nil {
return "", err
}
if len(rel) == 0 {
return "", fmt.Errorf("release %s not found", release)
}
if len(rel) > 1 {
return "", fmt.Errorf("multiple releases found for %s", release)
}
if rel[0] == nil || rel[0].Chart == nil || rel[0].Chart.Metadata == nil {
return "", fmt.Errorf("received invalid release %s", release)
}
return rel[0].Chart.Metadata.Version, nil
}
// Upgrade runs a helm-upgrade on all deployments that are managed via Helm.
// If the CLI receives an interrupt signal it will cancel the context.
// Canceling the context will prompt helm to abort and roll back the ongoing upgrade.
func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout time.Duration) error {
action := action.NewUpgrade(c.config)
action.Atomic = true
action.Namespace = constants.HelmNamespace
action.ReuseValues = false
action.Timeout = timeout
ciliumChart, err := loadChartsDir(helmFS, ciliumPath)
if err != nil {
return fmt.Errorf("loading cilium: %w", err)
}
certManagerChart, err := loadChartsDir(helmFS, certManagerPath)
if err != nil {
return fmt.Errorf("loading cert-manager: %w", err)
}
conOperatorChart, err := loadChartsDir(helmFS, conOperatorsPath)
if err != nil {
return fmt.Errorf("loading operators: %w", err)
}
conServicesChart, err := loadChartsDir(helmFS, conServicesPath)
if err != nil {
return fmt.Errorf("loading constellation-services chart: %w", err)
}
values, err := c.prepareValues(ciliumChart, ciliumReleaseName)
if err != nil {
return err
}
if _, err := action.RunWithContext(ctx, ciliumReleaseName, ciliumChart, values); err != nil {
return fmt.Errorf("upgrading cilium: %w", err)
}
values, err = c.prepareValues(certManagerChart, certManagerReleaseName)
if err != nil {
return err
}
if _, err := action.RunWithContext(ctx, certManagerReleaseName, certManagerChart, values); err != nil {
return fmt.Errorf("upgrading cert-manager: %w", err)
}
err = c.updateOperatorCRDs(ctx, conOperatorChart)
if err != nil {
return fmt.Errorf("updating operator CRDs: %w", err)
}
values, err = c.prepareValues(conOperatorChart, conOperatorsReleaseName)
if err != nil {
return err
}
if _, err := action.RunWithContext(ctx, conOperatorsReleaseName, conOperatorChart, values); err != nil {
return fmt.Errorf("upgrading services: %w", err)
}
values, err = c.prepareValues(conServicesChart, conServicesReleaseName)
if err != nil {
return err
}
if _, err := action.RunWithContext(ctx, conServicesReleaseName, conServicesChart, values); err != nil {
return fmt.Errorf("upgrading operators: %w", err)
}
return nil
}
// prepareCertManagerValues returns a values map as required for helm-upgrade.
// It imitates the behaviour of helm's reuse-values flag by fetching the current values from the cluster
// and merging the fetched values with the locally found values.
// This is done to ensure that new values (from upgrades of the local files) end up in the cluster.
// reuse-values does not ensure this.
func (c *Client) prepareValues(chart *chart.Chart, releaseName string) (map[string]any, error) {
// Ensure installCRDs is set for cert-manager chart.
if releaseName == certManagerReleaseName {
chart.Values["installCRDs"] = true
}
values, err := c.GetValues(releaseName)
if err != nil {
return nil, fmt.Errorf("getting values: %w", err)
}
return helm.MergeMaps(chart.Values, values), nil
}
// GetValues queries the cluster for the values of the given release.
func (c *Client) GetValues(release string) (map[string]any, error) {
client := action.NewGetValues(c.config)
// Version corresponds to the releases revision. Specifying a Version <= 0 yields the latest release.
client.Version = 0
values, err := client.Run(release)
if err != nil {
return nil, fmt.Errorf("getting values for %s: %w", release, err)
}
return values, nil
}
// ApplyCRD updates the given CRD by parsing it, querying it's version from the cluster and finally updating it.
func (c *Client) ApplyCRD(ctx context.Context, rawCRD []byte) error {
crd, err := parseCRD(rawCRD)
if err != nil {
return fmt.Errorf("parsing crds: %w", err)
}
clusterCRD, err := c.crdClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("getting crd: %w", err)
}
crd.ResourceVersion = clusterCRD.ResourceVersion
_, err = c.crdClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd, metav1.UpdateOptions{})
return err
}
// parseCRD takes a byte slice of data and tries to create a CustomResourceDefinition object from it.
func parseCRD(crdString []byte) (*v1.CustomResourceDefinition, error) {
sch := runtime.NewScheme()
_ = scheme.AddToScheme(sch)
_ = v1.AddToScheme(sch)
obj, groupVersionKind, err := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode(crdString, nil, nil)
if err != nil {
return nil, fmt.Errorf("decoding crd: %w", err)
}
if groupVersionKind.Kind == "CustomResourceDefinition" {
return obj.(*v1.CustomResourceDefinition), nil
}
return nil, errors.New("parsed []byte, but did not find a CRD")
}
// updateOperatorCRDs walks through the dependencies of the given chart and applies
// the files in the dependencie's 'crds' folder.
// This function is NOT recursive!
func (c *Client) updateOperatorCRDs(ctx context.Context, chart *chart.Chart) error {
for _, dep := range chart.Dependencies() {
for _, crdFile := range dep.Files {
if strings.HasPrefix(crdFile.Name, "crds/") {
c.log.Debugf("updating crd: %s", crdFile.Name)
err := c.ApplyCRD(ctx, crdFile.Data)
if err != nil {
return err
}
}
}
}
return nil
}
type debugLog interface {
Debugf(format string, args ...any)
Sync()
}