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 }