istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/admin/istiodconfig.go (about)

     1  // Copyright Istio 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 admin
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/url"
    26  	"regexp"
    27  	"sort"
    28  	"strings"
    29  	"sync"
    30  	"text/tabwriter"
    31  
    32  	"github.com/spf13/cobra"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	"istio.io/api/label"
    36  	"istio.io/istio/istioctl/pkg/cli"
    37  	"istio.io/istio/istioctl/pkg/clioptions"
    38  	"istio.io/istio/istioctl/pkg/completion"
    39  	"istio.io/istio/pkg/log"
    40  )
    41  
    42  type flagState interface {
    43  	run(out io.Writer) error
    44  }
    45  
    46  var (
    47  	_ flagState = (*resetState)(nil)
    48  	_ flagState = (*logLevelState)(nil)
    49  	_ flagState = (*stackTraceLevelState)(nil)
    50  	_ flagState = (*getAllLogLevelsState)(nil)
    51  )
    52  
    53  type resetState struct {
    54  	client *ControlzClient
    55  }
    56  
    57  func (rs *resetState) run(_ io.Writer) error {
    58  	const (
    59  		defaultOutputLevel     = "info"
    60  		defaultStackTraceLevel = "none"
    61  	)
    62  	allScopes, err := rs.client.GetScopes()
    63  	if err != nil {
    64  		return fmt.Errorf("could not get all scopes: %v", err)
    65  	}
    66  	var defaultScopes []*ScopeInfo
    67  	for _, scope := range allScopes {
    68  		defaultScopes = append(defaultScopes, &ScopeInfo{
    69  			Name:            scope.Name,
    70  			OutputLevel:     defaultOutputLevel,
    71  			StackTraceLevel: defaultStackTraceLevel,
    72  		})
    73  	}
    74  	err = rs.client.PutScopes(defaultScopes)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  type logLevelState struct {
    83  	client         *ControlzClient
    84  	outputLogLevel string
    85  }
    86  
    87  func (ll *logLevelState) run(_ io.Writer) error {
    88  	scopeInfos, err := newScopeInfosFromScopeLevelPairs(ll.outputLogLevel)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	err = ll.client.PutScopes(scopeInfos)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	return nil
    97  }
    98  
    99  type stackTraceLevelState struct {
   100  	client          *ControlzClient
   101  	stackTraceLevel string
   102  }
   103  
   104  func (stl *stackTraceLevelState) run(_ io.Writer) error {
   105  	scopeInfos, err := newScopeInfosFromScopeStackTraceLevelPairs(stl.stackTraceLevel)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	err = stl.client.PutScopes(scopeInfos)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	return nil
   114  }
   115  
   116  type getAllLogLevelsState struct {
   117  	client       *ControlzClient
   118  	outputFormat string
   119  }
   120  
   121  func (ga *getAllLogLevelsState) run(out io.Writer) error {
   122  	type scopeLogLevel struct {
   123  		ScopeName   string `json:"scope_name"`
   124  		LogLevel    string `json:"log_level"`
   125  		Description string `json:"description"`
   126  	}
   127  	allScopes, err := ga.client.GetScopes()
   128  	sort.Slice(allScopes, func(i, j int) bool {
   129  		return allScopes[i].Name < allScopes[j].Name
   130  	})
   131  	if err != nil {
   132  		return fmt.Errorf("could not get scopes information: %v", err)
   133  	}
   134  	var resultScopeLogLevel []*scopeLogLevel
   135  	for _, scope := range allScopes {
   136  		resultScopeLogLevel = append(resultScopeLogLevel,
   137  			&scopeLogLevel{
   138  				ScopeName:   scope.Name,
   139  				LogLevel:    scope.OutputLevel,
   140  				Description: scope.Description,
   141  			},
   142  		)
   143  	}
   144  	switch ga.outputFormat {
   145  	case "short":
   146  		w := new(tabwriter.Writer).Init(out, 0, 8, 3, ' ', 0)
   147  		_, _ = fmt.Fprintln(w, "ACTIVE SCOPE\tDESCRIPTION\tLOG LEVEL")
   148  		for _, sll := range resultScopeLogLevel {
   149  			_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", sll.ScopeName, sll.Description, sll.LogLevel)
   150  		}
   151  		return w.Flush()
   152  	case "json", "yaml":
   153  		outputBytes, err := json.MarshalIndent(&resultScopeLogLevel, "", "  ")
   154  		outputBytes = append(outputBytes, []byte("\n")...)
   155  		if err != nil {
   156  			return err
   157  		}
   158  		if ga.outputFormat == "yaml" {
   159  			if outputBytes, err = yaml.JSONToYAML(outputBytes); err != nil {
   160  				return err
   161  			}
   162  		}
   163  		_, err = out.Write(outputBytes)
   164  		return err
   165  	default:
   166  		return fmt.Errorf("output format %q not supported", ga.outputFormat)
   167  	}
   168  }
   169  
   170  type istiodConfigLog struct {
   171  	state flagState
   172  }
   173  
   174  func (id *istiodConfigLog) execute(out io.Writer) error {
   175  	return id.state.run(out)
   176  }
   177  
   178  func chooseClientFlag(ctrzClient *ControlzClient, reset bool, outputLogLevel, stackTraceLevel, outputFormat string) *istiodConfigLog {
   179  	if reset {
   180  		return &istiodConfigLog{state: &resetState{ctrzClient}}
   181  	} else if outputLogLevel != "" {
   182  		return &istiodConfigLog{state: &logLevelState{
   183  			client:         ctrzClient,
   184  			outputLogLevel: outputLogLevel,
   185  		}}
   186  	} else if stackTraceLevel != "" {
   187  		return &istiodConfigLog{state: &stackTraceLevelState{
   188  			client:          ctrzClient,
   189  			stackTraceLevel: stackTraceLevel,
   190  		}}
   191  	}
   192  	return &istiodConfigLog{state: &getAllLogLevelsState{
   193  		client:       ctrzClient,
   194  		outputFormat: outputFormat,
   195  	}}
   196  }
   197  
   198  type ScopeInfo struct {
   199  	Name            string `json:"name"`
   200  	Description     string `json:"description,omitempty"`
   201  	OutputLevel     string `json:"output_level,omitempty"`
   202  	StackTraceLevel string `json:"stack_trace_level,omitempty"`
   203  	LogCallers      bool   `json:"log_callers,omitempty"`
   204  }
   205  
   206  type ScopeLevelPair struct {
   207  	scope    string
   208  	logLevel string
   209  }
   210  
   211  type scopeStackTraceLevelPair ScopeLevelPair
   212  
   213  func newScopeLevelPair(slp, validationPattern string) (*ScopeLevelPair, error) {
   214  	matched, err := regexp.MatchString(validationPattern, slp)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	if !matched {
   219  		return nil, fmt.Errorf("pattern %s did not match", slp)
   220  	}
   221  	scopeLogLevel := strings.Split(slp, ":")
   222  	s := &ScopeLevelPair{
   223  		scope:    scopeLogLevel[0],
   224  		logLevel: scopeLogLevel[1],
   225  	}
   226  	return s, nil
   227  }
   228  
   229  func newScopeInfosFromScopeLevelPairs(scopeLevelPairs string) ([]*ScopeInfo, error) {
   230  	slParis := strings.Split(scopeLevelPairs, ",")
   231  	var scopeInfos []*ScopeInfo
   232  	for _, slp := range slParis {
   233  		sl, err := newScopeLevelPair(slp, validationPattern)
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  		si := &ScopeInfo{
   238  			Name:        sl.scope,
   239  			OutputLevel: sl.logLevel,
   240  		}
   241  		scopeInfos = append(scopeInfos, si)
   242  	}
   243  	return scopeInfos, nil
   244  }
   245  
   246  func newScopeStackTraceLevelPair(sslp, validationPattern string) (*scopeStackTraceLevelPair, error) {
   247  	matched, err := regexp.MatchString(validationPattern, sslp)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	if !matched {
   252  		return nil, fmt.Errorf("pattern %s did not match", sslp)
   253  	}
   254  	scopeStackTraceLevel := strings.Split(sslp, ":")
   255  	ss := &scopeStackTraceLevelPair{
   256  		scope:    scopeStackTraceLevel[0],
   257  		logLevel: scopeStackTraceLevel[1],
   258  	}
   259  	return ss, nil
   260  }
   261  
   262  func newScopeInfosFromScopeStackTraceLevelPairs(scopeStackTraceLevelPairs string) ([]*ScopeInfo, error) {
   263  	sslPairs := strings.Split(scopeStackTraceLevelPairs, ",")
   264  	var scopeInfos []*ScopeInfo
   265  	for _, sslp := range sslPairs {
   266  		slp, err := newScopeStackTraceLevelPair(sslp, validationPattern)
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		si := &ScopeInfo{
   271  			Name:            slp.scope,
   272  			StackTraceLevel: slp.logLevel,
   273  		}
   274  		scopeInfos = append(scopeInfos, si)
   275  	}
   276  	return scopeInfos, nil
   277  }
   278  
   279  type ControlzClient struct {
   280  	baseURL    *url.URL
   281  	httpClient *http.Client
   282  }
   283  
   284  func (c *ControlzClient) GetScopes() ([]*ScopeInfo, error) {
   285  	var scopeInfos []*ScopeInfo
   286  	resp, err := c.httpClient.Get(c.baseURL.String())
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	defer resp.Body.Close()
   291  	if resp.StatusCode != http.StatusOK {
   292  		return nil, fmt.Errorf("request not successful %s", resp.Status)
   293  	}
   294  
   295  	err = json.NewDecoder(resp.Body).Decode(&scopeInfos)
   296  	if err != nil {
   297  		return nil, fmt.Errorf("cannot deserialize response %s", err)
   298  	}
   299  	return scopeInfos, nil
   300  }
   301  
   302  func (c *ControlzClient) PutScope(scope *ScopeInfo) error {
   303  	var jsonScopeInfo bytes.Buffer
   304  	err := json.NewEncoder(&jsonScopeInfo).Encode(scope)
   305  	if err != nil {
   306  		return fmt.Errorf("cannot serialize scope %+v", *scope)
   307  	}
   308  	req, err := http.NewRequest(http.MethodPut, c.baseURL.String()+"/"+scope.Name, &jsonScopeInfo)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	defer req.Body.Close()
   313  
   314  	resp, err := c.httpClient.Do(req)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	defer resp.Body.Close()
   319  
   320  	if resp.StatusCode != http.StatusAccepted {
   321  		return fmt.Errorf("cannot update resource %s, got status %s", scope.Name, resp.Status)
   322  	}
   323  	return nil
   324  }
   325  
   326  func (c *ControlzClient) PutScopes(scopes []*ScopeInfo) error {
   327  	ch := make(chan struct {
   328  		err       error
   329  		scopeName string
   330  	}, len(scopes))
   331  	var wg sync.WaitGroup
   332  	for _, scope := range scopes {
   333  		wg.Add(1)
   334  		go func(si *ScopeInfo) {
   335  			defer wg.Done()
   336  			err := c.PutScope(si)
   337  			ch <- struct {
   338  				err       error
   339  				scopeName string
   340  			}{err: err, scopeName: si.Name}
   341  		}(scope)
   342  	}
   343  	wg.Wait()
   344  	close(ch)
   345  	for result := range ch {
   346  		if result.err != nil {
   347  			return fmt.Errorf("failed updating Scope %s: %v", result.scopeName, result.err)
   348  		}
   349  	}
   350  	return nil
   351  }
   352  
   353  func (c *ControlzClient) GetScope(scope string) (*ScopeInfo, error) {
   354  	var s ScopeInfo
   355  	resp, err := http.Get(c.baseURL.String() + "/" + scope)
   356  	if err != nil {
   357  		return &s, err
   358  	}
   359  	defer resp.Body.Close()
   360  	if resp.StatusCode != http.StatusOK {
   361  		return &s, fmt.Errorf("request not successful %s: ", resp.Status)
   362  	}
   363  
   364  	err = json.NewDecoder(resp.Body).Decode(&s)
   365  	if err != nil {
   366  		return &s, fmt.Errorf("cannot deserialize response: %s", err)
   367  	}
   368  	return &s, nil
   369  }
   370  
   371  var (
   372  	istiodLabelSelector = ""
   373  	istiodReset         = false
   374  	validationPattern   = `^\w+:(debug|error|warn|info|debug)`
   375  )
   376  
   377  func istiodLogCmd(ctx cli.Context) *cobra.Command {
   378  	var controlzPort int
   379  	var opts clioptions.ControlPlaneOptions
   380  	outputLogLevel := ""
   381  	stackTraceLevel := ""
   382  
   383  	// output format (yaml or short)
   384  	outputFormat := "short"
   385  
   386  	logCmd := &cobra.Command{
   387  		Use:   "log [<pod-name>]|[-r|--revision] [--level <scope>:<level>][--stack-trace-level <scope>:<level>]|[--reset]|[--output|-o short|json|yaml]",
   388  		Short: "Manage istiod logging.",
   389  		Long:  "Retrieve or update logging levels of istiod components.",
   390  		Example: `  # Retrieve information about istiod logging levels.
   391    istioctl admin log
   392  
   393    # Retrieve information about istiod logging levels on a specific control plane pod.
   394    istioctl admin l istiod-5c868d8bdd-pmvgg
   395  
   396    # Update levels of the specified loggers.
   397    istioctl admin log --level ads:debug,authorization:debug
   398  
   399    # Retrieve information about istiod logging levels for a specified revision.
   400    istioctl admin log --revision v1
   401  
   402    # Reset levels of all the loggers to default value (info).
   403    istioctl admin log --reset
   404  `,
   405  		Aliases: []string{"l"},
   406  		Args: func(logCmd *cobra.Command, args []string) error {
   407  			if istiodReset && outputLogLevel != "" {
   408  				logCmd.Println(logCmd.UsageString())
   409  				return fmt.Errorf("--level cannot be combined with --reset")
   410  			}
   411  			if istiodReset && stackTraceLevel != "" {
   412  				logCmd.Println(logCmd.UsageString())
   413  				return fmt.Errorf("--stack-trace-level cannot be combined with --reset")
   414  			}
   415  			return nil
   416  		},
   417  		RunE: func(logCmd *cobra.Command, args []string) error {
   418  			client, err := ctx.CLIClientWithRevision(opts.Revision)
   419  			if err != nil {
   420  				return fmt.Errorf("failed to create k8s client: %v", err)
   421  			}
   422  
   423  			var podName, ns string
   424  			if len(args) == 0 {
   425  				if opts.Revision == "" {
   426  					opts.Revision = "default"
   427  				}
   428  				if len(istiodLabelSelector) > 0 {
   429  					istiodLabelSelector = fmt.Sprintf("%s,%s=%s", istiodLabelSelector, label.IoIstioRev.Name, opts.Revision)
   430  				} else {
   431  					istiodLabelSelector = fmt.Sprintf("%s=%s", label.IoIstioRev.Name, opts.Revision)
   432  				}
   433  				pl, err := client.PodsForSelector(context.TODO(), ctx.NamespaceOrDefault(ctx.IstioNamespace()), istiodLabelSelector)
   434  				if err != nil {
   435  					return fmt.Errorf("not able to locate pod with selector %s: %v", istiodLabelSelector, err)
   436  				}
   437  
   438  				if len(pl.Items) < 1 {
   439  					return errors.New("no pods found")
   440  				}
   441  
   442  				if len(pl.Items) > 1 {
   443  					log.Warnf("more than 1 pods fits selector: %s; will use pod: %s", istiodLabelSelector, pl.Items[0].Name)
   444  				}
   445  
   446  				// only use the first pod in the list
   447  				podName = pl.Items[0].Name
   448  				ns = pl.Items[0].Namespace
   449  			} else if len(args) == 1 {
   450  				podName, ns = args[0], ctx.IstioNamespace()
   451  			}
   452  
   453  			portForwarder, err := client.NewPortForwarder(podName, ns, "", 0, controlzPort)
   454  			if err != nil {
   455  				return fmt.Errorf("could not build port forwarder for ControlZ %s: %v", podName, err)
   456  			}
   457  			defer portForwarder.Close()
   458  			err = portForwarder.Start()
   459  			if err != nil {
   460  				return fmt.Errorf("could not start port forwarder for ControlZ %s: %v", podName, err)
   461  			}
   462  
   463  			ctrlzClient := &ControlzClient{
   464  				baseURL: &url.URL{
   465  					Scheme: "http",
   466  					Host:   portForwarder.Address(),
   467  					Path:   "scopej",
   468  				},
   469  				httpClient: &http.Client{},
   470  			}
   471  			istiodConfigCmd := chooseClientFlag(ctrlzClient, istiodReset, outputLogLevel, stackTraceLevel, outputFormat)
   472  			err = istiodConfigCmd.execute(logCmd.OutOrStdout())
   473  			if err != nil {
   474  				return err
   475  			}
   476  			return nil
   477  		},
   478  		ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
   479  	}
   480  	opts.AttachControlPlaneFlags(logCmd)
   481  	logCmd.PersistentFlags().BoolVar(&istiodReset, "reset", istiodReset, "Reset levels to default value. (info)")
   482  	logCmd.PersistentFlags().IntVar(&controlzPort, "ctrlz_port", 9876, "ControlZ port")
   483  	logCmd.PersistentFlags().StringVar(&outputLogLevel, "level", outputLogLevel,
   484  		"Comma-separated list of output logging level for scopes in the format of <scope>:<level>[,<scope>:<level>,...]. "+
   485  			"Possible values for <level>: none, error, warn, info, debug")
   486  	logCmd.PersistentFlags().StringVar(&stackTraceLevel, "stack-trace-level", stackTraceLevel,
   487  		"Comma-separated list of stack trace level for scopes in the format of <scope>:<stack-trace-level>[,<scope>:<stack-trace-level>,...]. "+
   488  			"Possible values for <stack-trace-level>: none, error, warn, info, debug")
   489  	logCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o",
   490  		outputFormat, "Output format: one of json|yaml|short")
   491  	return logCmd
   492  }