github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/operators/cli/clioperator.go (about)

     1  // Copyright 2024 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package clioperator
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  
    22  	"sigs.k8s.io/yaml"
    23  
    24  	"github.com/inspektor-gadget/inspektor-gadget/pkg/datasource"
    25  	"github.com/inspektor-gadget/inspektor-gadget/pkg/datasource/formatters/json"
    26  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api"
    27  	apihelpers "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api-helpers"
    28  	"github.com/inspektor-gadget/inspektor-gadget/pkg/operators"
    29  	"github.com/inspektor-gadget/inspektor-gadget/pkg/params"
    30  )
    31  
    32  const (
    33  	// Priority is set to a high value, since this operator is used as sink and so all changes to DataSources need
    34  	// to have happened before the operator becomes active
    35  	Priority = 10000
    36  
    37  	ParamFields = "fields"
    38  	ParamMode   = "output"
    39  
    40  	ModeJSON       = "json"
    41  	ModeJSONPretty = "jsonpretty"
    42  	ModeColumns    = "columns"
    43  	ModeYAML       = "yaml"
    44  )
    45  
    46  type cliOperator struct{}
    47  
    48  func (o *cliOperator) Name() string {
    49  	return "cli"
    50  }
    51  
    52  func (o *cliOperator) Init(params *params.Params) error {
    53  	return nil
    54  }
    55  
    56  func (o *cliOperator) GlobalParams() api.Params {
    57  	return nil
    58  }
    59  
    60  func (o *cliOperator) InstanceParams() api.Params {
    61  	return nil
    62  }
    63  
    64  func (o *cliOperator) InstantiateDataOperator(gadgetCtx operators.GadgetContext, paramValues api.ParamValues) (operators.DataOperatorInstance, error) {
    65  	op := &cliOperatorInstance{
    66  		mode:        ModeColumns,
    67  		paramValues: paramValues,
    68  	}
    69  
    70  	return op, nil
    71  }
    72  
    73  func (o *cliOperator) Priority() int {
    74  	return Priority
    75  }
    76  
    77  type cliOperatorInstance struct {
    78  	mode        string
    79  	paramValues api.ParamValues
    80  }
    81  
    82  func (o *cliOperatorInstance) Name() string {
    83  	return "cli"
    84  }
    85  
    86  func (o *cliOperatorInstance) InstanceParams() params.ParamDescs {
    87  	return nil
    88  }
    89  
    90  func getNamesFromFields(fields []*api.Field) []string {
    91  	res := make([]string, 0, len(fields))
    92  	for _, f := range fields {
    93  		res = append(res, f.FullName)
    94  	}
    95  	return res
    96  }
    97  
    98  func (o *cliOperatorInstance) ExtraParams(gadgetCtx operators.GadgetContext) api.Params {
    99  	dataSources := gadgetCtx.GetDataSources()
   100  
   101  	nameDS := false
   102  
   103  	// if we have multiple DataSources, we need to prefix the list of fields with the DataSource's name
   104  	if len(dataSources) > 1 {
   105  		nameDS = true
   106  	}
   107  
   108  	fieldsDefaultValues := make([]string, 0, len(dataSources))
   109  	fieldsDescriptions := make([]string, 0, len(dataSources)+1)
   110  	fieldsDescriptions = append(fieldsDescriptions, "Available data sources / fields")
   111  	for _, ds := range dataSources {
   112  		fields := ds.Fields()
   113  		availableFields := make([]*api.Field, 0, len(fields))
   114  		defaultFields := make([]*api.Field, 0)
   115  		for _, f := range fields {
   116  			if datasource.FieldFlagUnreferenced.In(f.Flags) ||
   117  				datasource.FieldFlagContainer.In(f.Flags) ||
   118  				datasource.FieldFlagEmpty.In(f.Flags) {
   119  				continue
   120  			}
   121  			availableFields = append(availableFields, f)
   122  			if datasource.FieldFlagHidden.In(f.Flags) {
   123  				continue
   124  			}
   125  			defaultFields = append(defaultFields, f)
   126  		}
   127  
   128  		// Sort available fields by name
   129  		sort.Slice(availableFields, func(i, j int) bool {
   130  			return availableFields[i].FullName < availableFields[j].FullName
   131  		})
   132  
   133  		// Sort default fields by order value
   134  		sort.SliceStable(defaultFields, func(i, j int) bool {
   135  			return defaultFields[i].Order < defaultFields[j].Order
   136  		})
   137  
   138  		fieldsDefaultValue := strings.Join(getNamesFromFields(defaultFields), ",")
   139  		if nameDS {
   140  			fieldsDefaultValue = ds.Name() + ":" + fieldsDefaultValue
   141  		}
   142  
   143  		fieldsDefaultValues = append(fieldsDefaultValues, fieldsDefaultValue)
   144  
   145  		var sb strings.Builder
   146  		fmt.Fprintf(&sb, "  %q (data source):\n", ds.Name())
   147  		for _, f := range availableFields {
   148  			fmt.Fprintf(&sb, "    %s\n", f.FullName)
   149  			if desc, ok := f.Annotations["description"]; ok { // TODO: const to API?
   150  				fmt.Fprintf(&sb, "      %s\n", desc)
   151  			}
   152  		}
   153  		fieldsDescriptions = append(fieldsDescriptions, sb.String())
   154  	}
   155  
   156  	// --fields datasource:comma,separated,fields;datasource2:comma,separated,fields
   157  	fields := &api.Param{
   158  		Key:          ParamFields,
   159  		DefaultValue: strings.Join(fieldsDefaultValues, ";"),
   160  		Description:  strings.Join(fieldsDescriptions, "\n"),
   161  	}
   162  
   163  	mode := &api.Param{
   164  		Key:            ParamMode,
   165  		DefaultValue:   ModeColumns,
   166  		Description:    "output mode",
   167  		Alias:          "o",
   168  		PossibleValues: []string{ModeJSON, ModeJSONPretty, ModeColumns, ModeYAML},
   169  	}
   170  
   171  	return api.Params{fields, mode}
   172  }
   173  
   174  func (o *cliOperatorInstance) PreStart(gadgetCtx operators.GadgetContext) error {
   175  	params := apihelpers.ToParamDescs(o.ExtraParams(gadgetCtx)).ToParams()
   176  	params.CopyFromMap(o.paramValues, "")
   177  
   178  	fieldValues := strings.Split(params.Get(ParamFields).AsString(), ";")
   179  	fieldLookup := make(map[string]string)
   180  	for _, v := range fieldValues {
   181  		dsFieldValues := strings.SplitN(v, ":", 2)
   182  		var dsName string
   183  		dsFields := dsFieldValues[0]
   184  		if len(dsFieldValues) == 2 {
   185  			dsName = dsFieldValues[0]
   186  			dsFields = dsFieldValues[1]
   187  		}
   188  		fieldLookup[dsName] = dsFields
   189  	}
   190  
   191  	o.mode = params.Get(ParamMode).AsString()
   192  
   193  	for _, ds := range gadgetCtx.GetDataSources() {
   194  		gadgetCtx.Logger().Debugf("subscribing to %s", ds.Name())
   195  
   196  		fields, hasFields := fieldLookup[ds.Name()]
   197  		if !hasFields {
   198  			fields, hasFields = fieldLookup[""] // fall back to default
   199  		}
   200  
   201  		switch o.mode {
   202  		case ModeColumns:
   203  			p, err := ds.Parser()
   204  			if err != nil {
   205  				gadgetCtx.Logger().Debugf("failed to get parser: %v", err)
   206  				continue
   207  			}
   208  
   209  			defCols := p.GetDefaultColumns()
   210  			gadgetCtx.Logger().Debugf("default fields: %s", defCols)
   211  			formatter := p.GetTextColumnsFormatter()
   212  
   213  			if hasFields {
   214  				err := formatter.SetShowColumns(strings.Split(fields, ","))
   215  				if err != nil {
   216  					return fmt.Errorf("setting fields: %w", err)
   217  				}
   218  			}
   219  
   220  			formatter.SetEventCallback(func(s string) {
   221  				fmt.Print(s)
   222  				fmt.Print("\n")
   223  			})
   224  
   225  			p.SetEventCallback(formatter.EventHandlerFunc())
   226  			handler, ok := p.EventHandlerFunc().(func(data *datasource.DataTuple))
   227  			if !ok {
   228  				gadgetCtx.Logger().Warnf("invalid data format: expected func(data *datasource.DataTuple), got %T",
   229  					p.EventHandlerFunc())
   230  				continue
   231  			}
   232  
   233  			fmt.Println(formatter.FormatHeader())
   234  
   235  			ds.Subscribe(func(ds datasource.DataSource, data datasource.Data) error {
   236  				handler(datasource.NewDataTuple(ds, data))
   237  				return nil
   238  			}, Priority)
   239  		case ModeJSON, ModeJSONPretty, ModeYAML:
   240  			// var opts []json.Option
   241  			// if hasFields {
   242  			// 	opts = append(opts, json.WithFields(strings.Split(fields, ",")))
   243  			// }
   244  
   245  			jsonFormatter, err := json.New(ds,
   246  				// TODO: compatiblity for now: add all; remove me later on and use the commented version above
   247  				json.WithShowAll(true),
   248  				json.WithPretty(o.mode == ModeJSONPretty, "  "),
   249  			)
   250  			if err != nil {
   251  				return fmt.Errorf("initializing JSON formatter: %w", err)
   252  			}
   253  
   254  			df := func(ds datasource.DataSource, data datasource.Data) error {
   255  				fmt.Println(string(jsonFormatter.Marshal(data)))
   256  				return nil
   257  			}
   258  
   259  			if o.mode == ModeYAML {
   260  				// For the time being, this uses a slow approach to marshal to YAML, by first
   261  				// converting to JSON and then to YAML. This should get a dedicated formatter sooner or later.
   262  				df = func(ds datasource.DataSource, data datasource.Data) error {
   263  					yml, err := yaml.JSONToYAML(jsonFormatter.Marshal(data))
   264  					if err != nil {
   265  						return fmt.Errorf("serializing yaml: %w", err)
   266  					}
   267  					fmt.Println("---")
   268  					fmt.Print(string(yml))
   269  					return nil
   270  				}
   271  			}
   272  			ds.Subscribe(df, Priority)
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  func (o *cliOperatorInstance) Start(gadgetCtx operators.GadgetContext) error {
   279  	return nil
   280  }
   281  
   282  func (o *cliOperatorInstance) Stop(gadgetCtx operators.GadgetContext) error {
   283  	return nil
   284  }
   285  
   286  var CLIOperator = &cliOperator{}