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  }