github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/podinfo/podinfo.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package podinfo provides a coverage viewer for Spyglass 18 package podinfo 19 20 import ( 21 "bytes" 22 "encoding/json" 23 "fmt" 24 "html/template" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/sirupsen/logrus" 30 v1 "k8s.io/api/core/v1" 31 "sigs.k8s.io/prow/pkg/config" 32 k8sreporter "sigs.k8s.io/prow/pkg/crier/reporters/gcs/kubernetes" 33 "sigs.k8s.io/prow/pkg/entrypoint" 34 "sigs.k8s.io/yaml" 35 36 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 37 38 "sigs.k8s.io/prow/pkg/spyglass/api" 39 "sigs.k8s.io/prow/pkg/spyglass/lenses" 40 ) 41 42 const ( 43 name = "podinfo" 44 title = "Job Pod Info" 45 priority = 20 46 ) 47 48 func init() { 49 lenses.RegisterLens(Lens{}) 50 } 51 52 // ownConfig stores config specific to podinfo lens. 53 type ownConfig struct { 54 // RunnerConfig defines the mapping between build cluster alias: <template>, 55 // where the template is used for helping displaying url to pod. 56 RunnerConfigs map[string]RunnerConfig `json:"runner_configs,omitempty"` 57 } 58 59 type RunnerConfig struct { 60 PodLinkTemplate string `json:"pod_link_template,omitempty"` 61 } 62 63 // Lens is the implementation of a coverage-rendering Spyglass lens. 64 type Lens struct{} 65 66 // Config returns the lens's configuration. 67 func (lens Lens) Config() lenses.LensConfig { 68 return lenses.LensConfig{ 69 Name: name, 70 Title: title, 71 Priority: priority, 72 } 73 } 74 75 // Header renders the content of <head> from template.html. 76 func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string { 77 t, err := loadTemplate(filepath.Join(resourceDir, "template.html")) 78 if err != nil { 79 return fmt.Sprintf("<!-- FAILED LOADING HEADER: %v -->", err) 80 } 81 var buf bytes.Buffer 82 if err := t.ExecuteTemplate(&buf, "header", nil); err != nil { 83 return fmt.Sprintf("<!-- FAILED EXECUTING HEADER TEMPLATE: %v -->", err) 84 } 85 return buf.String() 86 } 87 88 // Callback does nothing. 89 func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 90 return "" 91 } 92 93 // Body renders the <body> 94 func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, rawConfig json.RawMessage, spyglassConfig config.Spyglass) string { 95 if len(artifacts) == 0 { 96 logrus.Error("podinfo Body() called with no artifacts, which should never happen.") 97 return "Why am I here? There is no podinfo file." 98 } 99 100 var conf ownConfig 101 if len(rawConfig) > 0 { 102 if err := json.Unmarshal(rawConfig, &conf); err != nil { 103 logrus.WithError(err).Error("Failed to decode podinfo config") 104 } 105 } 106 107 var p k8sreporter.PodReport 108 var pj prowapi.ProwJob 109 for _, artifact := range artifacts { 110 switch artifact.JobPath() { 111 case "podinfo.json": 112 content, err := artifact.ReadAll() 113 if err != nil { 114 logrus.WithError(err).Warn("Couldn't read a podinfo file that should exist.") 115 return fmt.Sprintf("Failed to read the podinfo file: %v", err) 116 } 117 118 if err := json.Unmarshal(content, &p); err != nil { 119 logrus.WithError(err).Infof("Error unmarshalling PodReport") 120 return fmt.Sprintf("Couldn't unmarshal podinfo.json: %v", err) 121 } 122 case "prowjob.json": 123 // Need to figure out which cluster this job runs on. But pod info 124 // itself doesn't really know where it belongs to, so get it from prowjob. 125 content, err := artifact.ReadAll() 126 if err != nil { 127 logrus.WithError(err).Warn("Couldn't read a prowjob file that should exist.") 128 return fmt.Sprintf("Failed to read the prowjob file: %v", err) 129 } 130 131 if err := json.Unmarshal(content, &pj); err != nil { 132 logrus.WithError(err).Infof("Error unmarshalling prowjob") 133 return fmt.Sprintf("Couldn't unmarshal prowjob.json: %v", err) 134 } 135 default: 136 logrus.WithField("artifact", artifact.JobPath()).Debug("Unsupported artifact by podinfo lens.") 137 } 138 } 139 140 infoTemplate, err := loadTemplate(filepath.Join(resourceDir, "template.html")) 141 if err != nil { 142 logrus.WithError(err).Error("Error loading template.") 143 return fmt.Sprintf("Failed to load template file: %v", err) 144 } 145 146 var podLink string 147 if len(conf.RunnerConfigs) > 0 && p.Pod.Name != "" && pj.Spec.Cluster != "" { 148 runnerConfig, ok := conf.RunnerConfigs[pj.Spec.Cluster] 149 if ok { 150 tmpl, err := template.New("tmp").Parse(runnerConfig.PodLinkTemplate) 151 if err == nil { 152 var b bytes.Buffer 153 err = tmpl.Execute(&b, p.Pod) 154 if err == nil { 155 podLink = b.String() 156 } 157 } 158 if err != nil { 159 logrus.WithError(err).Info("Error parsing template") 160 } 161 } 162 } 163 164 t := struct { 165 PodReport k8sreporter.PodReport 166 PodLink string 167 Containers []containerInfo 168 }{ 169 PodReport: p, 170 PodLink: podLink, 171 Containers: append(assembleContainers(p.Pod.Spec.InitContainers, p.Pod.Status.InitContainerStatuses), assembleContainers(p.Pod.Spec.Containers, p.Pod.Status.ContainerStatuses)...), 172 } 173 174 var buf bytes.Buffer 175 if err := infoTemplate.ExecuteTemplate(&buf, "body", t); err != nil { 176 logrus.WithError(err).Error("Error executing template.") 177 } 178 179 return buf.String() 180 } 181 182 type containerInfo struct { 183 // Container is a container spec 184 Container *v1.Container 185 // Status is a container status corresponding to the spec 186 Status *v1.ContainerStatus 187 // DecoratedArgs is the arguments the podutils entrypoint is invoking, 188 // which is explicitly extracted because `/tools/entrypoint` is not a very 189 // useful entrypoint to report. 190 DecoratedArgs []string 191 } 192 193 func assembleContainers(containers []v1.Container, containerStatuses []v1.ContainerStatus) []containerInfo { 194 var assembled []containerInfo 195 for i, c := range containers { 196 ci := containerInfo{ 197 Container: &containers[i], 198 Status: nil, 199 } 200 for _, env := range c.Env { 201 if env.Name == entrypoint.JSONConfigEnvVar && env.Value != "" { 202 entrypointOptions := entrypoint.NewOptions() 203 if err := entrypointOptions.LoadConfig(env.Value); err != nil { 204 logrus.WithError(err).Infof("Couldn't parse JSON config env var") 205 break 206 } 207 ci.DecoratedArgs = entrypointOptions.Args 208 break 209 } 210 } 211 for j, s := range containerStatuses { 212 if s.Name == c.Name { 213 ci.Status = &containerStatuses[j] 214 break 215 } 216 } 217 if ci.Status != nil { 218 assembled = append(assembled, ci) 219 } 220 } 221 return assembled 222 } 223 224 func loadTemplate(path string) (*template.Template, error) { 225 return template.New("template.html").Funcs(template.FuncMap{ 226 "isProw": func(s string) bool { 227 return strings.HasPrefix(s, "prow.k8s.io/") || strings.HasPrefix(s, "testgrid-") || s == "created-by-prow" 228 }, 229 "toYaml": func(o interface{}) (string, error) { 230 result, err := yaml.Marshal(o) 231 if err != nil { 232 return "", err 233 } 234 return string(result), nil 235 }, 236 "toAge": func(t time.Time) string { 237 d := time.Since(t) 238 if d < time.Minute { 239 return d.Truncate(time.Second).String() 240 } 241 s := d.Truncate(time.Minute).String() 242 // Chop off the 0s at the end. 243 return s[:len(s)-2] 244 }, 245 }).ParseFiles(path) 246 }