-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
build.go
346 lines (290 loc) · 8.3 KB
/
build.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package cmd
import (
"errors"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/eezhee/eezhee/pkg/config"
"github.com/eezhee/eezhee/pkg/core"
"github.com/eezhee/eezhee/pkg/k3s"
homedir "github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)
const statusCheckDelay = 2 * time.Second // time between checks on status of VM
const launchDelay = 10 * time.Second // time from when provider says vm ready to us ssh'ing in
func init() {
rootCmd.AddCommand(buildCmd)
}
var buildCmd = &cobra.Command{
Use: "build",
Short: "Print the version number of Eezhee",
Long: `All software has versions. This is Eezhee's`,
Run: func(cmd *cobra.Command, args []string) {
err := buildCluster()
if err != nil {
log.Error(err)
os.Exit(1)
}
},
}
// buildVM will create a cluster
func buildCluster() error {
// see which cloud we have an api token for
defaultCloud := AppConfig.GetDefaultCloud()
if len(defaultCloud) == 0 {
// opps, no api keys specified so can't proceed until resolved
return errors.New("no cloud provider configured. User 'eezhee auth add'")
}
// make sure the cluster doesn't already exist
// is there a deploy state file
deployState := config.NewDeployState()
if deployState.FileExists() {
return errors.New("cluster already running (as per deploy-state file)")
}
// nope, so we are clear to create a new cluster
// is there a deploy config file
deployConfig := config.NewDeployConfig()
if deployConfig.FileExists() {
err := deployConfig.Load()
if err != nil {
// there is a file but we couldn't load it
return err
}
}
// make sure we have a name for the cluster
// if not set, create a name
if len(deployConfig.Name) == 0 {
deployConfig.Name, _ = buildClusterName()
}
// load ssh key we will use
var sshKey core.SSHKey
dir, _ := homedir.Dir()
publicKeyFile := dir + "/.ssh/id_rsa.pub"
// see if file exists
_, err := os.Stat(publicKeyFile)
if os.IsNotExist(err) {
// need to generate an ssh key
err = sshKey.GenerateNewKey(publicKeyFile)
if err != nil {
return err
}
}
err = sshKey.LoadPublicKey(publicKeyFile)
if err != nil {
return err
}
deployConfig.SSHPublicKey = sshKey.GetPublicKey()
// does config specify which cloud to use
// if not, use one that we have credentials for
if len(deployConfig.Cloud) == 0 {
deployConfig.Cloud = defaultCloud
}
// make sure we have a valid cloud
switch deployConfig.Cloud {
case "digitalocean", "linode", "vultr":
// case "gcloud":
// case "azure":
// case "aws":
default:
return errors.New("no or invalid cloud specified")
}
log.Info("deploying to ", deployConfig.Cloud)
// has a release of k3s been specified?
// if not, use latest stable release
if len(deployConfig.K3sVersion) == 0 {
deployConfig.K3sVersion = "stable"
}
// we're pretty flexibly in how release is specified.
// could be 'stable', 'latest', validChannelName (v1.19) or validReleaseName (v1.19.3)
// as a result, we need to translate it to exactly which release to install
k3sManager := k3s.NewManager()
release, err := k3sManager.Releases.Translate(deployConfig.K3sVersion)
if err != nil {
return err
}
deployConfig.K3sVersion = release
// ok validation completed, time to get building
// create a manager for desired cloud
vmManager, err := GetManager(deployConfig.Cloud)
if err != nil {
log.Error(err)
return err
}
// TODO: for DO, should upload it if not there yet
// make sure this ssh key is loaded into cloud platform
_, err = vmManager.IsSSHKeyUploaded(sshKey)
if err != nil {
_, err = vmManager.UploadSSHKey("eezhee", sshKey)
if err != nil {
return err
}
// else we're all good and can continue
}
// TODO: need to valide if it a valid region for given cloud
// set rest of details for new VM
if len(deployConfig.Region) == 0 {
log.Info("selecting closest region")
deployConfig.Region, err = vmManager.SelectClosestRegion()
if err != nil {
return err
}
log.Info("using ", deployConfig.Region)
}
// TODO - allow config to specify size/type
// TODO - translate generic size/type to provider specific
var imageName string
switch deployConfig.Cloud {
case "digitalocean":
if len(deployConfig.Size) == 0 {
deployConfig.Size = "s-1vcpu-1gb"
}
imageName = "ubuntu-20-04-x64"
case "linode":
if len(deployConfig.Size) == 0 {
deployConfig.Size = "g6-nanode-1"
}
imageName = "linode/ubuntu20.04"
case "vultr":
if len(deployConfig.Size) == 0 {
deployConfig.Size = "vc2-1c-1gb" // $5/month
}
imageName = "387" // ubuntu 20.04
}
// time to create the VM
log.Info("creating a VM")
vmInfo, err := vmManager.CreateVM(
deployConfig.Name, imageName, deployConfig.Size,
deployConfig.Region, sshKey,
)
if err != nil {
return err
}
vmID := vmInfo.ID
status := vmInfo.Status
// see if vm ready. if not need to wait as don't have IP yet
// all providers have their own status messages
// the only one we standardize is the final one
// provider needs to convert to "running"
lastStatus := ""
for strings.Compare(status, "running") != 0 {
// wait a bit
time.Sleep(statusCheckDelay)
vmInfo, err = vmManager.GetVMInfo(vmID)
if err != nil {
// TODO: vm has been created, really should delete (or should we add retry to getVMInfo?)
return err
}
status = vmInfo.Status
// print status if it has changed since last time
if strings.Compare(lastStatus, status) != 0 {
log.Info("vm in ", status, " state")
lastStatus = status
}
}
// some VMs have multiple IPs (internal and public)
// we just need the public one
vmPublicIP, err := vmInfo.GetPublicIP()
if err != nil {
return err
}
// save current state
deployState.Cloud = deployConfig.Cloud
deployState.ID = vmInfo.ID
deployState.Name = vmInfo.Name
deployState.Region = vmInfo.Region.Slug
deployState.Size = vmInfo.Size.Slug
deployState.IP = vmPublicIP
// TODO save public key
deployState.SSHPublicKey = deployConfig.SSHPublicKey
err = deployState.Save()
if err != nil {
return err
}
// pause as ssh might not be ready
time.Sleep(launchDelay)
// TODO: refactor this into another function
// install k3s on the VM
k3sVersion := deployConfig.K3sVersion
log.Info("installing k3s release ", k3sVersion)
k3sManager.Install(vmPublicIP, k3sVersion, deployConfig.Name)
// done, cluster up and running
// update state file & re-save
deployState.K3sVersion = k3sVersion
err = deployState.Save()
if err != nil {
return err
}
log.Info("saved cluster details to 'deploy-state.yaml'")
// TODO: what about installing ingress?
return nil
}
// figure out what to call k3s cluster
// based on combo of app name and git branch (if not master
// eg webapp, webapp-staging, webapp-newFeatureBranch)
func buildClusterName() (string, error) {
clusterName := ""
appName, _ := getCurrentDir()
branchName, err := getGitBranchName()
if err != nil {
log.Error(err)
return "", err
}
if len(branchName) > 0 {
clusterName = appName + "-" + branchName
} else {
clusterName = appName + branchName
}
// check for invalid characters
clusterName = strings.ReplaceAll(clusterName, "_", "-")
return clusterName, nil
}
func getCurrentDir() (string, error) {
fullDirPath, _ := os.Getwd()
currentDir := filepath.Base(fullDirPath)
return currentDir, nil
}
func getGitBranchName() (string, error) {
// see if there is a .git subdir
command := "git"
cmd := exec.Command(command, "rev-parse", "--abbrev-ref", "HEAD")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
branchName := string(stdoutStderr)
branchName = strings.TrimSpace(branchName)
if strings.Compare(branchName, "master") == 0 {
return "", nil
}
return branchName, nil
}
// LoadSSHPublicKey will load a ssh key
func LoadSSHPublicKey() {
dir, _ := homedir.Dir()
keyFile := dir + "/.ssh/id_rsa.pub"
data, err := ioutil.ReadFile(keyFile)
if err != nil {
return
}
publicKey := string(data)
log.Debug(publicKey)
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(data))
if err != nil {
panic(err)
}
log.Debug(pk)
original := string(ssh.MarshalAuthorizedKey(pk))
log.Debug(original)
if strings.Compare(publicKey, original) == 0 {
// can't do this as publicKey has email address at end of string
log.Error("can go back and forth")
}
log.Debug(pk.Type())
fingerprint := ssh.FingerprintLegacyMD5(pk)
log.Debug(fingerprint)
}