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