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