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 }