get.porter.sh/porter@v1.3.0/pkg/porter/list.go (about) 1 package porter 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 "time" 9 10 "get.porter.sh/porter/pkg/cnab" 11 "get.porter.sh/porter/pkg/printer" 12 "get.porter.sh/porter/pkg/secrets" 13 "get.porter.sh/porter/pkg/storage" 14 "get.porter.sh/porter/pkg/tracing" 15 dtprinter "github.com/carolynvs/datetime-printer" 16 17 "reflect" 18 ) 19 20 const ( 21 StateInstalled = "installed" 22 StateUninstalled = "uninstalled" 23 StateDefined = "defined" 24 25 StatusInstalling = "installing" 26 StatusUninstalling = "uninstalling" 27 StatusUpgrading = "upgrading" 28 ) 29 30 // ListOptions represent generic options for use by Porter's list commands 31 type ListOptions struct { 32 printer.PrintOptions 33 AllNamespaces bool 34 Namespace string 35 Name string 36 Labels []string 37 Skip int64 38 Limit int64 39 FieldSelector string 40 } 41 42 func (o *ListOptions) Validate() error { 43 return o.ParseFormat() 44 } 45 46 func (o ListOptions) GetNamespace() string { 47 if o.AllNamespaces { 48 return "*" 49 } 50 return o.Namespace 51 } 52 53 func (o ListOptions) ParseLabels() map[string]string { 54 return parseLabels(o.Labels) 55 } 56 57 func parseLabels(raw []string) map[string]string { 58 if len(raw) == 0 { 59 return nil 60 } 61 62 labelMap := make(map[string]string, len(raw)) 63 for _, label := range raw { 64 parts := strings.SplitN(label, "=", 2) 65 k := parts[0] 66 v := "" 67 if len(parts) > 1 { 68 v = parts[1] 69 } 70 labelMap[k] = v 71 } 72 return labelMap 73 } 74 75 // DisplayInstallation holds a subset of pertinent values to be listed from installation data 76 // originating from its runs, results and outputs records 77 type DisplayInstallation struct { 78 // SchemaType helps when we export the definition so editors can detect the type of document, it's not used by porter. 79 SchemaType string `json:"schemaType" yaml:"schemaType" toml:"schemaType"` 80 81 SchemaVersion cnab.SchemaVersion `json:"schemaVersion" yaml:"schemaVersion" toml:"schemaVersion"` 82 83 ID string `json:"id" yaml:"id" toml:"id"` 84 // Name of the installation. Immutable. 85 Name string `json:"name" yaml:"name" toml:"name"` 86 87 // Namespace in which the installation is defined. 88 Namespace string `json:"namespace" yaml:"namespace" toml:"namespace"` 89 90 // Uninstalled specifies if the installation isn't used anymore and should be uninstalled. 91 Uninstalled bool `json:"uninstalled,omitempty" yaml:"uninstalled,omitempty" toml:"uninstalled,omitempty"` 92 93 // Bundle specifies the bundle reference to use with the installation. 94 Bundle storage.OCIReferenceParts `json:"bundle" yaml:"bundle" toml:"bundle"` 95 96 // Custom extension data applicable to a given runtime. 97 // TODO(carolynvs): remove and populate in ToCNAB when we firm up the spec 98 Custom interface{} `json:"custom,omitempty" yaml:"custom,omitempty" toml:"custom,omitempty"` 99 100 // Labels applied to the installation. 101 Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" toml:"labels,omitempty"` 102 103 // CredentialSets that should be included when the bundle is reconciled. 104 CredentialSets []string `json:"credentialSets,omitempty" yaml:"credentialSets,omitempty" toml:"credentialSets,omitempty"` 105 106 // Parameters specified by the user through overrides. 107 // Does not include defaults, or values resolved from parameter sources. 108 Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty" toml:"parameters,omitempty"` 109 110 // ParameterSets that should be included when the bundle is reconciled. 111 ParameterSets []string `json:"parameterSets,omitempty" yaml:"parameterSets,omitempty" toml:"parameterSets,omitempty"` 112 113 // Status of the installation. 114 Status storage.InstallationStatus `json:"status,omitempty" yaml:"status,omitempty" toml:"status,omitempty"` 115 DisplayInstallationMetadata `json:"_calculated" yaml:"_calculated"` 116 } 117 118 type DisplayInstallationMetadata struct { 119 ResolvedParameters DisplayValues `json:"resolvedParameters" yaml:"resolvedParameters"` 120 121 // DisplayInstallationState is the latest state of the installation. 122 // It is either "installed", "uninstalled", or "defined". 123 DisplayInstallationState string `json:"displayInstallationState,omitempty" yaml:"displayInstallationState,omitempty" toml:"displayInstallationState,omitempty"` 124 // DisplayInstallationStatus is the latest status of the installation. 125 // It is either "succeeded, "failed", "installing", "uninstalling", "upgrading", or "running <custom action>" 126 DisplayInstallationStatus string `json:"displayInstallationStatus,omitempty" yaml:"displayInstallationStatus,omitempty" toml:"displayInstallationStatus,omitempty"` 127 } 128 129 func NewDisplayInstallation(installation storage.Installation) DisplayInstallation { 130 131 di := DisplayInstallation{ 132 SchemaType: storage.SchemaTypeInstallation, 133 SchemaVersion: installation.SchemaVersion, 134 ID: installation.ID, 135 Name: installation.Name, 136 Namespace: installation.Namespace, 137 Uninstalled: installation.Uninstalled, 138 Bundle: installation.Bundle, 139 Custom: installation.Custom, 140 Labels: installation.Labels, 141 CredentialSets: installation.CredentialSets, 142 ParameterSets: installation.ParameterSets, 143 Status: installation.Status, 144 DisplayInstallationMetadata: DisplayInstallationMetadata{ 145 DisplayInstallationState: getDisplayInstallationState(installation), 146 DisplayInstallationStatus: getDisplayInstallationStatus(installation), 147 }, 148 } 149 150 return di 151 } 152 153 // ConvertToInstallationClaim transforms the data from DisplayInstallation into 154 // a Installation record. 155 func (d DisplayInstallation) ConvertToInstallation() (storage.Installation, error) { 156 i := storage.Installation{ 157 ID: d.ID, 158 InstallationSpec: storage.InstallationSpec{ 159 SchemaVersion: d.SchemaVersion, 160 Name: d.Name, 161 Namespace: d.Namespace, 162 Uninstalled: d.Uninstalled, 163 Bundle: d.Bundle, 164 Custom: d.Custom, 165 Labels: d.Labels, 166 CredentialSets: d.CredentialSets, 167 ParameterSets: d.ParameterSets, 168 }, 169 Status: d.Status, 170 } 171 172 var err error 173 i.Parameters, err = d.ConvertParamToSet() 174 if err != nil { 175 return storage.Installation{}, err 176 } 177 178 // do not validate here, validate the converted installation right before we save it to the database 179 return i, nil 180 } 181 182 // ConvertParamToSet converts a Parameters into an internal ParameterSet. 183 func (d DisplayInstallation) ConvertParamToSet() (storage.ParameterSet, error) { 184 strategies := make([]secrets.SourceMap, 0, len(d.Parameters)) 185 for name, value := range d.Parameters { 186 stringVal, err := cnab.WriteParameterToString(name, value) 187 if err != nil { 188 return storage.ParameterSet{}, err 189 } 190 191 strategies = append(strategies, storage.ValueStrategy(name, stringVal)) 192 } 193 194 return storage.NewInternalParameterSet(d.Namespace, d.Name, strategies...), nil 195 } 196 197 // TODO(carolynvs): be consistent with sorting results from list, either keep the default sort by name 198 // or update the other types to also sort by modified 199 type DisplayInstallations []DisplayInstallation 200 201 func (l DisplayInstallations) Len() int { 202 return len(l) 203 } 204 205 func (l DisplayInstallations) Swap(i, j int) { 206 l[i], l[j] = l[j], l[i] 207 } 208 209 func (l DisplayInstallations) Less(i, j int) bool { 210 return l[i].Status.Modified.Before(l[j].Status.Modified) 211 } 212 213 type DisplayRun struct { 214 ID string `json:"id" yaml:"id"` 215 Bundle string `json:"bundle,omitempty" yaml:"bundle,omitempty"` 216 Version string `json:"version" yaml:"version"` 217 Action string `json:"action" yaml:"action"` 218 Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` 219 Started time.Time `json:"started" yaml:"started"` 220 Stopped *time.Time `json:"stopped" yaml:"stopped"` 221 Status string `json:"status" yaml:"status"` 222 } 223 224 func NewDisplayRun(run storage.Run) DisplayRun { 225 return DisplayRun{ 226 ID: run.ID, 227 Action: run.Action, 228 Parameters: run.TypedParameterValues(), 229 Started: run.Created, 230 Bundle: run.BundleReference, 231 Version: run.Bundle.Version, 232 } 233 } 234 235 // ListInstallations lists installed bundles. 236 func (p *Porter) ListInstallations(ctx context.Context, opts ListOptions) (DisplayInstallations, error) { 237 ctx, log := tracing.StartSpan(ctx) 238 defer log.EndSpan() 239 240 installations, err := p.Installations.ListInstallations(ctx, storage.ListOptions{ 241 Namespace: opts.GetNamespace(), 242 Name: opts.Name, 243 Labels: opts.ParseLabels(), 244 Skip: opts.Skip, 245 Limit: opts.Limit, 246 }) 247 if err != nil { 248 return nil, log.Error(fmt.Errorf("could not list installations: %w", err)) 249 } 250 251 var displayInstallations DisplayInstallations = DisplayInstallations{} 252 var fieldSelectorMap map[string]string 253 if opts.FieldSelector != "" { 254 fieldSelectorMap, err = parseFieldSelector(opts.FieldSelector) 255 if err != nil { 256 return nil, err 257 } 258 } 259 260 for _, installation := range installations { 261 di := NewDisplayInstallation(installation) 262 if opts.FieldSelector != "" && !doesInstallationMatchFieldSelectors(di, fieldSelectorMap) { 263 continue 264 } 265 displayInstallations = append(displayInstallations, di) 266 267 } 268 sort.Sort(sort.Reverse(displayInstallations)) 269 270 return displayInstallations, nil 271 } 272 273 // PrintInstallations prints installed bundles. 274 func (p *Porter) PrintInstallations(ctx context.Context, opts ListOptions) error { 275 displayInstallations, err := p.ListInstallations(ctx, opts) 276 if err != nil { 277 return err 278 } 279 280 switch opts.Format { 281 case printer.FormatJson: 282 return printer.PrintJson(p.Out, displayInstallations) 283 case printer.FormatYaml: 284 return printer.PrintYaml(p.Out, displayInstallations) 285 case printer.FormatPlaintext: 286 // have every row use the same "now" starting ... NOW! 287 now := time.Now() 288 tp := dtprinter.DateTimePrinter{ 289 Now: func() time.Time { return now }, 290 } 291 292 row := 293 func(v interface{}) []string { 294 cl, ok := v.(DisplayInstallation) 295 if !ok { 296 return nil 297 } 298 return []string{cl.Namespace, cl.Name, cl.Status.BundleVersion, cl.DisplayInstallationState, cl.DisplayInstallationStatus, tp.Format(cl.Status.Modified)} 299 } 300 return printer.PrintTable(p.Out, displayInstallations, row, 301 "NAMESPACE", "NAME", "VERSION", "STATE", "STATUS", "MODIFIED") 302 default: 303 return fmt.Errorf("invalid format: %s", opts.Format) 304 } 305 } 306 307 func getDisplayInstallationState(installation storage.Installation) string { 308 if installation.IsInstalled() { 309 return StateInstalled 310 } else if installation.IsUninstalled() { 311 return StateUninstalled 312 } 313 314 return StateDefined 315 } 316 317 func getDisplayInstallationStatus(installation storage.Installation) string { 318 var status string 319 320 switch installation.Status.ResultStatus { 321 case cnab.StatusSucceeded: 322 status = cnab.StatusSucceeded 323 case cnab.StatusFailed: 324 status = cnab.StatusFailed 325 case cnab.StatusRunning: 326 switch installation.Status.Action { 327 case cnab.ActionInstall: 328 status = StatusInstalling 329 case cnab.ActionUninstall: 330 status = StatusUninstalling 331 case cnab.ActionUpgrade: 332 status = StatusUpgrading 333 default: 334 status = fmt.Sprintf("running %s", installation.Status.Action) 335 } 336 } 337 338 return status 339 } 340 341 // Split the fieldSelector into a map of fields and values 342 // e.g. "bundle.version=0.2.0,status.action=install" => map[string]string{"bundle.version": "0.2.0", "status.action": "install"} 343 func parseFieldSelector(fieldSelector string) (map[string]string, error) { 344 fieldSelectorMap := make(map[string]string) 345 for _, field := range strings.Split(fieldSelector, ",") { 346 fieldParts := strings.Split(field, "=") 347 if len(fieldParts) != 2 { 348 return nil, fmt.Errorf("invalid field selector: %s", fieldSelector) 349 } 350 fieldSelectorMap[fieldParts[0]] = fieldParts[1] 351 } 352 353 return fieldSelectorMap, nil 354 } 355 356 // Check if the installation matches the field selectors 357 func doesInstallationMatchFieldSelectors(installation DisplayInstallation, fieldSelectorMap map[string]string) bool { 358 for field, value := range fieldSelectorMap { 359 if !installationHasFieldWithValue(installation, field, value) { 360 return false 361 } 362 } 363 return true 364 } 365 366 // Check if the installation has the field with the value 367 // e.g. installationHasFieldWithValue(installation, "bundle.version", "0.2.0") => true if installation.Bundle.Version (for which json tag is bunde.version) == "0.2.0" 368 func installationHasFieldWithValue(installation DisplayInstallation, fieldJsonTagPath string, value string) bool { 369 370 fieldJsonTagPathParts := strings.Split(fieldJsonTagPath, ".") 371 current := reflect.ValueOf(installation) 372 373 for _, fieldJsonTagPart := range fieldJsonTagPathParts { 374 if current.Kind() != reflect.Struct { 375 return false 376 } 377 field := getFieldByJSONTag(current, fieldJsonTagPart) 378 if !field.IsValid() { 379 return false 380 } 381 current = field 382 } 383 384 return reflect.DeepEqual(current.Interface(), value) 385 } 386 387 // Return the reflect.value based on the field's json tag 388 func getFieldByJSONTag(value reflect.Value, fieldJsonTag string) reflect.Value { 389 for i := 0; i < value.NumField(); i++ { 390 field := value.Type().Field(i) 391 392 reflectTag := field.Tag.Get("json") 393 if strings.Contains(reflectTag, ",") { 394 reflectTag = strings.Split(reflectTag, ",")[0] 395 } 396 if reflectTag == fieldJsonTag { 397 return value.Field(i) 398 } 399 } 400 return reflect.Value{} 401 }