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

     1  package storage
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/json"
     6  	"fmt"
     7  	"time"
     8  
     9  	"get.porter.sh/porter/pkg/cnab"
    10  	"github.com/cnabio/cnab-go/bundle"
    11  )
    12  
    13  var _ Document = Run{}
    14  var _ json.Marshaler = Run{}
    15  var _ json.Unmarshaler = &Run{}
    16  
    17  // Run represents the execution of an installation's bundle. It contains both the
    18  // instructions used by Porter to run the bundle, and additional status/audit
    19  // fields so users can keep track of how the bundle was run.
    20  type Run struct {
    21  	// SchemaVersion of the document.
    22  	SchemaVersion cnab.SchemaVersion `json:"schemaVersion"`
    23  
    24  	// ID of the Run.
    25  	ID string `json:"_id"`
    26  
    27  	// Created timestamp of the Run.
    28  	Created time.Time `json:"created"`
    29  
    30  	// Modified timestamp of the Run, set when we resolve run parameters just-in-time.
    31  	// A run can be created ahead of time as Pending and not have its parameters resolved until much later.
    32  	Modified time.Time `json:"modified"`
    33  
    34  	// Namespace of the installation.
    35  	Namespace string `json:"namespace"`
    36  
    37  	// Installation name.
    38  	Installation string `json:"installation"`
    39  
    40  	// Revision of the installation.
    41  	Revision string `json:"revision"`
    42  
    43  	// Action executed against the installation.
    44  	Action string `json:"action"`
    45  
    46  	// Bundle is the definition of the bundle.
    47  	// Bundle has custom marshal logic in MarshalJson.
    48  	Bundle bundle.Bundle `json:"-"`
    49  
    50  	// BundleReference is the canonical reference to the bundle used in the action.
    51  	BundleReference string `json:"bundleReference"`
    52  
    53  	// BundleDigest is the digest of the bundle.
    54  	// TODO(carolynvs): populate this
    55  	BundleDigest string `json:"bundleDigest"`
    56  
    57  	// ParameterOverrides are the key/value parameter overrides (taking precedence over
    58  	// parameters specified in a parameter set) specified during the run.
    59  	// This is a status/audit field and is not used to resolve parameters for a Run.
    60  	ParameterOverrides ParameterSet `json:"parameterOverrides,omitempty"`
    61  
    62  	// CredentialSets is a list of the credential set names used during the run.
    63  	// This is a status/audit field and is not used to resolve credentials for a Run.
    64  	CredentialSets []string `json:"credentialSets,omitempty"`
    65  
    66  	// ParameterSets is the list of parameter set names used during the run.
    67  	// This is a status/audit field and is not used to resolve parameters for a Run.
    68  	ParameterSets []string `json:"parameterSets,omitempty"`
    69  
    70  	// Parameters is the full set of parameters that should be resolved just-in-time
    71  	// (JIT) before executing the bundle. This includes internal parameters,
    72  	// parameter sources, values from parameter sets, etc. These should be a "clean"
    73  	// set of parameters that have sensitive values persisted in secrets using the
    74  	// Sanitizer.
    75  	// After the parameters are resolved, this structure holds (but does not marshal)
    76  	// the resolved values, in addition to the mapping strategy.
    77  	Parameters ParameterSet `json:"parameters,omitempty"`
    78  
    79  	// Custom extension data applicable to a given runtime.
    80  	// TODO(carolynvs): remove custom and populate it in ToCNAB
    81  	Custom interface{} `json:"custom"`
    82  
    83  	// ParametersDigest is a hash or digest of the final set of parameters, which allows us to
    84  	// quickly determine if the parameters have changed without requiring that they
    85  	// are re-resolved. The value should contain the hash type, e.g. sha256:abc123...
    86  	// This is a status/audit field and is not used to resolve parameters for a Run.
    87  	ParametersDigest string `json:"parametersDigest,omitempty"`
    88  
    89  	// Credentials is the full set of credentials that should be resolved
    90  	// just-in-time (JIT) before executing the bundle. These should be a "clean" set
    91  	// of parameters that have sensitive values persisted in secrets using the
    92  	// Sanitizer.
    93  	Credentials CredentialSet `json:"credentials,omitempty"`
    94  
    95  	// CredentialsDigest is a hash or digest of the final set of credentials, which allows us to
    96  	// quickly determine if the credentials have changed without requiring that they
    97  	// are re-resolved. The value should contain the hash type, e.g. sha256:abc123...
    98  	// This is a status/audit field and is not used to resolve credentials for a Run.
    99  	CredentialsDigest string `json:"credentialsDigest,omitempty"`
   100  }
   101  
   102  // rawRun is an alias for Run that does not have a json marshal functions defined,
   103  // so it's safe to marshal without causing infinite recursive calls.
   104  // See http://choly.ca/post/go-json-marshalling/
   105  type rawRun Run
   106  
   107  // mongoRun is the representation of the Run that we store in mongodb.
   108  type mongoRun struct {
   109  	rawRun
   110  
   111  	// Bundle is stored in mongo as a string because it has fields that are prefixed with a $, such as $id and $comment.
   112  	// It overrides Run.Bundle.
   113  	Bundle BundleDocument `json:"bundle"`
   114  }
   115  
   116  // MarshalJSON converts the run to its storage representation in mongo.
   117  func (r Run) MarshalJSON() ([]byte, error) {
   118  	data, err := json.Marshal(mongoRun{
   119  		rawRun: rawRun(r),
   120  		Bundle: BundleDocument(r.Bundle),
   121  	})
   122  	if err != nil {
   123  		return nil, fmt.Errorf("error marshaling Run into its storage representation: %w", err)
   124  	}
   125  	return data, nil
   126  }
   127  
   128  // UnmarshalJSON converts the run to its storage representation in mongo.
   129  func (r *Run) UnmarshalJSON(data []byte) error {
   130  	var mr mongoRun
   131  	if err := json.Unmarshal(data, &mr); err != nil {
   132  		return fmt.Errorf("error unmarshaling Run from its storage representation: %w", err)
   133  	}
   134  
   135  	mr.rawRun.Bundle = bundle.Bundle(mr.Bundle)
   136  	*r = Run(mr.rawRun)
   137  	return nil
   138  }
   139  
   140  func (r Run) DefaultDocumentFilter() map[string]interface{} {
   141  	return map[string]interface{}{"_id": r.ID}
   142  }
   143  
   144  // NewRun creates a run with default values initialized.
   145  func NewRun(namespace string, installation string) Run {
   146  	return Run{
   147  		SchemaVersion: DefaultInstallationSchemaVersion,
   148  		ID:            cnab.NewULID(),
   149  		Revision:      cnab.NewULID(),
   150  		Created:       time.Now(),
   151  		Modified:      time.Now(),
   152  		Namespace:     namespace,
   153  		Installation:  installation,
   154  		Parameters:    NewInternalParameterSet(namespace, installation),
   155  	}
   156  }
   157  
   158  // ShouldRecord the current run in the Installation history.
   159  // Runs are only recorded for actions that modify the bundle resources,
   160  // or for stateful actions. Stateless actions do not require an existing
   161  // installation or credentials, and are for actions such as documentation, dry-run, etc.
   162  func (r Run) ShouldRecord() bool {
   163  	// Assume all actions modify bundle resources, and should be recorded.
   164  	stateful := true
   165  	modifies := true
   166  	hasOutput := false
   167  
   168  	if action, err := r.Bundle.GetAction(r.Action); err == nil {
   169  		modifies = action.Modifies
   170  		stateful = !action.Stateless
   171  	}
   172  
   173  	bun := cnab.ExtendedBundle{Bundle: r.Bundle}
   174  	for _, outputDef := range r.Bundle.Outputs {
   175  		if outputDef.AppliesTo(r.Action) && !bun.IsInternalOutput(outputDef.Definition) {
   176  			hasOutput = true
   177  			break
   178  		}
   179  	}
   180  
   181  	return modifies || stateful || hasOutput
   182  }
   183  
   184  // ToCNAB associated with the Run.
   185  func (r Run) ToCNAB() cnab.Claim {
   186  	return cnab.Claim{
   187  		// CNAB doesn't have the concept of namespace, so we smoosh them together to make a unique name
   188  		SchemaVersion:   cnab.ClaimSchemaVersion(),
   189  		ID:              r.ID,
   190  		Installation:    r.Namespace + "/" + r.Installation,
   191  		Revision:        r.Revision,
   192  		Created:         r.Created,
   193  		Action:          r.Action,
   194  		Bundle:          r.Bundle,
   195  		BundleReference: r.BundleReference,
   196  		Parameters:      r.TypedParameterValues(),
   197  		Custom:          r.Custom,
   198  	}
   199  }
   200  
   201  // TypedParameterValues returns parameters values that have been converted to
   202  // its typed value based on its bundle definition.
   203  func (r Run) TypedParameterValues() map[string]interface{} {
   204  	bun := cnab.NewBundle(r.Bundle)
   205  	value := make(map[string]interface{})
   206  
   207  	for _, param := range r.Parameters.Parameters {
   208  		v, err := bun.ConvertParameterValue(param.Name, param.ResolvedValue)
   209  		if err != nil {
   210  			value[param.Name] = param.ResolvedValue
   211  			continue
   212  		}
   213  		def, ok := bun.Definitions[param.Name]
   214  		if !ok {
   215  			value[param.Name] = v
   216  			continue
   217  		}
   218  		if bun.IsFileType(def) && v == "" {
   219  			v = nil
   220  		}
   221  
   222  		value[param.Name] = v
   223  	}
   224  
   225  	return value
   226  
   227  }
   228  
   229  // SetParametersDigest records the hash of the resolved parameters, so we can
   230  // quickly tell if the parameters between runs were different without
   231  // re-resolving them.
   232  func (r *Run) SetParametersDigest() error {
   233  	// Calculate a hash of the resolved parameters
   234  	paramB, err := json.Marshal(r.Parameters.Parameters)
   235  	if err != nil {
   236  		r.ParametersDigest = ""
   237  		return fmt.Errorf("error calculating the digest of the run parameters: %w", err)
   238  	}
   239  
   240  	r.ParametersDigest = fmt.Sprintf("sha256:%x", sha256.Sum256(paramB))
   241  	return nil
   242  }
   243  
   244  // SetCredentialsDigest records the hash of the resolved credentials, so we can
   245  // quickly tell if the parameters between runs were different without
   246  // re-resolving them.
   247  func (r *Run) SetCredentialsDigest() error {
   248  	// Calculate a hash of the resolved credentials
   249  	credB, err := json.Marshal(r.Credentials.Credentials)
   250  	if err != nil {
   251  		r.CredentialsDigest = ""
   252  		return fmt.Errorf("error calculating the digest of the run credentials: %w", err)
   253  	}
   254  
   255  	r.CredentialsDigest = fmt.Sprintf("sha256:%x", sha256.Sum256(credB))
   256  	return nil
   257  }
   258  
   259  // NewRun creates a result for the current Run.
   260  func (r Run) NewResult(status string) Result {
   261  	result := NewResult()
   262  	result.RunID = r.ID
   263  	result.Namespace = r.Namespace
   264  	result.Installation = r.Installation
   265  	result.Status = status
   266  	return result
   267  }
   268  
   269  // NewResultFrom creates a result from the output of a CNAB run.
   270  func (r Run) NewResultFrom(cnabResult cnab.Result) Result {
   271  	return Result{
   272  		SchemaVersion:  DefaultInstallationSchemaVersion,
   273  		ID:             cnabResult.ID,
   274  		Namespace:      r.Namespace,
   275  		Installation:   r.Installation,
   276  		RunID:          r.ID,
   277  		Created:        cnabResult.Created,
   278  		Status:         cnabResult.Status,
   279  		Message:        cnabResult.Message,
   280  		OutputMetadata: cnabResult.OutputMetadata,
   281  		Custom:         cnabResult.Custom,
   282  	}
   283  }