github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/clonerefs/options.go (about)

     1  /*
     2  Copyright 2018 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 clonerefs
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"errors"
    23  	"flag"
    24  	"fmt"
    25  	"strings"
    26  	"text/template"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    30  	"sigs.k8s.io/prow/pkg/pod-utils/clone"
    31  )
    32  
    33  // Options configures the clonerefs tool
    34  // completely and may be provided using JSON
    35  // or user-specified flags, but not both.
    36  type Options struct {
    37  	// SrcRoot is the root directory under which
    38  	// all source code is cloned
    39  	SrcRoot string `json:"src_root"`
    40  	// Log is the log file to which clone records are written
    41  	Log string `json:"log"`
    42  
    43  	// GitUserName is an optional field that is used with
    44  	// `git config user.name`
    45  	GitUserName string `json:"git_user_name,omitempty"`
    46  	// GitUserEmail is an optional field that is used with
    47  	// `git config user.email`
    48  	GitUserEmail string `json:"git_user_email,omitempty"`
    49  
    50  	// GitRefs are the refs to clone
    51  	GitRefs []prowapi.Refs `json:"refs"`
    52  	// KeyFiles are files containing SSH keys to be used
    53  	// when cloning. Will be added to `ssh-agent`.
    54  	KeyFiles []string `json:"key_files,omitempty"`
    55  
    56  	// OauthTokenFile is the path of a file that contains an OAuth token.
    57  	OauthTokenFile string `json:"oauth_token_file,omitempty"`
    58  
    59  	// HostFingerPrints are ssh-keyscan host fingerprint lines to use
    60  	// when cloning. Will be added to ~/.ssh/known_hosts
    61  	HostFingerprints []string `json:"host_fingerprints,omitempty"`
    62  
    63  	// MaxParallelWorkers determines how many repositories
    64  	// can be cloned in parallel. If 0, interpreted as no
    65  	// limit to parallelism
    66  	MaxParallelWorkers int `json:"max_parallel_workers,omitempty"`
    67  
    68  	Fail bool `json:"fail,omitempty"`
    69  
    70  	CookiePath string `json:"cookie_path,omitempty"`
    71  
    72  	GitHubAPIEndpoints      []string `json:"github_api_endpoints,omitempty"`
    73  	GitHubAppID             string   `json:"github_app_id,omitempty"`
    74  	GitHubAppPrivateKeyFile string   `json:"github_app_private_key_file,omitempty"`
    75  
    76  	// used to hold flag values
    77  	refs      gitRefs
    78  	clonePath orgRepoFormat
    79  	cloneURI  orgRepoFormat
    80  	keys      stringSlice
    81  }
    82  
    83  // Validate ensures that the configuration options are valid
    84  func (o *Options) Validate() error {
    85  	if o.SrcRoot == "" {
    86  		return errors.New("no source root specified")
    87  	}
    88  
    89  	if o.Log == "" {
    90  		return errors.New("no log file specified")
    91  	}
    92  
    93  	if len(o.GitRefs) == 0 {
    94  		return errors.New("no refs specified to clone")
    95  	}
    96  
    97  	seen := make(map[string]int)
    98  	for i, ref := range o.GitRefs {
    99  		path := clone.PathForRefs(o.SrcRoot, ref)
   100  		if existing, ok := seen[path]; ok {
   101  			existingRef := o.GitRefs[existing]
   102  			err := fmt.Errorf("clone ref config %d (for %s/%s) will be extracted to %s, which clone ref %d (for %s/%s) is also using", i, ref.Org, ref.Repo, path, existing, existingRef.Org, existingRef.Repo)
   103  			if existingRef.Org == ref.Org && existingRef.Repo == ref.Repo {
   104  				return err
   105  			}
   106  			// preserving existing behavior where this is a warning, not an error
   107  			logrus.WithError(err).WithField("path", path).Warning("multiple refs clone to the same location")
   108  		}
   109  		seen[path] = i
   110  	}
   111  
   112  	if o.GitHubAppID != "" || o.GitHubAppPrivateKeyFile != "" {
   113  		if o.OauthTokenFile != "" {
   114  			return errors.New("multiple authentication methods specified")
   115  		}
   116  		if len(o.GitHubAPIEndpoints) == 0 {
   117  			return errors.New("no GitHub API endpoints for GitHub App authentication")
   118  		}
   119  	}
   120  	if o.GitHubAppID != "" && o.GitHubAppPrivateKeyFile == "" {
   121  		return errors.New("no GitHub App private key file specified")
   122  	}
   123  	if o.GitHubAppID == "" && o.GitHubAppPrivateKeyFile != "" {
   124  		return errors.New("no GitHub App ID specified")
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  const (
   131  	// JSONConfigEnvVar is the environment variable that
   132  	// clonerefs expects to find a full JSON configuration
   133  	// in when run.
   134  	JSONConfigEnvVar = "CLONEREFS_OPTIONS"
   135  	// DefaultGitUserName is the default name used in git config
   136  	DefaultGitUserName = "ci-robot"
   137  	// DefaultGitUserEmail is the default email used in git config
   138  	DefaultGitUserEmail = "ci-robot@k8s.io"
   139  )
   140  
   141  // ConfigVar exposes the environment variable used
   142  // to store serialized configuration
   143  func (o *Options) ConfigVar() string {
   144  	return JSONConfigEnvVar
   145  }
   146  
   147  // LoadConfig loads options from serialized config
   148  func (o *Options) LoadConfig(config string) error {
   149  	return json.Unmarshal([]byte(config), o)
   150  }
   151  
   152  // Complete internalizes command line arguments
   153  func (o *Options) Complete(args []string) {
   154  	o.GitRefs = o.refs.gitRefs
   155  	o.KeyFiles = o.keys.data
   156  
   157  	for _, ref := range o.GitRefs {
   158  		alias, err := o.clonePath.Execute(OrgRepo{Org: ref.Org, Repo: ref.Repo})
   159  		if err != nil {
   160  			panic(err)
   161  		}
   162  		ref.PathAlias = alias
   163  
   164  		alias, err = o.cloneURI.Execute(OrgRepo{Org: ref.Org, Repo: ref.Repo})
   165  		if err != nil {
   166  			panic(err)
   167  		}
   168  		ref.CloneURI = alias
   169  	}
   170  }
   171  
   172  // AddFlags adds flags to the FlagSet that populate
   173  // the GCS upload options struct given.
   174  func (o *Options) AddFlags(fs *flag.FlagSet) {
   175  	fs.StringVar(&o.SrcRoot, "src-root", "", "Where to root source checkouts")
   176  	fs.StringVar(&o.Log, "log", "", "Where to write logs")
   177  	fs.StringVar(&o.GitUserName, "git-user-name", DefaultGitUserName, "Username to set in git config")
   178  	fs.StringVar(&o.GitUserEmail, "git-user-email", DefaultGitUserEmail, "Email to set in git config")
   179  	fs.Var(&o.refs, "repo", "Mapping of Git URI to refs to check out, can be provided more than once")
   180  	fs.Var(&o.keys, "ssh-key", "Path to SSH key to enable during cloning, can be provided more than once")
   181  	fs.Var(&o.clonePath, "clone-alias", "Format string for the path to clone to")
   182  	fs.Var(&o.cloneURI, "uri-prefix", "Format string for the URI prefix to clone from")
   183  	fs.IntVar(&o.MaxParallelWorkers, "max-workers", 0, "Maximum number of parallel workers, unset for unlimited.")
   184  	fs.StringVar(&o.CookiePath, "cookiefile", "", "Path to git http.cookiefile")
   185  	fs.BoolVar(&o.Fail, "fail", false, "Exit with failure if any of the refs can't be fetched.")
   186  }
   187  
   188  type gitRefs struct {
   189  	gitRefs []prowapi.Refs
   190  }
   191  
   192  func (r *gitRefs) String() string {
   193  	representation := bytes.Buffer{}
   194  	for _, ref := range r.gitRefs {
   195  		fmt.Fprintf(&representation, "%s,%s=%s", ref.Org, ref.Repo, ref.String())
   196  	}
   197  	return representation.String()
   198  }
   199  
   200  // Set parses out a prowapi.Refs from the user string.
   201  // The following example shows all possible fields:
   202  //
   203  //	org,repo=base-ref:base-sha[,pull-number:pull-sha]...
   204  //
   205  // For the base ref and every pull number, the SHAs
   206  // are optional and any number of them may be set or
   207  // unset.
   208  func (r *gitRefs) Set(value string) error {
   209  	gitRef, err := ParseRefs(value)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	r.gitRefs = append(r.gitRefs, *gitRef)
   214  	return nil
   215  }
   216  
   217  type stringSlice struct {
   218  	data []string
   219  }
   220  
   221  func (r *stringSlice) String() string {
   222  	return strings.Join(r.data, ",")
   223  }
   224  
   225  // Set records the value passed
   226  func (r *stringSlice) Set(value string) error {
   227  	r.data = append(r.data, value)
   228  	return nil
   229  }
   230  
   231  type orgRepoFormat struct {
   232  	raw    string
   233  	format *template.Template
   234  }
   235  
   236  func (a *orgRepoFormat) String() string {
   237  	return a.raw
   238  }
   239  
   240  // Set parses out overrides from user input
   241  func (a *orgRepoFormat) Set(value string) error {
   242  	templ, err := template.New("format").Parse(value)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	a.raw = value
   247  	a.format = templ
   248  	return nil
   249  }
   250  
   251  // OrgRepo hold both an org and repo name.
   252  type OrgRepo struct {
   253  	Org, Repo string
   254  }
   255  
   256  func (a *orgRepoFormat) Execute(data OrgRepo) (string, error) {
   257  	if a.format != nil {
   258  		output := bytes.Buffer{}
   259  		err := a.format.Execute(&output, data)
   260  		return output.String(), err
   261  	}
   262  	return "", nil
   263  }
   264  
   265  // Encode will encode the set of options in the format that
   266  // is expected for the configuration environment variable
   267  func Encode(options Options) (string, error) {
   268  	encoded, err := json.Marshal(options)
   269  	return string(encoded), err
   270  }