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 }