github.com/jmrodri/operator-sdk@v0.5.0/pkg/helm/release/manager.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package release
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  
    25  	yaml "gopkg.in/yaml.v2"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	apitypes "k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions/resource"
    31  	"k8s.io/client-go/rest"
    32  	"k8s.io/helm/pkg/chartutil"
    33  	"k8s.io/helm/pkg/kube"
    34  	cpb "k8s.io/helm/pkg/proto/hapi/chart"
    35  	rpb "k8s.io/helm/pkg/proto/hapi/release"
    36  	"k8s.io/helm/pkg/proto/hapi/services"
    37  	"k8s.io/helm/pkg/storage"
    38  	"k8s.io/helm/pkg/tiller"
    39  
    40  	"github.com/mattbaird/jsonpatch"
    41  	"github.com/operator-framework/operator-sdk/pkg/helm/internal/types"
    42  )
    43  
    44  var (
    45  	// ErrNotFound indicates the release was not found.
    46  	ErrNotFound = errors.New("release not found")
    47  )
    48  
    49  // Manager manages a Helm release. It can install, update, reconcile,
    50  // and uninstall a release.
    51  type Manager interface {
    52  	ReleaseName() string
    53  	IsInstalled() bool
    54  	IsUpdateRequired() bool
    55  	Sync(context.Context) error
    56  	InstallRelease(context.Context) (*rpb.Release, error)
    57  	UpdateRelease(context.Context) (*rpb.Release, *rpb.Release, error)
    58  	ReconcileRelease(context.Context) (*rpb.Release, error)
    59  	UninstallRelease(context.Context) (*rpb.Release, error)
    60  }
    61  
    62  type manager struct {
    63  	storageBackend   *storage.Storage
    64  	tillerKubeClient *kube.Client
    65  	chartDir         string
    66  
    67  	tiller      *tiller.ReleaseServer
    68  	releaseName string
    69  	namespace   string
    70  
    71  	spec   interface{}
    72  	status *types.HelmAppStatus
    73  
    74  	isInstalled      bool
    75  	isUpdateRequired bool
    76  	deployedRelease  *rpb.Release
    77  	chart            *cpb.Chart
    78  	config           *cpb.Config
    79  }
    80  
    81  // ReleaseName returns the name of the release.
    82  func (m manager) ReleaseName() string {
    83  	return m.releaseName
    84  }
    85  
    86  func (m manager) IsInstalled() bool {
    87  	return m.isInstalled
    88  }
    89  
    90  func (m manager) IsUpdateRequired() bool {
    91  	return m.isUpdateRequired
    92  }
    93  
    94  // Sync ensures the Helm storage backend is in sync with the status of the
    95  // custom resource.
    96  func (m *manager) Sync(ctx context.Context) error {
    97  	if err := m.syncReleaseStatus(*m.status); err != nil {
    98  		return fmt.Errorf("failed to sync release status to storage backend: %s", err)
    99  	}
   100  
   101  	// Get release history for this release name
   102  	releases, err := m.storageBackend.History(m.releaseName)
   103  	if err != nil && !notFoundErr(err) {
   104  		return fmt.Errorf("failed to retrieve release history: %s", err)
   105  	}
   106  
   107  	// Cleanup non-deployed release versions. If all release versions are
   108  	// non-deployed, this will ensure that failed installations are correctly
   109  	// retried.
   110  	for _, rel := range releases {
   111  		if rel.GetInfo().GetStatus().GetCode() != rpb.Status_DEPLOYED {
   112  			_, err := m.storageBackend.Delete(rel.GetName(), rel.GetVersion())
   113  			if err != nil && !notFoundErr(err) {
   114  				return fmt.Errorf("failed to delete stale release version: %s", err)
   115  			}
   116  		}
   117  	}
   118  
   119  	// Load the chart and config based on the current state of the custom resource.
   120  	chart, config, err := m.loadChartAndConfig()
   121  	if err != nil {
   122  		return fmt.Errorf("failed to load chart and config: %s", err)
   123  	}
   124  	m.chart = chart
   125  	m.config = config
   126  
   127  	// Load the most recently deployed release from the storage backend.
   128  	deployedRelease, err := m.getDeployedRelease()
   129  	if err == ErrNotFound {
   130  		return nil
   131  	}
   132  	if err != nil {
   133  		return fmt.Errorf("failed to get deployed release: %s", err)
   134  	}
   135  	m.deployedRelease = deployedRelease
   136  	m.isInstalled = true
   137  
   138  	// Get the next candidate release to determine if an update is necessary.
   139  	candidateRelease, err := m.getCandidateRelease(ctx, m.tiller, m.releaseName, chart, config)
   140  	if err != nil {
   141  		return fmt.Errorf("failed to get candidate release: %s", err)
   142  	}
   143  	if deployedRelease.GetManifest() != candidateRelease.GetManifest() {
   144  		m.isUpdateRequired = true
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func (m manager) syncReleaseStatus(status types.HelmAppStatus) error {
   151  	var release *rpb.Release
   152  	for _, condition := range status.Conditions {
   153  		if condition.Type == types.ConditionDeployed && condition.Status == types.StatusTrue {
   154  			release = condition.Release
   155  			break
   156  		}
   157  	}
   158  	if release == nil {
   159  		return nil
   160  	}
   161  
   162  	name := release.GetName()
   163  	version := release.GetVersion()
   164  	_, err := m.storageBackend.Get(name, version)
   165  	if err == nil {
   166  		return nil
   167  	}
   168  
   169  	if !notFoundErr(err) {
   170  		return err
   171  	}
   172  	return m.storageBackend.Create(release)
   173  }
   174  
   175  func notFoundErr(err error) bool {
   176  	return strings.Contains(err.Error(), "not found")
   177  }
   178  
   179  func (m manager) loadChartAndConfig() (*cpb.Chart, *cpb.Config, error) {
   180  	// chart is mutated by the call to processRequirements,
   181  	// so we need to reload it from disk every time.
   182  	chart, err := chartutil.LoadDir(m.chartDir)
   183  	if err != nil {
   184  		return nil, nil, fmt.Errorf("failed to load chart: %s", err)
   185  	}
   186  
   187  	cr, err := yaml.Marshal(m.spec)
   188  	if err != nil {
   189  		return nil, nil, fmt.Errorf("failed to parse values: %s", err)
   190  	}
   191  	config := &cpb.Config{Raw: string(cr)}
   192  
   193  	err = processRequirements(chart, config)
   194  	if err != nil {
   195  		return nil, nil, fmt.Errorf("failed to process chart requirements: %s", err)
   196  	}
   197  	return chart, config, nil
   198  }
   199  
   200  // processRequirements will process the requirements file
   201  // It will disable/enable the charts based on condition in requirements file
   202  // Also imports the specified chart values from child to parent.
   203  func processRequirements(chart *cpb.Chart, values *cpb.Config) error {
   204  	err := chartutil.ProcessRequirementsEnabled(chart, values)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	err = chartutil.ProcessRequirementsImportValues(chart)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	return nil
   213  }
   214  
   215  func (m manager) getDeployedRelease() (*rpb.Release, error) {
   216  	deployedRelease, err := m.storageBackend.Deployed(m.releaseName)
   217  	if err != nil {
   218  		if strings.Contains(err.Error(), "has no deployed releases") {
   219  			return nil, ErrNotFound
   220  		}
   221  		return nil, err
   222  	}
   223  	return deployedRelease, nil
   224  }
   225  
   226  func (m manager) getCandidateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   227  	dryRunReq := &services.UpdateReleaseRequest{
   228  		Name:   name,
   229  		Chart:  chart,
   230  		Values: config,
   231  		DryRun: true,
   232  	}
   233  	dryRunResponse, err := tiller.UpdateRelease(ctx, dryRunReq)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	return dryRunResponse.GetRelease(), nil
   238  }
   239  
   240  // InstallRelease performs a Helm release install.
   241  func (m manager) InstallRelease(ctx context.Context) (*rpb.Release, error) {
   242  	return installRelease(ctx, m.tiller, m.namespace, m.releaseName, m.chart, m.config)
   243  }
   244  
   245  func installRelease(ctx context.Context, tiller *tiller.ReleaseServer, namespace, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   246  	installReq := &services.InstallReleaseRequest{
   247  		Namespace: namespace,
   248  		Name:      name,
   249  		Chart:     chart,
   250  		Values:    config,
   251  	}
   252  
   253  	releaseResponse, err := tiller.InstallRelease(ctx, installReq)
   254  	if err != nil {
   255  		// Workaround for helm/helm#3338
   256  		if releaseResponse.GetRelease() != nil {
   257  			uninstallReq := &services.UninstallReleaseRequest{
   258  				Name:  releaseResponse.GetRelease().GetName(),
   259  				Purge: true,
   260  			}
   261  			_, uninstallErr := tiller.UninstallRelease(ctx, uninstallReq)
   262  			if uninstallErr != nil {
   263  				return nil, fmt.Errorf("failed to roll back failed installation: %s: %s", uninstallErr, err)
   264  			}
   265  		}
   266  		return nil, err
   267  	}
   268  	return releaseResponse.GetRelease(), nil
   269  }
   270  
   271  // UpdateRelease performs a Helm release update.
   272  func (m manager) UpdateRelease(ctx context.Context) (*rpb.Release, *rpb.Release, error) {
   273  	updatedRelease, err := updateRelease(ctx, m.tiller, m.releaseName, m.chart, m.config)
   274  	return m.deployedRelease, updatedRelease, err
   275  }
   276  
   277  func updateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   278  	updateReq := &services.UpdateReleaseRequest{
   279  		Name:   name,
   280  		Chart:  chart,
   281  		Values: config,
   282  	}
   283  
   284  	releaseResponse, err := tiller.UpdateRelease(ctx, updateReq)
   285  	if err != nil {
   286  		// Workaround for helm/helm#3338
   287  		if releaseResponse.GetRelease() != nil {
   288  			rollbackReq := &services.RollbackReleaseRequest{
   289  				Name:  name,
   290  				Force: true,
   291  			}
   292  			_, rollbackErr := tiller.RollbackRelease(ctx, rollbackReq)
   293  			if rollbackErr != nil {
   294  				return nil, fmt.Errorf("failed to roll back failed update: %s: %s", rollbackErr, err)
   295  			}
   296  		}
   297  		return nil, err
   298  	}
   299  	return releaseResponse.GetRelease(), nil
   300  }
   301  
   302  // ReconcileRelease creates or patches resources as necessary to match the
   303  // deployed release's manifest.
   304  func (m manager) ReconcileRelease(ctx context.Context) (*rpb.Release, error) {
   305  	err := reconcileRelease(ctx, m.tillerKubeClient, m.namespace, m.deployedRelease.GetManifest())
   306  	return m.deployedRelease, err
   307  }
   308  
   309  func reconcileRelease(ctx context.Context, tillerKubeClient *kube.Client, namespace string, expectedManifest string) error {
   310  	expectedInfos, err := tillerKubeClient.BuildUnstructured(namespace, bytes.NewBufferString(expectedManifest))
   311  	if err != nil {
   312  		return err
   313  	}
   314  	return expectedInfos.Visit(func(expected *resource.Info, err error) error {
   315  		if err != nil {
   316  			return err
   317  		}
   318  
   319  		expectedClient := resource.NewClientWithOptions(expected.Client, func(r *rest.Request) {
   320  			*r = *r.Context(ctx)
   321  		})
   322  		helper := resource.NewHelper(expectedClient, expected.Mapping)
   323  
   324  		existing, err := helper.Get(expected.Namespace, expected.Name, false)
   325  		if apierrors.IsNotFound(err) {
   326  			if _, err := helper.Create(expected.Namespace, true, expected.Object, &metav1.CreateOptions{}); err != nil {
   327  				return fmt.Errorf("create error: %s", err)
   328  			}
   329  			return nil
   330  		} else if err != nil {
   331  			return err
   332  		}
   333  
   334  		patch, err := generatePatch(existing, expected.Object)
   335  		if err != nil {
   336  			return fmt.Errorf("failed to marshal JSON patch: %s", err)
   337  		}
   338  
   339  		if patch == nil {
   340  			return nil
   341  		}
   342  
   343  		_, err = helper.Patch(expected.Namespace, expected.Name, apitypes.JSONPatchType, patch, &metav1.UpdateOptions{})
   344  		if err != nil {
   345  			return fmt.Errorf("patch error: %s", err)
   346  		}
   347  		return nil
   348  	})
   349  }
   350  
   351  func generatePatch(existing, expected runtime.Object) ([]byte, error) {
   352  	existingJSON, err := json.Marshal(existing)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	expectedJSON, err := json.Marshal(expected)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	ops, err := jsonpatch.CreatePatch(existingJSON, expectedJSON)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	// We ignore the "remove" operations from the full patch because they are
   367  	// fields added by Kubernetes or by the user after the existing release
   368  	// resource has been applied. The goal for this patch is to make sure that
   369  	// the fields managed by the Helm chart are applied.
   370  	patchOps := make([]jsonpatch.JsonPatchOperation, 0)
   371  	for _, op := range ops {
   372  		if op.Operation != "remove" {
   373  			patchOps = append(patchOps, op)
   374  		}
   375  	}
   376  
   377  	// If there are no patch operations, return nil. Callers are expected
   378  	// to check for a nil response and skip the patch operation to avoid
   379  	// unnecessary chatter with the API server.
   380  	if len(patchOps) == 0 {
   381  		return nil, nil
   382  	}
   383  
   384  	return json.Marshal(patchOps)
   385  }
   386  
   387  // UninstallRelease performs a Helm release uninstall.
   388  func (m manager) UninstallRelease(ctx context.Context) (*rpb.Release, error) {
   389  	return uninstallRelease(ctx, m.storageBackend, m.tiller, m.releaseName)
   390  }
   391  
   392  func uninstallRelease(ctx context.Context, storageBackend *storage.Storage, tiller *tiller.ReleaseServer, releaseName string) (*rpb.Release, error) {
   393  	// Get history of this release
   394  	h, err := storageBackend.History(releaseName)
   395  	if err != nil {
   396  		return nil, fmt.Errorf("failed to get release history: %s", err)
   397  	}
   398  
   399  	// If there is no history, the release has already been uninstalled,
   400  	// so return ErrNotFound.
   401  	if len(h) == 0 {
   402  		return nil, ErrNotFound
   403  	}
   404  
   405  	uninstallResponse, err := tiller.UninstallRelease(ctx, &services.UninstallReleaseRequest{
   406  		Name:  releaseName,
   407  		Purge: true,
   408  	})
   409  	return uninstallResponse.GetRelease(), err
   410  }