| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package config |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "math" |
| "strings" |
| "time" |
| ) |
| |
| type ResourceType int |
| |
| const ( |
| Namespace ResourceType = iota |
| Deployment |
| Pod |
| Label |
| Annotation |
| Container |
| ) |
| |
| // SelectionSpec is a spec for pods that will be Include in the capture |
| // archive. The format is: |
| // |
| // Namespace1,Namespace2../Deployments/Pods/Label1,Label2.../Annotation1,Annotation2.../ContainerName1,ContainerName2... |
| // |
| // Namespace, pod and container names are pattern matching while labels |
| // and annotations may have pattern in the values with exact match for keys. |
| // All labels and annotations in the list must match. |
| // All fields are optional, if they are not specified, all values match. |
| // Pattern matching style is glob. |
| // Exclusions have a higher precedence than inclusions. |
| // Ordering defines pod priority for cases where the archive exceeds the maximum |
| // size and some logs must be dropped. |
| // |
| // Examples: |
| // |
| // 1. All pods in test-namespace with label "test=foo" but without label "private" (with any value): |
| // include: |
| // test-namespace/*/*/test=foo |
| // exclude: |
| // test-namespace/*/*/private |
| // |
| // 2. Pods in all namespaces except "kube-system" with annotation "revision" |
| // matching wildcard 1.6*: |
| // |
| // exclude: |
| // kube-system/*/*/*/revision=1.6* |
| // |
| // 3. Pods with "prometheus" in the name, except those with |
| // the annotation "internal=true": |
| // |
| // include: |
| // */*/*prometheus* |
| // exclude: |
| // */*/*prometheus*/*/internal=true |
| // |
| // 4. Container logs for all containers called "istio-proxy": |
| // include: |
| // */*/*/*/*/istio-proxy |
| type SelectionSpec struct { |
| Namespaces []string `json:"namespaces,omitempty"` |
| Deployments []string `json:"deployments,omitempty"` |
| Pods []string `json:"pods,omitempty"` |
| Containers []string `json:"containers,omitempty"` |
| Labels map[string]string `json:"labels,omitempty"` |
| Annotations map[string]string `json:"annotations,omitempty"` |
| } |
| |
| type SelectionSpecs []*SelectionSpec |
| |
| func (s SelectionSpecs) String() string { |
| var out []string |
| for _, ss := range s { |
| st := "" |
| if !defaultListSetting(ss.Namespaces) { |
| st += fmt.Sprintf("Namespaces: %s", strings.Join(ss.Namespaces, ",")) |
| } |
| if !defaultListSetting(ss.Deployments) { |
| st += fmt.Sprintf("/Deployments: %s", strings.Join(ss.Deployments, ",")) |
| } |
| if !defaultListSetting(ss.Pods) { |
| st += fmt.Sprintf("/Pods:%s", strings.Join(ss.Pods, ",")) |
| } |
| if !defaultListSetting(ss.Containers) { |
| st += fmt.Sprintf("/Containers: %s", strings.Join(ss.Containers, ",")) |
| } |
| if len(ss.Labels) > 0 { |
| st += fmt.Sprintf("/Labels: %v", ss.Labels) |
| } |
| if len(ss.Annotations) > 0 { |
| st += fmt.Sprintf("/Annotations: %v", ss.Annotations) |
| } |
| out = append(out, "{ "+st+" }") |
| } |
| return strings.Join(out, " AND ") |
| } |
| |
| func defaultListSetting(s []string) bool { |
| if len(s) < 1 { |
| return true |
| } |
| if len(s) == 1 { |
| return strings.TrimSpace(s[0]) == "" || s[0] == "*" |
| } |
| return false |
| } |
| |
| // BugReportConfig controls what is captured and Include in the kube-capture tool |
| // archive. |
| type BugReportConfig struct { |
| // KubeConfigPath is the path to kube config file. |
| KubeConfigPath string `json:"kubeConfigPath,omitempty"` |
| // Context is the cluster Context in the kube config |
| Context string `json:"context,omitempty"` |
| |
| // IstioNamespace is the namespace where the istio control plane is installed. |
| IstioNamespace string `json:"istioNamespace,omitempty"` |
| |
| // DryRun controls whether logs are actually captured and saved. |
| DryRun bool `json:"dryRun,omitempty"` |
| |
| // FullSecrets controls whether secret contents are included. |
| FullSecrets bool `json:"fullSecrets,omitempty"` |
| |
| // CommandTimeout is the maximum amount of time running the command |
| // before giving up, even if not all logs are captured. Upon timeout, |
| // the command creates an archive with only the logs captured so far. |
| CommandTimeout Duration `json:"commandTimeout,omitempty"` |
| |
| // Include is a list of SelectionSpec entries for resources to include. |
| Include SelectionSpecs `json:"include,omitempty"` |
| // Exclude is a list of SelectionSpec entries for resources t0 exclude. |
| Exclude SelectionSpecs `json:"exclude,omitempty"` |
| |
| // StartTime is the start time the the log capture time range. |
| // If set, Since must be unset. |
| StartTime time.Time `json:"startTime,omitempty"` |
| // EndTime is the end time the the log capture time range. |
| // Default is now. |
| EndTime time.Time `json:"endTime,omitempty"` |
| // Since defines the start time the the log capture time range. |
| // StartTime is set to EndTime - Since. |
| // If set, StartTime must be unset. |
| Since Duration `json:"since,omitempty"` |
| |
| // CriticalErrors is a list of glob pattern matches for errors that, |
| // if found in a log, set the highest priority for the log to ensure |
| // that it is Include in the capture archive. |
| CriticalErrors []string `json:"criticalErrors,omitempty"` |
| // IgnoredErrors are glob error patterns which are ignored when |
| // calculating the error heuristic for a log. |
| IgnoredErrors []string `json:"ignoredErrors,omitempty"` |
| } |
| |
| func (b *BugReportConfig) String() string { |
| out := "" |
| if b.KubeConfigPath != "" { |
| out += fmt.Sprintf("kubeconfig: %s\n", b.KubeConfigPath) |
| } |
| if b.Context != "" { |
| out += fmt.Sprintf("context: %s\n", b.Context) |
| } |
| out += fmt.Sprintf("istio-namespace: %s\n", b.IstioNamespace) |
| out += fmt.Sprintf("full-secrets: %v\n", b.FullSecrets) |
| out += fmt.Sprintf("timeout (mins): %v\n", math.Round(float64(int(b.CommandTimeout))/float64(time.Minute))) |
| out += fmt.Sprintf("include: %s\n", b.Include) |
| out += fmt.Sprintf("exclude: %s\n", b.Exclude) |
| if !b.StartTime.Equal(time.Time{}) { |
| out += fmt.Sprintf("start-time: %v\n", b.StartTime) |
| } |
| out += fmt.Sprintf("end-time: %v\n", b.EndTime) |
| if b.Since != 0 { |
| out += fmt.Sprintf("since: %v\n", b.Since) |
| } |
| return out |
| } |
| |
| func parseToIncludeTypeSlice(s string) []string { |
| if strings.TrimSpace(s) == "*" || s == "" { |
| return nil |
| } |
| return strings.Split(s, ",") |
| } |
| |
| func parseToIncludeTypeMap(s string) (map[string]string, error) { |
| if strings.TrimSpace(s) == "*" { |
| return nil, nil |
| } |
| out := make(map[string]string) |
| for _, ss := range strings.Split(s, ",") { |
| if len(ss) == 0 { |
| continue |
| } |
| kv := strings.Split(ss, "=") |
| if len(kv) != 2 { |
| return nil, fmt.Errorf("bad label/annotation selection %s, must have format key=value", ss) |
| } |
| if strings.Contains(kv[0], "*") { |
| return nil, fmt.Errorf("bad label/annotation selection %s, key cannot have '*' wildcards", ss) |
| } |
| out[kv[0]] = kv[1] |
| } |
| return out, nil |
| } |
| |
| func (s *SelectionSpec) UnmarshalJSON(b []byte) error { |
| ft := []ResourceType{Namespace, Deployment, Pod, Label, Annotation, Container} |
| str := strings.TrimPrefix(strings.TrimSuffix(string(b), `"`), `"`) |
| for i, f := range strings.Split(str, "/") { |
| var err error |
| switch ft[i] { |
| case Namespace: |
| s.Namespaces = parseToIncludeTypeSlice(f) |
| case Deployment: |
| s.Deployments = parseToIncludeTypeSlice(f) |
| case Pod: |
| s.Pods = parseToIncludeTypeSlice(f) |
| case Label: |
| s.Labels, err = parseToIncludeTypeMap(f) |
| if err != nil { |
| return err |
| } |
| case Annotation: |
| s.Annotations, err = parseToIncludeTypeMap(f) |
| if err != nil { |
| return err |
| } |
| case Container: |
| s.Containers = parseToIncludeTypeSlice(f) |
| } |
| } |
| |
| return nil |
| } |
| |
| func (s *SelectionSpec) MarshalJSON() ([]byte, error) { |
| out := fmt.Sprint(strings.Join(s.Namespaces, ",")) |
| out += fmt.Sprintf("/%s", strings.Join(s.Deployments, ",")) |
| out += fmt.Sprintf("/%s", strings.Join(s.Pods, ",")) |
| tmp := []string{} |
| for k, v := range s.Labels { |
| tmp = append(tmp, fmt.Sprintf("%s=%s", k, v)) |
| } |
| out += fmt.Sprintf("/%s", strings.Join(tmp, ",")) |
| tmp = []string{} |
| for k, v := range s.Annotations { |
| tmp = append(tmp, fmt.Sprintf("%s=%s", k, v)) |
| } |
| out += fmt.Sprintf("/%s", strings.Join(tmp, ",")) |
| out += fmt.Sprintf("/%s", strings.Join(s.Containers, ",")) |
| return []byte(`"` + out + `"`), nil |
| } |
| |
| type Duration time.Duration |
| |
| func (d Duration) MarshalJSON() ([]byte, error) { |
| return json.Marshal(time.Duration(d).String()) |
| } |
| |
| func (d *Duration) UnmarshalJSON(b []byte) error { |
| var v interface{} |
| if err := json.Unmarshal(b, &v); err != nil { |
| return err |
| } |
| switch value := v.(type) { |
| case float64: |
| *d = Duration(time.Duration(value)) |
| return nil |
| case string: |
| tmp, err := time.ParseDuration(value) |
| if err != nil { |
| return err |
| } |
| *d = Duration(tmp) |
| return nil |
| default: |
| return errors.New("invalid duration") |
| } |
| } |