You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
6.7 KiB
238 lines
6.7 KiB
/* |
|
Copyright 2018 The Kubernetes Authors. |
|
|
|
Licensed under the Apache License, Version 2.0 (the "License"); |
|
you may not use this file except in compliance with the License. |
|
You may obtain a copy of the License at |
|
|
|
http://www.apache.org/licenses/LICENSE-2.0 |
|
|
|
Unless required by applicable law or agreed to in writing, software |
|
distributed under the License is distributed on an "AS IS" BASIS, |
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
See the License for the specific language governing permissions and |
|
limitations under the License. |
|
*/ |
|
|
|
package clonerefs |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"errors" |
|
"flag" |
|
"fmt" |
|
"strings" |
|
"text/template" |
|
|
|
"k8s.io/apimachinery/pkg/util/sets" |
|
"k8s.io/test-infra/prow/kube" |
|
) |
|
|
|
// Options configures the clonerefs tool |
|
// completely and may be provided using JSON |
|
// or user-specified flags, but not both. |
|
type Options struct { |
|
// SrcRoot is the root directory under which |
|
// all source code is cloned |
|
SrcRoot string `json:"src_root"` |
|
// Log is the log file to which clone records are written |
|
Log string `json:"log"` |
|
|
|
// GitUserName is an optional field that is used with |
|
// `git config user.name` |
|
GitUserName string `json:"git_user_name,omitempty"` |
|
// GitUserEmail is an optional field that is used with |
|
// `git config user.email` |
|
GitUserEmail string `json:"git_user_email,omitempty"` |
|
|
|
// GitRefs are the refs to clone |
|
GitRefs []kube.Refs `json:"refs"` |
|
// KeyFiles are files containing SSH keys to be used |
|
// when cloning. Will be added to `ssh-agent`. |
|
KeyFiles []string `json:"key_files,omitempty"` |
|
|
|
// HostFingerPrints are ssh-keyscan host fingerprint lines to use |
|
// when cloning. Will be added to ~/.ssh/known_hosts |
|
HostFingerprints []string `json:"host_fingerprints,omitempty"` |
|
|
|
// MaxParallelWorkers determines how many repositories |
|
// can be cloned in parallel. If 0, interpreted as no |
|
// limit to parallelism |
|
MaxParallelWorkers int `json:"max_parallel_workers,omitempty"` |
|
|
|
// used to hold flag values |
|
refs gitRefs |
|
clonePath orgRepoFormat |
|
cloneURI orgRepoFormat |
|
keys stringSlice |
|
CookiePath string `json:"cookie_path,omitempty"` |
|
} |
|
|
|
// Validate ensures that the configuration options are valid |
|
func (o *Options) Validate() error { |
|
if o.SrcRoot == "" { |
|
return errors.New("no source root specified") |
|
} |
|
|
|
if o.Log == "" { |
|
return errors.New("no log file specified") |
|
} |
|
|
|
if len(o.GitRefs) == 0 { |
|
return errors.New("no refs specified to clone") |
|
} |
|
|
|
seen := map[string]sets.String{} |
|
for _, ref := range o.GitRefs { |
|
if _, seenOrg := seen[ref.Org]; seenOrg { |
|
if seen[ref.Org].Has(ref.Repo) { |
|
return errors.New("sync config for %s/%s provided more than once") |
|
} |
|
seen[ref.Org].Insert(ref.Repo) |
|
} else { |
|
seen[ref.Org] = sets.NewString(ref.Repo) |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
const ( |
|
// JSONConfigEnvVar is the environment variable that |
|
// clonerefs expects to find a full JSON configuration |
|
// in when run. |
|
JSONConfigEnvVar = "CLONEREFS_OPTIONS" |
|
// DefaultGitUserName is the default name used in git config |
|
DefaultGitUserName = "ci-robot" |
|
// DefaultGitUserEmail is the default email used in git config |
|
DefaultGitUserEmail = "[email protected]" |
|
) |
|
|
|
// ConfigVar exposes the environment variable used |
|
// to store serialized configuration |
|
func (o *Options) ConfigVar() string { |
|
return JSONConfigEnvVar |
|
} |
|
|
|
// LoadConfig loads options from serialized config |
|
func (o *Options) LoadConfig(config string) error { |
|
return json.Unmarshal([]byte(config), o) |
|
} |
|
|
|
// Complete internalizes command line arguments |
|
func (o *Options) Complete(args []string) { |
|
o.GitRefs = o.refs.gitRefs |
|
o.KeyFiles = o.keys.data |
|
|
|
for _, ref := range o.GitRefs { |
|
alias, err := o.clonePath.Execute(OrgRepo{Org: ref.Org, Repo: ref.Repo}) |
|
if err != nil { |
|
panic(err) |
|
} |
|
ref.PathAlias = alias |
|
|
|
alias, err = o.cloneURI.Execute(OrgRepo{Org: ref.Org, Repo: ref.Repo}) |
|
if err != nil { |
|
panic(err) |
|
} |
|
ref.CloneURI = alias |
|
} |
|
} |
|
|
|
// AddFlags adds flags to the FlagSet that populate |
|
// the GCS upload options struct given. |
|
func (o *Options) AddFlags(fs *flag.FlagSet) { |
|
fs.StringVar(&o.SrcRoot, "src-root", "", "Where to root source checkouts") |
|
fs.StringVar(&o.Log, "log", "", "Where to write logs") |
|
fs.StringVar(&o.GitUserName, "git-user-name", DefaultGitUserName, "Username to set in git config") |
|
fs.StringVar(&o.GitUserEmail, "git-user-email", DefaultGitUserEmail, "Email to set in git config") |
|
fs.Var(&o.refs, "repo", "Mapping of Git URI to refs to check out, can be provided more than once") |
|
fs.Var(&o.keys, "ssh-key", "Path to SSH key to enable during cloning, can be provided more than once") |
|
fs.Var(&o.clonePath, "clone-alias", "Format string for the path to clone to") |
|
fs.Var(&o.cloneURI, "uri-prefix", "Format string for the URI prefix to clone from") |
|
fs.IntVar(&o.MaxParallelWorkers, "max-workers", 0, "Maximum number of parallel workers, unset for unlimited.") |
|
fs.StringVar(&o.CookiePath, "cookiefile", "", "Path to git http.coookiefile") |
|
} |
|
|
|
type gitRefs struct { |
|
gitRefs []kube.Refs |
|
} |
|
|
|
func (r *gitRefs) String() string { |
|
representation := bytes.Buffer{} |
|
for _, ref := range r.gitRefs { |
|
fmt.Fprintf(&representation, "%s,%s=%s", ref.Org, ref.Repo, ref.String()) |
|
} |
|
return representation.String() |
|
} |
|
|
|
// Set parses out a kube.Refs from the user string. |
|
// The following example shows all possible fields: |
|
// org,repo=base-ref:base-sha[,pull-number:pull-sha]... |
|
// For the base ref and every pull number, the SHAs |
|
// are optional and any number of them may be set or |
|
// unset. |
|
func (r *gitRefs) Set(value string) error { |
|
gitRef, err := ParseRefs(value) |
|
if err != nil { |
|
return err |
|
} |
|
r.gitRefs = append(r.gitRefs, *gitRef) |
|
return nil |
|
} |
|
|
|
type stringSlice struct { |
|
data []string |
|
} |
|
|
|
func (r *stringSlice) String() string { |
|
return strings.Join(r.data, ",") |
|
} |
|
|
|
// Set records the value passed |
|
func (r *stringSlice) Set(value string) error { |
|
r.data = append(r.data, value) |
|
return nil |
|
} |
|
|
|
type orgRepoFormat struct { |
|
raw string |
|
format *template.Template |
|
} |
|
|
|
func (a *orgRepoFormat) String() string { |
|
return a.raw |
|
} |
|
|
|
// Set parses out overrides from user input |
|
func (a *orgRepoFormat) Set(value string) error { |
|
templ, err := template.New("format").Parse(value) |
|
if err != nil { |
|
return err |
|
} |
|
a.raw = value |
|
a.format = templ |
|
return nil |
|
} |
|
|
|
// OrgRepo hold both an org and repo name. |
|
type OrgRepo struct { |
|
Org, Repo string |
|
} |
|
|
|
func (a *orgRepoFormat) Execute(data OrgRepo) (string, error) { |
|
if a.format != nil { |
|
output := bytes.Buffer{} |
|
err := a.format.Execute(&output, data) |
|
return output.String(), err |
|
} |
|
return "", nil |
|
} |
|
|
|
// Encode will encode the set of options in the format that |
|
// is expected for the configuration environment variable |
|
func Encode(options Options) (string, error) { |
|
encoded, err := json.Marshal(options) |
|
return string(encoded), err |
|
}
|
|
|