github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/mkpod/main.go (about)

     1  /*
     2  Copyright 2017 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 main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path"
    26  	"strings"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	v1 "k8s.io/api/core/v1"
    30  	"sigs.k8s.io/yaml"
    31  
    32  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	"sigs.k8s.io/prow/pkg/kube"
    34  	"sigs.k8s.io/prow/pkg/pjutil"
    35  	"sigs.k8s.io/prow/pkg/pod-utils/decorate"
    36  )
    37  
    38  type options struct {
    39  	prowJobPath string
    40  	buildID     string
    41  
    42  	localMode bool
    43  	outputDir string
    44  }
    45  
    46  func (o *options) Validate() error {
    47  	if o.prowJobPath == "" {
    48  		return errors.New("required flag --prow-job was unset")
    49  	}
    50  
    51  	if !o.localMode && o.outputDir != "" {
    52  		return errors.New("out-dir may only be specified in --local mode")
    53  	}
    54  
    55  	return nil
    56  }
    57  
    58  func gatherOptions() options {
    59  	o := options{}
    60  	flag.StringVar(&o.prowJobPath, "prow-job", "", "ProwJob to decorate, - for stdin.")
    61  	flag.StringVar(&o.buildID, "build-id", "", "Build ID for the job run or 'snowflake' to generate one. Use 'snowflake' if tot is not used.")
    62  	flag.BoolVar(&o.localMode, "local", false, "Configures pod utils for local mode which avoids uploading to GCS and the need for credentials. Instead, files are copied to a directory on the host. Hint: This works great with kind!")
    63  	flag.StringVar(&o.outputDir, "out-dir", "", "Only allowed in --local mode. This is the directory to 'upload' to instead of GCS. If unspecified a temp dir is created.")
    64  	flag.Parse()
    65  	return o
    66  }
    67  
    68  func main() {
    69  	o := gatherOptions()
    70  	if err := o.Validate(); err != nil {
    71  		logrus.Fatalf("Invalid options: %v", err)
    72  	}
    73  
    74  	var rawJob []byte
    75  	if o.prowJobPath == "-" {
    76  		raw, err := io.ReadAll(os.Stdin)
    77  		if err != nil {
    78  			logrus.WithError(err).Fatal("Could not read ProwJob YAML from stdin.")
    79  		}
    80  		rawJob = raw
    81  	} else {
    82  		raw, err := os.ReadFile(o.prowJobPath)
    83  		if err != nil {
    84  			logrus.WithError(err).Fatal("Could not open ProwJob YAML.")
    85  		}
    86  		rawJob = raw
    87  	}
    88  
    89  	var job prowapi.ProwJob
    90  	if err := yaml.Unmarshal(rawJob, &job); err != nil {
    91  		logrus.WithError(err).Fatal("Could not unmarshal ProwJob YAML.")
    92  	}
    93  
    94  	if o.buildID == "" && job.Status.BuildID != "" {
    95  		o.buildID = job.Status.BuildID
    96  	}
    97  
    98  	if strings.ToLower(o.buildID) == "snowflake" {
    99  		// No error possible since this won't use tot.
   100  		o.buildID, _ = pjutil.GetBuildID(job.Spec.Job, "")
   101  		logrus.WithField("build-id", o.buildID).Info("Generated build-id for job.")
   102  	}
   103  
   104  	if o.buildID == "" {
   105  		logrus.Warning("No BuildID found in ProwJob status or given with --build-id, GCS interaction will be poor.")
   106  	}
   107  
   108  	var pod *v1.Pod
   109  	var err error
   110  	if o.localMode {
   111  		outDir := o.outputDir
   112  		if outDir == "" {
   113  			prefix := strings.Join([]string{"prowjob-out", job.Spec.Job, o.buildID}, "-")
   114  			logrus.Infof("Creating temp directory for job output in %q with prefix %q.", os.TempDir(), prefix)
   115  			outDir, err = os.MkdirTemp("", prefix)
   116  			if err != nil {
   117  				logrus.WithError(err).Fatal("Could not create temp directory for job output.")
   118  			}
   119  		} else {
   120  			outDir = path.Join(outDir, o.buildID)
   121  		}
   122  		logrus.WithField("out-dir", outDir).Info("Pod-utils configured for local mode. Instead of uploading to GCS, files will be copied to an output dir on the node.")
   123  
   124  		job.Status.BuildID = o.buildID
   125  		pod, err = makeLocalPod(job, outDir)
   126  		if err != nil {
   127  			logrus.WithError(err).Fatal("Could not decorate PodSpec for local mode.")
   128  		}
   129  	} else {
   130  		job.Status.BuildID = o.buildID
   131  		pod, err = decorate.ProwJobToPod(job)
   132  		if err != nil {
   133  			logrus.WithError(err).Fatal("Could not decorate PodSpec.")
   134  		}
   135  	}
   136  
   137  	// We need to remove the created-by-prow label, otherwise sinker will promptly clean this
   138  	// up as there is no associated prowjob
   139  	newLabels := map[string]string{}
   140  	for k, v := range pod.Labels {
   141  		if k == kube.CreatedByProw {
   142  			continue
   143  		}
   144  		newLabels[k] = v
   145  	}
   146  	pod.Labels = newLabels
   147  
   148  	pod.GetObjectKind().SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Pod"))
   149  	podYAML, err := yaml.Marshal(pod)
   150  	if err != nil {
   151  		logrus.WithError(err).Fatal("Could not marshal Pod YAML.")
   152  	}
   153  	fmt.Println(string(podYAML))
   154  }
   155  
   156  func makeLocalPod(pj prowapi.ProwJob, outDir string) (*v1.Pod, error) {
   157  	pod, err := decorate.ProwJobToPodLocal(pj, outDir)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	// Prompt for emptyDir or hostPath replacements for all volume sources besides those two.
   163  	volsToFix := nonLocalVolumes(pod.Spec.Volumes)
   164  	if len(volsToFix) > 0 {
   165  		prompt := `For each of the following volumes specify one of:
   166   - 'empty' to use an emptyDir;
   167   - a path on the host to use hostPath;
   168   - '' (nothing) to use the existing volume source and assume it is available in the cluster`
   169  		fmt.Fprintln(os.Stderr, prompt)
   170  		for _, vol := range volsToFix {
   171  			fmt.Fprintf(os.Stderr, "Volume %q: ", vol.Name)
   172  
   173  			var choice string
   174  			fmt.Scanln(&choice)
   175  			choice = strings.TrimSpace(choice)
   176  			switch {
   177  			case choice == "":
   178  				// Leave the VolumeSource as is.
   179  			case choice == "empty" || strings.ToLower(choice) == "emptydir":
   180  				vol.VolumeSource = v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}
   181  			default:
   182  				vol.VolumeSource = v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: choice}}
   183  			}
   184  		}
   185  	}
   186  
   187  	return pod, nil
   188  }
   189  
   190  func nonLocalVolumes(vols []v1.Volume) []*v1.Volume {
   191  	var res []*v1.Volume
   192  	for i, vol := range vols {
   193  		if vol.HostPath == nil && vol.EmptyDir == nil {
   194  			res = append(res, &vols[i])
   195  		}
   196  	}
   197  	return res
   198  }