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 }