istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/bug-report/pkg/config/config.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 config
    16  
    17  import (
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"math"
    22  	"strings"
    23  	"time"
    24  )
    25  
    26  type ResourceType int
    27  
    28  const (
    29  	Namespace ResourceType = iota
    30  	Deployment
    31  	Pod
    32  	Label
    33  	Annotation
    34  	Container
    35  )
    36  
    37  // SelectionSpec is a spec for pods that will be Include in the capture
    38  // archive. The format is:
    39  //
    40  //	Namespace1,Namespace2../Deployments/Pods/Label1,Label2.../Annotation1,Annotation2.../ContainerName1,ContainerName2...
    41  //
    42  // Namespace, pod and container names are pattern matching while labels
    43  // and annotations may have pattern in the values with exact match for keys.
    44  // All labels and annotations in the list must match.
    45  // All fields are optional, if they are not specified, all values match.
    46  // Pattern matching style is glob.
    47  // Exclusions have a higher precedence than inclusions.
    48  // Ordering defines pod priority for cases where the archive exceeds the maximum
    49  // size and some logs must be dropped.
    50  //
    51  // Examples:
    52  //
    53  // 1. All pods in test-namespace with label "test=foo" but without label "private" (with any value):
    54  //
    55  //	include:
    56  //	  test-namespace/*/*/test=foo
    57  //	exclude:
    58  //	  test-namespace/*/*/private
    59  //
    60  // 2. Pods in all namespaces except "kube-system" with annotation "revision"
    61  // matching wildcard 1.6*:
    62  //
    63  //	exclude:
    64  //	  kube-system/*/*/*/revision=1.6*
    65  //
    66  // 3. Pods with "prometheus" in the name, except those with
    67  // the annotation "internal=true":
    68  //
    69  //	include:
    70  //	  */*/*prometheus*
    71  //	exclude:
    72  //	  */*/*prometheus*/*/internal=true
    73  //
    74  // 4. Container logs for all containers called "istio-proxy":
    75  //
    76  //	include:
    77  //	  */*/*/*/*/istio-proxy
    78  type SelectionSpec struct {
    79  	Namespaces  []string          `json:"namespaces,omitempty"`
    80  	Deployments []string          `json:"deployments,omitempty"`
    81  	Daemonsets  []string          `json:"daemonsets,omitempty"`
    82  	Pods        []string          `json:"pods,omitempty"`
    83  	Containers  []string          `json:"containers,omitempty"`
    84  	Labels      map[string]string `json:"labels,omitempty"`
    85  	Annotations map[string]string `json:"annotations,omitempty"`
    86  }
    87  
    88  type SelectionSpecs []*SelectionSpec
    89  
    90  func (s SelectionSpecs) String() string {
    91  	var out []string
    92  	for _, ss := range s {
    93  		st := ""
    94  		if !defaultListSetting(ss.Namespaces) {
    95  			st += fmt.Sprintf("Namespaces: %s", strings.Join(ss.Namespaces, ","))
    96  		}
    97  		if !defaultListSetting(ss.Deployments) {
    98  			st += fmt.Sprintf("/Deployments: %s", strings.Join(ss.Deployments, ","))
    99  		}
   100  		if !defaultListSetting(ss.Pods) {
   101  			st += fmt.Sprintf("/Pods:%s", strings.Join(ss.Pods, ","))
   102  		}
   103  		if !defaultListSetting(ss.Containers) {
   104  			st += fmt.Sprintf("/Containers: %s", strings.Join(ss.Containers, ","))
   105  		}
   106  		if len(ss.Labels) > 0 {
   107  			st += fmt.Sprintf("/Labels: %v", ss.Labels)
   108  		}
   109  		if len(ss.Annotations) > 0 {
   110  			st += fmt.Sprintf("/Annotations: %v", ss.Annotations)
   111  		}
   112  		out = append(out, "{ "+st+" }")
   113  	}
   114  	return strings.Join(out, " AND ")
   115  }
   116  
   117  func defaultListSetting(s []string) bool {
   118  	if len(s) < 1 {
   119  		return true
   120  	}
   121  	if len(s) == 1 {
   122  		return strings.TrimSpace(s[0]) == "" || s[0] == "*"
   123  	}
   124  	return false
   125  }
   126  
   127  // BugReportConfig controls what is captured and Include in the kube-capture tool
   128  // archive.
   129  type BugReportConfig struct {
   130  	// KubeConfigPath is the path to kube config file.
   131  	KubeConfigPath string `json:"kubeConfigPath,omitempty"`
   132  	// Context is the cluster Context in the kube config
   133  	Context string `json:"context,omitempty"`
   134  
   135  	// IstioNamespace is the namespace where the istio control plane is installed.
   136  	IstioNamespace string `json:"istioNamespace,omitempty"`
   137  
   138  	// DryRun controls whether logs are actually captured and saved.
   139  	DryRun bool `json:"dryRun,omitempty"`
   140  
   141  	// FullSecrets controls whether secret contents are included.
   142  	FullSecrets bool `json:"fullSecrets,omitempty"`
   143  
   144  	// CommandTimeout is the maximum amount of time running the command
   145  	// before giving up, even if not all logs are captured. Upon timeout,
   146  	// the command creates an archive with only the logs captured so far.
   147  	CommandTimeout Duration `json:"commandTimeout,omitempty"`
   148  
   149  	// Include is a list of SelectionSpec entries for resources to include.
   150  	Include SelectionSpecs `json:"include,omitempty"`
   151  	// Exclude is a list of SelectionSpec entries for resources t0 exclude.
   152  	Exclude SelectionSpecs `json:"exclude,omitempty"`
   153  
   154  	// StartTime is the start time the log capture time range.
   155  	// If set, Since must be unset.
   156  	StartTime time.Time `json:"startTime,omitempty"`
   157  	// EndTime is the end time the log capture time range.
   158  	// Default is now.
   159  	EndTime time.Time `json:"endTime,omitempty"`
   160  	// Since defines the start time the log capture time range.
   161  	// StartTime is set to EndTime - Since.
   162  	// If set, StartTime must be unset.
   163  	Since Duration `json:"since,omitempty"`
   164  
   165  	// TimeFilterApplied stores if user has provided any time filtering flags.
   166  	// If Since, StartTime, EndTime are all not applied by the user, set TimeFilterApplied as false; Otherwise set true
   167  	TimeFilterApplied bool `json:"timeFilterApplied,omitempty"`
   168  
   169  	// CriticalErrors is a list of glob pattern matches for errors that,
   170  	// if found in a log, set the highest priority for the log to ensure
   171  	// that it is Include in the capture archive.
   172  	CriticalErrors []string `json:"criticalErrors,omitempty"`
   173  	// IgnoredErrors are glob error patterns which are ignored when
   174  	// calculating the error heuristic for a log.
   175  	IgnoredErrors []string `json:"ignoredErrors,omitempty"`
   176  
   177  	// RequestConcurrency controls the request concurrency limit to the API server.
   178  	RequestConcurrency int `json:"requestConcurrency,omitempty"`
   179  }
   180  
   181  func (b *BugReportConfig) String() string {
   182  	out := ""
   183  	if b.KubeConfigPath != "" {
   184  		out += fmt.Sprintf("kubeconfig: %s\n", b.KubeConfigPath)
   185  	}
   186  	if b.Context != "" {
   187  		out += fmt.Sprintf("context: %s\n", b.Context)
   188  	}
   189  	out += fmt.Sprintf("istio-namespace: %s\n", b.IstioNamespace)
   190  	out += fmt.Sprintf("full-secrets: %v\n", b.FullSecrets)
   191  	out += fmt.Sprintf("timeout (mins): %v\n", math.Round(float64(int(b.CommandTimeout))/float64(time.Minute)))
   192  	out += fmt.Sprintf("include: %s\n", b.Include)
   193  	out += fmt.Sprintf("exclude: %s\n", b.Exclude)
   194  	if !b.StartTime.Equal(time.Time{}) {
   195  		out += fmt.Sprintf("start-time: %v\n", b.StartTime)
   196  	}
   197  	out += fmt.Sprintf("end-time: %v\n", b.EndTime)
   198  	if b.Since != 0 {
   199  		out += fmt.Sprintf("since: %v\n", b.Since)
   200  	}
   201  	return out
   202  }
   203  
   204  func parseToIncludeTypeSlice(s string) []string {
   205  	if strings.TrimSpace(s) == "*" || s == "" {
   206  		return nil
   207  	}
   208  	return strings.Split(s, ",")
   209  }
   210  
   211  func parseToIncludeTypeMap(s string) (map[string]string, error) {
   212  	if strings.TrimSpace(s) == "*" {
   213  		return nil, nil
   214  	}
   215  	out := make(map[string]string)
   216  	for _, ss := range strings.Split(s, ",") {
   217  		if len(ss) == 0 {
   218  			continue
   219  		}
   220  		kv := strings.Split(ss, "=")
   221  		if len(kv) != 2 {
   222  			return nil, fmt.Errorf("bad label/annotation selection %s, must have format key=value", ss)
   223  		}
   224  		if strings.Contains(kv[0], "*") {
   225  			return nil, fmt.Errorf("bad label/annotation selection %s, key cannot have '*' wildcards", ss)
   226  		}
   227  		out[kv[0]] = kv[1]
   228  	}
   229  	return out, nil
   230  }
   231  
   232  func (s *SelectionSpec) UnmarshalJSON(b []byte) error {
   233  	ft := []ResourceType{Namespace, Deployment, Pod, Label, Annotation, Container}
   234  	str := strings.TrimPrefix(strings.TrimSuffix(string(b), `"`), `"`)
   235  	for i, f := range strings.Split(str, "/") {
   236  		var err error
   237  		switch ft[i] {
   238  		case Namespace:
   239  			s.Namespaces = parseToIncludeTypeSlice(f)
   240  		case Deployment:
   241  			s.Deployments = parseToIncludeTypeSlice(f)
   242  		case Pod:
   243  			s.Pods = parseToIncludeTypeSlice(f)
   244  		case Label:
   245  			s.Labels, err = parseToIncludeTypeMap(f)
   246  			if err != nil {
   247  				return err
   248  			}
   249  		case Annotation:
   250  			s.Annotations, err = parseToIncludeTypeMap(f)
   251  			if err != nil {
   252  				return err
   253  			}
   254  		case Container:
   255  			s.Containers = parseToIncludeTypeSlice(f)
   256  		}
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  func (s *SelectionSpec) MarshalJSON() ([]byte, error) {
   263  	out := fmt.Sprint(strings.Join(s.Namespaces, ","))
   264  	out += fmt.Sprintf("/%s", strings.Join(s.Deployments, ","))
   265  	out += fmt.Sprintf("/%s", strings.Join(s.Pods, ","))
   266  	tmp := []string{}
   267  	for k, v := range s.Labels {
   268  		tmp = append(tmp, fmt.Sprintf("%s=%s", k, v))
   269  	}
   270  	out += fmt.Sprintf("/%s", strings.Join(tmp, ","))
   271  	tmp = []string{}
   272  	for k, v := range s.Annotations {
   273  		tmp = append(tmp, fmt.Sprintf("%s=%s", k, v))
   274  	}
   275  	out += fmt.Sprintf("/%s", strings.Join(tmp, ","))
   276  	out += fmt.Sprintf("/%s", strings.Join(s.Containers, ","))
   277  	return []byte(`"` + out + `"`), nil
   278  }
   279  
   280  type Duration time.Duration
   281  
   282  func (d Duration) MarshalJSON() ([]byte, error) {
   283  	return json.Marshal(time.Duration(d).String())
   284  }
   285  
   286  func (d *Duration) UnmarshalJSON(b []byte) error {
   287  	var v any
   288  	if err := json.Unmarshal(b, &v); err != nil {
   289  		return err
   290  	}
   291  	switch value := v.(type) {
   292  	case float64:
   293  		*d = Duration(time.Duration(value))
   294  		return nil
   295  	case string:
   296  		tmp, err := time.ParseDuration(value)
   297  		if err != nil {
   298  			return err
   299  		}
   300  		*d = Duration(tmp)
   301  		return nil
   302  	default:
   303  		return errors.New("invalid duration")
   304  	}
   305  }