github.com/mkimuram/operator-sdk@v0.7.1-0.20190410172100-52ad33a4bda0/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  	// TODO: We're now persisting releases as secrets. To support seamless upgrades, we
    98  	// need to sync the release status from the CR to the persistent storage backend.
    99  	// Once we release the storage backend migration, this function (and comment)
   100  	// can be removed.
   101  	if err := m.syncReleaseStatus(*m.status); err != nil {
   102  		return fmt.Errorf("failed to sync release status to storage backend: %s", err)
   103  	}
   104  
   105  	// Get release history for this release name
   106  	releases, err := m.storageBackend.History(m.releaseName)
   107  	if err != nil && !notFoundErr(err) {
   108  		return fmt.Errorf("failed to retrieve release history: %s", err)
   109  	}
   110  
   111  	// Cleanup non-deployed release versions. If all release versions are
   112  	// non-deployed, this will ensure that failed installations are correctly
   113  	// retried.
   114  	for _, rel := range releases {
   115  		if rel.GetInfo().GetStatus().GetCode() != rpb.Status_DEPLOYED {
   116  			_, err := m.storageBackend.Delete(rel.GetName(), rel.GetVersion())
   117  			if err != nil && !notFoundErr(err) {
   118  				return fmt.Errorf("failed to delete stale release version: %s", err)
   119  			}
   120  		}
   121  	}
   122  
   123  	// Load the chart and config based on the current state of the custom resource.
   124  	chart, config, err := m.loadChartAndConfig()
   125  	if err != nil {
   126  		return fmt.Errorf("failed to load chart and config: %s", err)
   127  	}
   128  	m.chart = chart
   129  	m.config = config
   130  
   131  	// Load the most recently deployed release from the storage backend.
   132  	deployedRelease, err := m.getDeployedRelease()
   133  	if err == ErrNotFound {
   134  		return nil
   135  	}
   136  	if err != nil {
   137  		return fmt.Errorf("failed to get deployed release: %s", err)
   138  	}
   139  	m.deployedRelease = deployedRelease
   140  	m.isInstalled = true
   141  
   142  	// Get the next candidate release to determine if an update is necessary.
   143  	candidateRelease, err := m.getCandidateRelease(ctx, m.tiller, m.releaseName, chart, config)
   144  	if err != nil {
   145  		return fmt.Errorf("failed to get candidate release: %s", err)
   146  	}
   147  	if deployedRelease.GetManifest() != candidateRelease.GetManifest() {
   148  		m.isUpdateRequired = true
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  func (m manager) syncReleaseStatus(status types.HelmAppStatus) error {
   155  	var release *rpb.Release
   156  	for _, condition := range status.Conditions {
   157  		if condition.Type == types.ConditionDeployed && condition.Status == types.StatusTrue {
   158  			release = condition.Release
   159  			break
   160  		}
   161  	}
   162  	if release == nil {
   163  		return nil
   164  	}
   165  
   166  	name := release.GetName()
   167  	version := release.GetVersion()
   168  	_, err := m.storageBackend.Get(name, version)
   169  	if err == nil {
   170  		return nil
   171  	}
   172  
   173  	if !notFoundErr(err) {
   174  		return err
   175  	}
   176  	return m.storageBackend.Create(release)
   177  }
   178  
   179  func notFoundErr(err error) bool {
   180  	return strings.Contains(err.Error(), "not found")
   181  }
   182  
   183  func (m manager) loadChartAndConfig() (*cpb.Chart, *cpb.Config, error) {
   184  	// chart is mutated by the call to processRequirements,
   185  	// so we need to reload it from disk every time.
   186  	chart, err := chartutil.LoadDir(m.chartDir)
   187  	if err != nil {
   188  		return nil, nil, fmt.Errorf("failed to load chart: %s", err)
   189  	}
   190  
   191  	cr, err := yaml.Marshal(m.spec)
   192  	if err != nil {
   193  		return nil, nil, fmt.Errorf("failed to parse values: %s", err)
   194  	}
   195  	config := &cpb.Config{Raw: string(cr)}
   196  
   197  	err = processRequirements(chart, config)
   198  	if err != nil {
   199  		return nil, nil, fmt.Errorf("failed to process chart requirements: %s", err)
   200  	}
   201  	return chart, config, nil
   202  }
   203  
   204  // processRequirements will process the requirements file
   205  // It will disable/enable the charts based on condition in requirements file
   206  // Also imports the specified chart values from child to parent.
   207  func processRequirements(chart *cpb.Chart, values *cpb.Config) error {
   208  	err := chartutil.ProcessRequirementsEnabled(chart, values)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	err = chartutil.ProcessRequirementsImportValues(chart)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	return nil
   217  }
   218  
   219  func (m manager) getDeployedRelease() (*rpb.Release, error) {
   220  	deployedRelease, err := m.storageBackend.Deployed(m.releaseName)
   221  	if err != nil {
   222  		if strings.Contains(err.Error(), "has no deployed releases") {
   223  			return nil, ErrNotFound
   224  		}
   225  		return nil, err
   226  	}
   227  	return deployedRelease, nil
   228  }
   229  
   230  func (m manager) getCandidateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   231  	dryRunReq := &services.UpdateReleaseRequest{
   232  		Name:   name,
   233  		Chart:  chart,
   234  		Values: config,
   235  		DryRun: true,
   236  	}
   237  	dryRunResponse, err := tiller.UpdateRelease(ctx, dryRunReq)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	return dryRunResponse.GetRelease(), nil
   242  }
   243  
   244  // InstallRelease performs a Helm release install.
   245  func (m manager) InstallRelease(ctx context.Context) (*rpb.Release, error) {
   246  	return installRelease(ctx, m.tiller, m.namespace, m.releaseName, m.chart, m.config)
   247  }
   248  
   249  func installRelease(ctx context.Context, tiller *tiller.ReleaseServer, namespace, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   250  	installReq := &services.InstallReleaseRequest{
   251  		Namespace: namespace,
   252  		Name:      name,
   253  		Chart:     chart,
   254  		Values:    config,
   255  	}
   256  
   257  	releaseResponse, err := tiller.InstallRelease(ctx, installReq)
   258  	if err != nil {
   259  		// Workaround for helm/helm#3338
   260  		if releaseResponse.GetRelease() != nil {
   261  			uninstallReq := &services.UninstallReleaseRequest{
   262  				Name:  releaseResponse.GetRelease().GetName(),
   263  				Purge: true,
   264  			}
   265  			_, uninstallErr := tiller.UninstallRelease(ctx, uninstallReq)
   266  			if uninstallErr != nil {
   267  				return nil, fmt.Errorf("failed to roll back failed installation: %s: %s", uninstallErr, err)
   268  			}
   269  		}
   270  		return nil, err
   271  	}
   272  	return releaseResponse.GetRelease(), nil
   273  }
   274  
   275  // UpdateRelease performs a Helm release update.
   276  func (m manager) UpdateRelease(ctx context.Context) (*rpb.Release, *rpb.Release, error) {
   277  	updatedRelease, err := updateRelease(ctx, m.tiller, m.releaseName, m.chart, m.config)
   278  	return m.deployedRelease, updatedRelease, err
   279  }
   280  
   281  func updateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) {
   282  	updateReq := &services.UpdateReleaseRequest{
   283  		Name:   name,
   284  		Chart:  chart,
   285  		Values: config,
   286  	}
   287  
   288  	releaseResponse, err := tiller.UpdateRelease(ctx, updateReq)
   289  	if err != nil {
   290  		// Workaround for helm/helm#3338
   291  		if releaseResponse.GetRelease() != nil {
   292  			rollbackReq := &services.RollbackReleaseRequest{
   293  				Name:  name,
   294  				Force: true,
   295  			}
   296  			_, rollbackErr := tiller.RollbackRelease(ctx, rollbackReq)
   297  			if rollbackErr != nil {
   298  				return nil, fmt.Errorf("failed to roll back failed update: %s: %s", rollbackErr, err)
   299  			}
   300  		}
   301  		return nil, err
   302  	}
   303  	return releaseResponse.GetRelease(), nil
   304  }
   305  
   306  // ReconcileRelease creates or patches resources as necessary to match the
   307  // deployed release's manifest.
   308  func (m manager) ReconcileRelease(ctx context.Context) (*rpb.Release, error) {
   309  	err := reconcileRelease(ctx, m.tillerKubeClient, m.namespace, m.deployedRelease.GetManifest())
   310  	return m.deployedRelease, err
   311  }
   312  
   313  func reconcileRelease(ctx context.Context, tillerKubeClient *kube.Client, namespace string, expectedManifest string) error {
   314  	expectedInfos, err := tillerKubeClient.BuildUnstructured(namespace, bytes.NewBufferString(expectedManifest))
   315  	if err != nil {
   316  		return err
   317  	}
   318  	return expectedInfos.Visit(func(expected *resource.Info, err error) error {
   319  		if err != nil {
   320  			return err
   321  		}
   322  
   323  		expectedClient := resource.NewClientWithOptions(expected.Client, func(r *rest.Request) {
   324  			*r = *r.Context(ctx)
   325  		})
   326  		helper := resource.NewHelper(expectedClient, expected.Mapping)
   327  
   328  		existing, err := helper.Get(expected.Namespace, expected.Name, false)
   329  		if apierrors.IsNotFound(err) {
   330  			if _, err := helper.Create(expected.Namespace, true, expected.Object, &metav1.CreateOptions{}); err != nil {
   331  				return fmt.Errorf("create error: %s", err)
   332  			}
   333  			return nil
   334  		} else if err != nil {
   335  			return err
   336  		}
   337  
   338  		patch, err := generatePatch(existing, expected.Object)
   339  		if err != nil {
   340  			return fmt.Errorf("failed to marshal JSON patch: %s", err)
   341  		}
   342  
   343  		if patch == nil {
   344  			return nil
   345  		}
   346  
   347  		_, err = helper.Patch(expected.Namespace, expected.Name, apitypes.JSONPatchType, patch, &metav1.UpdateOptions{})
   348  		if err != nil {
   349  			return fmt.Errorf("patch error: %s", err)
   350  		}
   351  		return nil
   352  	})
   353  }
   354  
   355  func generatePatch(existing, expected runtime.Object) ([]byte, error) {
   356  	existingJSON, err := json.Marshal(existing)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	expectedJSON, err := json.Marshal(expected)
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	ops, err := jsonpatch.CreatePatch(existingJSON, expectedJSON)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	// We ignore the "remove" operations from the full patch because they are
   371  	// fields added by Kubernetes or by the user after the existing release
   372  	// resource has been applied. The goal for this patch is to make sure that
   373  	// the fields managed by the Helm chart are applied.
   374  	patchOps := make([]jsonpatch.JsonPatchOperation, 0)
   375  	for _, op := range ops {
   376  		if op.Operation != "remove" {
   377  			patchOps = append(patchOps, op)
   378  		}
   379  	}
   380  
   381  	// If there are no patch operations, return nil. Callers are expected
   382  	// to check for a nil response and skip the patch operation to avoid
   383  	// unnecessary chatter with the API server.
   384  	if len(patchOps) == 0 {
   385  		return nil, nil
   386  	}
   387  
   388  	return json.Marshal(patchOps)
   389  }
   390  
   391  // UninstallRelease performs a Helm release uninstall.
   392  func (m manager) UninstallRelease(ctx context.Context) (*rpb.Release, error) {
   393  	return uninstallRelease(ctx, m.storageBackend, m.tiller, m.releaseName)
   394  }
   395  
   396  func uninstallRelease(ctx context.Context, storageBackend *storage.Storage, tiller *tiller.ReleaseServer, releaseName string) (*rpb.Release, error) {
   397  	// Get history of this release
   398  	h, err := storageBackend.History(releaseName)
   399  	if err != nil {
   400  		return nil, fmt.Errorf("failed to get release history: %s", err)
   401  	}
   402  
   403  	// If there is no history, the release has already been uninstalled,
   404  	// so return ErrNotFound.
   405  	if len(h) == 0 {
   406  		return nil, ErrNotFound
   407  	}
   408  
   409  	uninstallResponse, err := tiller.UninstallRelease(ctx, &services.UninstallReleaseRequest{
   410  		Name:  releaseName,
   411  		Purge: true,
   412  	})
   413  	return uninstallResponse.GetRelease(), err
   414  }