get.porter.sh/porter@v1.3.0/pkg/porter/reconcile.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  
     9  	"get.porter.sh/porter/pkg/cnab"
    10  	"get.porter.sh/porter/pkg/storage"
    11  	"get.porter.sh/porter/pkg/tracing"
    12  	"get.porter.sh/porter/pkg/yaml"
    13  	"github.com/google/go-cmp/cmp"
    14  	"go.opentelemetry.io/otel/attribute"
    15  )
    16  
    17  type ReconcileOptions struct {
    18  	Name         string
    19  	Namespace    string
    20  	Installation storage.Installation
    21  
    22  	// Just reapply the installation regardless of what has changed (or not)
    23  	Force bool
    24  
    25  	// DryRun only checks if the changes would trigger a bundle run
    26  	DryRun bool
    27  }
    28  
    29  // ReconcileInstallation compares the desired state of an installation
    30  // as stored in the installation record with the current state of the
    31  // installation. If they are not in sync, the appropriate bundle action
    32  // is executed to bring them in sync.
    33  // This is only used for install/upgrade actions triggered by applying a file
    34  // to an installation. For uninstall or invoke, you should call those directly.
    35  func (p *Porter) ReconcileInstallation(ctx context.Context, opts ReconcileOptions) error {
    36  	ctx, log := tracing.StartSpan(ctx)
    37  	defer log.EndSpan()
    38  	log.Debugf("Reconciling %s/%s installation", opts.Namespace, opts.Name)
    39  
    40  	// Get the last run of the installation, if available
    41  	var lastRun *storage.Run
    42  	r, err := p.Installations.GetLastRun(ctx, opts.Namespace, opts.Name)
    43  	neverRun := errors.Is(err, storage.ErrNotFound{})
    44  	if err != nil && !neverRun {
    45  		return err
    46  	}
    47  	if !neverRun {
    48  		lastRun = &r
    49  	}
    50  
    51  	ref, ok, err := opts.Installation.Bundle.GetBundleReference()
    52  	if err != nil {
    53  		return log.Error(err)
    54  	}
    55  	if !ok {
    56  		instYaml, _ := yaml.Marshal(opts.Installation)
    57  		return log.Error(fmt.Errorf("the installation does not define a valid bundle reference.\n%s", instYaml))
    58  	}
    59  
    60  	// Configure the bundle action that we should execute IF IT'S OUT OF SYNC
    61  	var actionOpts BundleAction
    62  	if opts.Installation.IsInstalled() {
    63  		if opts.Installation.Uninstalled {
    64  			actionOpts = NewUninstallOptions()
    65  		} else {
    66  			actionOpts = NewUpgradeOptions()
    67  		}
    68  	} else {
    69  		actionOpts = NewInstallOptions()
    70  	}
    71  
    72  	lifecycleOpts := actionOpts.GetOptions()
    73  	lifecycleOpts.Reference = ref.String()
    74  	lifecycleOpts.Name = opts.Name
    75  	lifecycleOpts.Namespace = opts.Namespace
    76  	lifecycleOpts.CredentialIdentifiers = opts.Installation.CredentialSets
    77  	lifecycleOpts.ParameterSets = opts.Installation.ParameterSets
    78  
    79  	if err = p.applyActionOptionsToInstallation(ctx, actionOpts, &opts.Installation); err != nil {
    80  		return err
    81  	}
    82  
    83  	// Determine if the installation's desired state is out of sync with reality 🤯
    84  	inSync, err := p.IsInstallationInSync(ctx, opts.Installation, lastRun, actionOpts)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	if inSync {
    90  		if opts.Force {
    91  			log.Info("The installation is up-to-date but will be re-applied because --force was specified")
    92  		} else {
    93  			log.Info("The installation is already up-to-date.")
    94  			return nil
    95  		}
    96  	}
    97  
    98  	log.Infof("The installation is out-of-sync, running the %s action...", actionOpts.GetAction())
    99  	if err := actionOpts.Validate(ctx, nil, p); err != nil {
   100  		return err
   101  	}
   102  
   103  	if opts.DryRun {
   104  		log.Info("Skipping bundle execution because --dry-run was specified")
   105  		return nil
   106  	} else {
   107  		if err = p.Installations.UpsertInstallation(ctx, opts.Installation); err != nil {
   108  			return err
   109  		}
   110  	}
   111  
   112  	return p.ExecuteAction(ctx, opts.Installation, actionOpts)
   113  }
   114  
   115  // IsInstallationInSync determines if the desired state of the installation matches
   116  // the state of the installation the last time it was modified.
   117  func (p *Porter) IsInstallationInSync(ctx context.Context, i storage.Installation, lastRun *storage.Run, action BundleAction) (bool, error) {
   118  	ctx, log := tracing.StartSpan(ctx)
   119  	defer log.EndSpan()
   120  
   121  	// Only print out info messages if we are triggering a bundle run. Otherwise, keep the explanations in debug output.
   122  
   123  	// Has it been uninstalled? If so, we don't ever reconcile it again
   124  	if i.IsUninstalled() {
   125  		log.Info("Ignoring because the installation is uninstalled")
   126  		return true, nil
   127  	}
   128  
   129  	// Should we uninstall it?
   130  	if i.Uninstalled {
   131  		// Only try to uninstall if it's been installed before
   132  		if i.IsInstalled() {
   133  			log.Info("Triggering because installation.uninstalled is true")
   134  			return false, nil
   135  		}
   136  
   137  		// Otherwise ignore this installation
   138  		log.Info("Ignoring because installation.uninstalled is true but the installation doesn't exist yet")
   139  		return true, nil
   140  	} else {
   141  		// Should we install it?
   142  		if !i.IsInstalled() {
   143  			log.Info("Triggering because the installation has not completed successfully yet")
   144  			return false, nil
   145  		}
   146  	}
   147  
   148  	// We want to upgrade, but we don't have values to compare against
   149  	// This shouldn't happen but check just in case
   150  	if lastRun == nil {
   151  		log.Info("Triggering because the last run for the installation wasn't recorded")
   152  		return false, nil
   153  	}
   154  
   155  	// Figure out if we need to upgrade
   156  	opts := action.GetOptions()
   157  
   158  	newRef, err := opts.GetBundleReference(ctx, p)
   159  	if err != nil {
   160  		return false, err
   161  	}
   162  
   163  	// Has the bundle definition changed?
   164  	if lastRun.BundleDigest != newRef.Digest.String() {
   165  		log.Info("Triggering because the bundle definition has changed",
   166  			attribute.String("oldReference", lastRun.BundleReference),
   167  			attribute.String("oldDigest", lastRun.BundleDigest),
   168  			attribute.String("newReference", newRef.Reference.String()),
   169  			attribute.String("newDigest", newRef.Digest.String()))
   170  		return false, nil
   171  	}
   172  
   173  	// Convert parameters to a string to compare them. This avoids problems comparing
   174  	// values that may be equal but have different types due to how the parameter
   175  	// value was loaded.
   176  	b := newRef.Definition
   177  	prepParametersForComparison := func(params map[string]interface{}) (map[string]string, error) {
   178  		compParams := make(map[string]string, len(params))
   179  		for paramName, rawValue := range params {
   180  			if b.IsInternalParameter(paramName) {
   181  				continue
   182  			}
   183  
   184  			typedValue, err := b.ConvertParameterValue(paramName, rawValue)
   185  			if err != nil {
   186  				return nil, err
   187  			}
   188  
   189  			stringValue, err := b.WriteParameterToString(paramName, typedValue)
   190  			if err != nil {
   191  				return nil, err
   192  			}
   193  
   194  			compParams[paramName] = stringValue
   195  		}
   196  		return compParams, nil
   197  	}
   198  
   199  	lastRunParams, err := p.Sanitizer.RestoreParameterSet(ctx, lastRun.Parameters, cnab.NewBundle(lastRun.Bundle))
   200  	if err != nil {
   201  		return false, err
   202  	}
   203  
   204  	oldParams, err := prepParametersForComparison(lastRunParams)
   205  	if err != nil {
   206  		return false, fmt.Errorf("error prepping old parameters for comparison: %w", err)
   207  	}
   208  
   209  	newParams, err := prepParametersForComparison(opts.GetParameters())
   210  	if err != nil {
   211  		return false, fmt.Errorf("error prepping current parameters for comparison: %w", err)
   212  	}
   213  
   214  	if !cmp.Equal(oldParams, newParams) {
   215  		diff := cmp.Diff(oldParams, newParams)
   216  		log.Info("Triggering because the parameters have changed",
   217  			attribute.String("diff", diff))
   218  		return false, nil
   219  	}
   220  
   221  	// Check only if the names of the associated credential sets have changed
   222  	// This is a "good enough for now" decision that can be revisited if we
   223  	// get use cases for needing to diff the actual credentials.
   224  	sort.Strings(lastRun.CredentialSets)
   225  	sort.Strings(i.CredentialSets)
   226  	if !cmp.Equal(lastRun.CredentialSets, i.CredentialSets) {
   227  		diff := cmp.Diff(lastRun.CredentialSets, i.CredentialSets)
   228  		log.Info("Triggering because the credential set names have changed",
   229  			attribute.String("diff", diff))
   230  		return false, nil
   231  	}
   232  	return true, nil
   233  }