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  }