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

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"get.porter.sh/porter/pkg/cnab"
    12  	"get.porter.sh/porter/pkg/schema"
    13  	"get.porter.sh/porter/pkg/secrets"
    14  	"get.porter.sh/porter/pkg/tracing"
    15  	"github.com/Masterminds/semver/v3"
    16  	"github.com/opencontainers/go-digest"
    17  	"go.opentelemetry.io/otel/attribute"
    18  	"go.opentelemetry.io/otel/trace"
    19  )
    20  
    21  var _ Document = Installation{}
    22  
    23  type Installation struct {
    24  	// ID is the unique identifier for an installation record.
    25  	ID string `json:"id"`
    26  
    27  	InstallationSpec
    28  
    29  	// Status of the installation.
    30  	Status InstallationStatus `json:"status,omitempty"`
    31  }
    32  
    33  // InstallationSpec contains installation fields that represent the desired state of the installation.
    34  type InstallationSpec struct {
    35  	// SchemaType indicates the type of resource imported from a file.
    36  	SchemaType string `json:"schemaType"`
    37  
    38  	// SchemaVersion is the version of the installation state schema.
    39  	SchemaVersion cnab.SchemaVersion `json:"schemaVersion"`
    40  
    41  	// Name of the installation. Immutable.
    42  	Name string `json:"name"`
    43  
    44  	// Namespace in which the installation is defined.
    45  	Namespace string `json:"namespace"`
    46  
    47  	// Uninstalled specifies if the installation isn't used anymore and should be uninstalled.
    48  	Uninstalled bool `json:"uninstalled,omitempty"`
    49  
    50  	// Bundle specifies the bundle reference to use with the installation.
    51  	Bundle OCIReferenceParts `json:"bundle"`
    52  
    53  	// Custom extension data applicable to a given runtime.
    54  	// TODO(carolynvs): remove and populate in ToCNAB when we firm up the spec
    55  	Custom interface{} `json:"custom,omitempty"`
    56  
    57  	// Labels applied to the installation.
    58  	Labels map[string]string `json:"labels,omitempty"`
    59  
    60  	// CredentialSets that should be included when the bundle is reconciled.
    61  	CredentialSets []string `json:"credentialSets,omitempty"`
    62  
    63  	// ParameterSets that should be included when the bundle is reconciled.
    64  	ParameterSets []string `json:"parameterSets,omitempty"`
    65  
    66  	// Parameters specified by the user through overrides.
    67  	// Does not include defaults, or values resolved from parameter sources.
    68  	Parameters ParameterSet `json:"parameters,omitempty"`
    69  }
    70  
    71  func (i InstallationSpec) String() string {
    72  	return fmt.Sprintf("%s/%s", i.Namespace, i.Name)
    73  }
    74  
    75  func (i Installation) DefaultDocumentFilter() map[string]interface{} {
    76  	return map[string]interface{}{"namespace": i.Namespace, "name": i.Name}
    77  }
    78  
    79  func NewInstallation(namespace string, name string) Installation {
    80  	now := time.Now()
    81  	return Installation{
    82  		ID: cnab.NewULID(),
    83  		InstallationSpec: InstallationSpec{
    84  			SchemaType:    SchemaTypeInstallation,
    85  			SchemaVersion: DefaultInstallationSchemaVersion,
    86  			Namespace:     namespace,
    87  			Name:          name,
    88  			Parameters:    NewInternalParameterSet(namespace, name),
    89  		},
    90  		Status: InstallationStatus{
    91  			Created:  now,
    92  			Modified: now,
    93  		},
    94  	}
    95  }
    96  
    97  // NewRun creates a run of the current bundle.
    98  func (i Installation) NewRun(action string, b cnab.ExtendedBundle) Run {
    99  	run := NewRun(i.Namespace, i.Name)
   100  	run.Action = action
   101  
   102  	// Copy over relevant overrides from the installation to the run
   103  	// An installation may have an overridden parameter that doesn't apply to this current action
   104  	run.ParameterOverrides = NewInternalParameterSet(i.Namespace, i.Name)
   105  	for _, p := range i.Parameters.Parameters {
   106  		if parmDef, ok := b.Parameters[p.Name]; ok {
   107  			if !parmDef.AppliesTo(action) {
   108  				continue
   109  			}
   110  			run.ParameterOverrides.Parameters = append(run.ParameterOverrides.Parameters, p)
   111  		}
   112  	}
   113  
   114  	return run
   115  }
   116  
   117  // ApplyResult updates cached status data on the installation from the
   118  // last bundle run.
   119  func (i *Installation) ApplyResult(run Run, result Result) {
   120  	// Update the installation with the last modifying action
   121  	if action, err := run.Bundle.GetAction(run.Action); err == nil && action.Modifies {
   122  		i.Status.BundleReference = run.BundleReference
   123  		i.Status.BundleVersion = run.Bundle.Version
   124  		i.Status.BundleDigest = run.BundleDigest
   125  		i.Status.RunID = run.ID
   126  		i.Status.Action = run.Action
   127  		i.Status.ResultID = result.ID
   128  		i.Status.ResultStatus = result.Status
   129  	}
   130  
   131  	if !i.IsInstalled() && run.Action == cnab.ActionInstall && result.Status == cnab.StatusSucceeded {
   132  		i.Status.Installed = &result.Created
   133  	}
   134  
   135  	if !i.IsUninstalled() && run.Action == cnab.ActionUninstall && result.Status == cnab.StatusSucceeded {
   136  		i.Status.Uninstalled = &result.Created
   137  	}
   138  }
   139  
   140  // Apply user-provided changes to an existing installation.
   141  // Only updates fields that users are allowed to modify.
   142  // For example, Name, Namespace and Status cannot be modified.
   143  func (i *InstallationSpec) Apply(input InstallationSpec) {
   144  	i.SchemaType = input.SchemaType
   145  	i.SchemaVersion = input.SchemaVersion
   146  	i.Uninstalled = input.Uninstalled
   147  	i.Bundle = input.Bundle
   148  	i.Parameters = input.Parameters
   149  	i.CredentialSets = input.CredentialSets
   150  	i.ParameterSets = input.ParameterSets
   151  	i.Labels = input.Labels
   152  }
   153  
   154  // Validate the installation document and report the first error.
   155  func (i *InstallationSpec) Validate(ctx context.Context, strategy schema.CheckStrategy) error {
   156  	_, span := tracing.StartSpan(ctx,
   157  		attribute.String("installation", i.String()),
   158  		attribute.String("schemaVersion", string(i.SchemaVersion)),
   159  		attribute.String("defaultSchemaVersion", string(DefaultInstallationSchemaVersion)))
   160  	defer span.EndSpan()
   161  
   162  	// Before we can validate, get our resource in a consistent state
   163  	// 1. Check if we know what to do with this version of the resource
   164  	defaultSchemaVersion := semver.MustParse(string(DefaultInstallationSchemaVersion))
   165  	if warnOnly, err := schema.ValidateSchemaVersion(strategy, SupportedInstallationSchemaVersions, string(i.SchemaVersion), defaultSchemaVersion); err != nil {
   166  		if warnOnly {
   167  			span.Warn(err.Error())
   168  		} else {
   169  			return span.Error(err)
   170  		}
   171  	}
   172  
   173  	// 2. Check if they passed in the right resource type
   174  	if i.SchemaType != "" && !strings.EqualFold(i.SchemaType, SchemaTypeInstallation) {
   175  		return span.Errorf("invalid schemaType %s, expected %s", i.SchemaType, SchemaTypeInstallation)
   176  	}
   177  
   178  	// OK! Now we can do resource specific validations
   179  
   180  	// Default the schema type before importing into the database if it's not set already
   181  	// SchemaType isn't really used by our code, it's a type hint for editors, but this will ensure we are consistent in our persisted documents
   182  	if i.SchemaType == "" {
   183  		i.SchemaType = SchemaTypeInstallation
   184  	}
   185  
   186  	// OK! Now we can do resource specific validations
   187  
   188  	// We can change these to better checks if we consolidate our logic around the various ways we let you
   189  	// install from a bundle definition https://github.com/getporter/porter/issues/1024#issuecomment-899828081
   190  	// Until then, these are pretty weak checks
   191  	_, _, err := i.Bundle.GetBundleReference()
   192  	if err != nil {
   193  		return span.Errorf("could not determine the fully-qualified bundle reference: %w", err)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // TrackBundle updates the bundle that the installation is tracking.
   200  func (i *Installation) TrackBundle(ref cnab.OCIReference) {
   201  	// Determine if the bundle is managed by version, digest or tag
   202  	i.Bundle.Repository = ref.Repository()
   203  	if ref.HasVersion() {
   204  		i.Bundle.Version = ref.Version()
   205  	} else if ref.HasDigest() {
   206  		i.Bundle.Digest = ref.Digest().String()
   207  	} else {
   208  		i.Bundle.Tag = ref.Tag()
   209  	}
   210  }
   211  
   212  // SetLabel on the installation.
   213  func (i *Installation) SetLabel(key string, value string) {
   214  	if i.Labels == nil {
   215  		i.Labels = make(map[string]string, 1)
   216  	}
   217  	i.Labels[key] = value
   218  }
   219  
   220  // NewInternalParameterSet creates a new ParameterSet that's used to store
   221  // parameter overrides with the required fields initialized.
   222  func (i Installation) NewInternalParameterSet(params ...secrets.SourceMap) ParameterSet {
   223  	return NewInternalParameterSet(i.Namespace, i.ID, params...)
   224  }
   225  
   226  func (i Installation) AddToTrace(ctx context.Context) {
   227  	span := trace.SpanFromContext(ctx)
   228  	doc, _ := json.Marshal(i)
   229  	span.SetAttributes(
   230  		attribute.String("installation", i.String()),
   231  		attribute.String("installationDefinition", string(doc)))
   232  }
   233  
   234  // InstallationStatus's purpose is to assist with making porter list be able to display everything
   235  // with a single database query. Do not replicate data available on Run and Result here.
   236  type InstallationStatus struct {
   237  	// RunID of the bundle execution that last altered the installation status.
   238  	RunID string `json:"runId" yaml:"runId" toml:"runId"`
   239  
   240  	// Action of the bundle run that last informed the installation status.
   241  	Action string `json:"action" yaml:"action" toml:"action"`
   242  
   243  	// ResultID of the result that last informed the installation status.
   244  	ResultID string `json:"resultId" yaml:"resultId" toml:"resultId"`
   245  
   246  	// ResultStatus is the status of the result that last informed the installation status.
   247  	ResultStatus string `json:"resultStatus" yaml:"resultStatus" toml:"resultStatus"`
   248  
   249  	// Created timestamp of the installation.
   250  	Created time.Time `json:"created" yaml:"created" toml:"created"`
   251  
   252  	// Modified timestamp of the installation.
   253  	Modified time.Time `json:"modified" yaml:"modified" toml:"modified"`
   254  
   255  	// Installed indicates if the install action has successfully completed for this installation.
   256  	// Once that state is reached, Porter should not allow it to be reinstalled as a protection from installations
   257  	// being overwritten.
   258  	Installed *time.Time `json:"installed" yaml:"installed" toml:"installed"`
   259  
   260  	// Uninstalled indicates if the installation has successfully completed the uninstall action.
   261  	// Once that state is reached, Porter should not allow further stateful actions.
   262  	Uninstalled *time.Time `json:"uninstalled" yaml:"uninstalled" toml:"uninstalled"`
   263  
   264  	// BundleReference of the bundle that last altered the installation state.
   265  	BundleReference string `json:"bundleReference" yaml:"bundleReference" toml:"bundleReference"`
   266  
   267  	// BundleVersion is the version of the bundle that last altered the installation state.
   268  	BundleVersion string `json:"bundleVersion" yaml:"bundleVersion" toml:"bundleVersion"`
   269  
   270  	// BundleDigest is the digest of the bundle that last altered the installation state.
   271  	BundleDigest string `json:"bundleDigest" yaml:"bundleDigest" toml:"bundleDigest"`
   272  }
   273  
   274  // IsInstalled checks if the installation is currently installed.
   275  func (i Installation) IsInstalled() bool {
   276  	if i.Status.Uninstalled != nil && i.Status.Installed != nil {
   277  		return i.Status.Installed.After(*i.Status.Uninstalled)
   278  	}
   279  	return i.Status.Uninstalled == nil && i.Status.Installed != nil
   280  }
   281  
   282  // IsUninstalled checks if the installation has been uninstalled.
   283  func (i Installation) IsUninstalled() bool {
   284  	if i.Status.Uninstalled != nil && i.Status.Installed != nil {
   285  		return i.Status.Uninstalled.After(*i.Status.Installed)
   286  	}
   287  	return i.Status.Uninstalled != nil
   288  }
   289  
   290  // IsDefined checks if the installation is has already been defined but not installed yet.
   291  func (i Installation) IsDefined() bool {
   292  	return i.Status.Installed == nil
   293  }
   294  
   295  // OCIReferenceParts is our storage representation of cnab.OCIReference
   296  // with the parts explicitly stored separately so that they are queryable.
   297  type OCIReferenceParts struct {
   298  	// Repository is the OCI repository of the bundle.
   299  	// For example, "getporter/porter-hello".
   300  	Repository string `json:"repository,omitempty" yaml:"repository,omitempty" toml:"repository,omitempty"`
   301  
   302  	// Version is the current version of the bundle.
   303  	// For example, "1.2.3".
   304  	Version string `json:"version,omitempty" yaml:"version,omitempty" toml:"version,omitempty"`
   305  
   306  	// Digest is the current digest of the bundle.
   307  	// For example, "sha256:abc123"
   308  	Digest string `json:"digest,omitempty" yaml:"digest,omitempty" toml:"digest,omitempty"`
   309  
   310  	// Tag is the OCI tag of the bundle.
   311  	// For example, "latest".
   312  	Tag string `json:"tag,omitempty" yaml:"tag,omitempty" toml:"tag,omitempty"`
   313  }
   314  
   315  func (r OCIReferenceParts) GetBundleReference() (cnab.OCIReference, bool, error) {
   316  	if r.Repository == "" {
   317  		return cnab.OCIReference{}, false, nil
   318  	}
   319  
   320  	ref, err := cnab.ParseOCIReference(r.Repository)
   321  	if err != nil {
   322  		return cnab.OCIReference{}, false, fmt.Errorf("invalid bundle Repository %s: %w", r.Repository, err)
   323  	}
   324  
   325  	if r.Digest != "" {
   326  		d, err := digest.Parse(r.Digest)
   327  		if err != nil {
   328  			return cnab.OCIReference{}, false, fmt.Errorf("invalid bundle Digest %s: %w", r.Digest, err)
   329  		}
   330  
   331  		ref, err = ref.WithDigest(d)
   332  		if err != nil {
   333  			return cnab.OCIReference{}, false, fmt.Errorf("error joining the bundle Repository %s and Digest %s: %w", r.Repository, r.Digest, err)
   334  		}
   335  		return ref, true, nil
   336  	}
   337  
   338  	if r.Version != "" {
   339  		v, err := semver.NewVersion(r.Version)
   340  		if err != nil {
   341  			return cnab.OCIReference{}, false, errors.New("invalid BundleVersion")
   342  		}
   343  
   344  		// The bundle version feature can only be used with standard naming conventions
   345  		// everyone else can use the tag field if they do weird things
   346  		ref, err = ref.WithTag("v" + v.String())
   347  		if err != nil {
   348  			return cnab.OCIReference{}, false, fmt.Errorf("error joining the bundle Repository %s and Version %s: %w", r.Repository, r.Version, err)
   349  		}
   350  		return ref, true, nil
   351  	}
   352  
   353  	if r.Tag != "" {
   354  		ref, err = ref.WithTag(r.Tag)
   355  		if err != nil {
   356  			return cnab.OCIReference{}, false, fmt.Errorf("error joining the bundle Repository %s and Tag %s: %w", r.Repository, r.Tag, err)
   357  		}
   358  		return ref, true, nil
   359  	}
   360  
   361  	return cnab.OCIReference{}, false, errors.New("Invalid bundle reference, either Digest, Version, or Tag must be specified")
   362  }