github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/spawn/spawn.go (about) 1 package spawn 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "net/url" 8 "strings" 9 "time" 10 11 "github.com/evergreen-ci/evergreen" 12 "github.com/evergreen-ci/evergreen/cloud" 13 "github.com/evergreen-ci/evergreen/cloud/providers" 14 "github.com/evergreen-ci/evergreen/cloud/providers/ec2" 15 "github.com/evergreen-ci/evergreen/command" 16 "github.com/evergreen-ci/evergreen/model/distro" 17 "github.com/evergreen-ci/evergreen/model/host" 18 "github.com/evergreen-ci/evergreen/model/user" 19 "github.com/pkg/errors" 20 "gopkg.in/yaml.v2" 21 ) 22 23 const ( 24 MaxPerUser = 3 25 DefaultExpiration = time.Duration(24 * time.Hour) 26 ) 27 28 var SpawnLimitErr = errors.New("User is already running the max allowed # of spawn hosts") 29 30 // BadOptionsErr represents an in valid set of spawn options. 31 type BadOptionsErr struct { 32 message string 33 } 34 35 func (bsoe BadOptionsErr) Error() string { 36 return "Invalid spawn options:" + bsoe.message 37 } 38 39 // Spawn handles Spawning hosts for users. 40 type Spawn struct { 41 settings *evergreen.Settings 42 } 43 44 // Options holds the required parameters for spawning a host. 45 type Options struct { 46 Distro string 47 UserName string 48 PublicKey string 49 UserData string 50 TaskId string 51 } 52 53 // New returns an initialized Spawn controller. 54 func New(settings *evergreen.Settings) Spawn { 55 return Spawn{settings} 56 } 57 58 // Validate returns an instance of BadOptionsErr if the SpawnOptions object contains invalid 59 // data, SpawnLimitErr if the user is already at the spawned host limit, or some other untyped 60 // instance of Error if something fails during validation. 61 func (sm Spawn) Validate(so Options) error { 62 d, err := distro.FindOne(distro.ById(so.Distro)) 63 if err != nil { 64 return BadOptionsErr{fmt.Sprintf("Invalid dist %v", so.Distro)} 65 } 66 67 if !d.SpawnAllowed { 68 return BadOptionsErr{fmt.Sprintf("Spawning not allowed for dist %v", so.Distro)} 69 } 70 71 // if the user already has too many active spawned hosts, deny the request 72 activeSpawnedHosts, err := host.Find(host.ByUserWithRunningStatus(so.UserName)) 73 if err != nil { 74 return errors.Wrap(err, "Error occurred finding user's current hosts") 75 } 76 77 if len(activeSpawnedHosts) >= MaxPerUser { 78 return SpawnLimitErr 79 } 80 81 // validate public key 82 rsa := "ssh-rsa" 83 dss := "ssh-dss" 84 isRSA := strings.HasPrefix(so.PublicKey, rsa) 85 isDSS := strings.HasPrefix(so.PublicKey, dss) 86 if !isRSA && !isDSS { 87 return BadOptionsErr{"key does not start with ssh-rsa or ssh-dss"} 88 } 89 90 sections := strings.Split(so.PublicKey, " ") 91 if len(sections) < 2 { 92 keyType := rsa 93 if sections[0] == dss { 94 keyType = dss 95 } 96 return BadOptionsErr{fmt.Sprintf("missing space after '%v'", keyType)} 97 } 98 99 // check for valid base64 100 if _, err = base64.StdEncoding.DecodeString(sections[1]); err != nil { 101 return BadOptionsErr{"key contains invalid base64 string"} 102 } 103 104 if d.UserData.File != "" { 105 if strings.TrimSpace(so.UserData) == "" { 106 return BadOptionsErr{} 107 } 108 109 var err error 110 switch d.UserData.Validate { 111 case distro.UserDataFormatFormURLEncoded: 112 _, err = url.ParseQuery(so.UserData) 113 case distro.UserDataFormatJSON: 114 var out map[string]interface{} 115 err = json.Unmarshal([]byte(so.UserData), &out) 116 case distro.UserDataFormatYAML: 117 var out map[string]interface{} 118 err = yaml.Unmarshal([]byte(so.UserData), &out) 119 } 120 121 if err != nil { 122 return BadOptionsErr{fmt.Sprintf("invalid %v: %v", d.UserData.Validate, err)} 123 } 124 } 125 return nil 126 } 127 128 // CreateHost spawns a host with the given options. 129 func (sm Spawn) CreateHost(so Options, owner *user.DBUser) error { 130 131 // load in the appropriate distro 132 d, err := distro.FindOne(distro.ById(so.Distro)) 133 if err != nil { 134 return errors.WithStack(err) 135 } 136 // add any extra user-specified data into the setup script 137 if d.UserData.File != "" { 138 userDataCmd := fmt.Sprintf("echo \"%v\" > %v\n", 139 strings.Replace(so.UserData, "\"", "\\\"", -1), d.UserData.File) 140 // prepend the setup script to add the userdata file 141 if strings.HasPrefix(d.Setup, "#!") { 142 firstLF := strings.Index(d.Setup, "\n") 143 d.Setup = d.Setup[0:firstLF+1] + userDataCmd + d.Setup[firstLF+1:] 144 } else { 145 d.Setup = userDataCmd + d.Setup 146 } 147 } 148 149 // modify the setup script to add the user's public key 150 d.Setup += fmt.Sprintf("\necho \"\n%v\" >> ~%v/.ssh/authorized_keys\n", so.PublicKey, d.User) 151 152 // replace expansions in the script 153 exp := command.NewExpansions(sm.settings.Expansions) 154 d.Setup, err = exp.ExpandString(d.Setup) 155 if err != nil { 156 return errors.Wrap(err, "expansions error") 157 } 158 159 // fake out replacing spot instances with on-demand equivalents 160 if d.Provider == ec2.SpotProviderName { 161 d.Provider = ec2.OnDemandProviderName 162 } 163 164 // get the appropriate cloud manager 165 cloudManager, err := providers.GetCloudManager(d.Provider, sm.settings) 166 if err != nil { 167 return errors.WithStack(err) 168 } 169 170 // spawn the host 171 provisionOptions := &host.ProvisionOptions{ 172 LoadCLI: true, 173 TaskId: so.TaskId, 174 OwnerId: owner.Id, 175 } 176 expiration := DefaultExpiration 177 hostOptions := cloud.HostOptions{ 178 ProvisionOptions: provisionOptions, 179 UserName: so.UserName, 180 ExpirationDuration: &expiration, 181 UserData: so.UserData, 182 UserHost: true, 183 } 184 185 _, err = cloudManager.SpawnInstance(d, hostOptions) 186 return errors.WithStack(err) 187 }