-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathmain.go
366 lines (321 loc) · 12.3 KB
/
main.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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"strings"
"syscall"
"github.com/blang/semver"
raven "github.com/getsentry/raven-go"
"github.com/jessevdk/go-flags"
"github.com/weaveworks/launcher/pkg/gcloud"
"github.com/weaveworks/launcher/pkg/kubectl"
"github.com/weaveworks/launcher/pkg/sentry"
"github.com/weaveworks/launcher/pkg/text"
"github.com/weaveworks/launcher/pkg/weavecloud"
)
const (
agentK8sURLTemplate = "{{.Scheme}}://{{.LauncherHostname}}/k8s/agent.yaml" +
"?read-only={{.ReadOnly}}" +
"{{if .CRIEndpoint}}&cri-endpoint={{.CRIEndpoint}}{{end}}"
)
type options struct {
AssumeYes bool `short:"y" long:"assume-yes" description:"Install without user confirmation"`
Scheme string `long:"scheme" description:"Weave Cloud scheme" default:"https"`
LauncherHostname string `long:"wc.launcher" description:"Weave Cloud launcher hostname" default:"get.weave.works"`
WCHostname string `long:"wc.hostname" description:"Weave Cloud hostname" default:"cloud.weave.works"`
Token string `long:"token" description:"Weave Cloud token" required:"true"`
GKE bool `long:"gke" description:"Create clusterrolebinding for GKE instances"`
ReportErrors bool `long:"report-errors" description:"Should install errors be reported to sentry"`
SkipChecks bool `long:"skip-checks" description:"Skip pre-flight checks"`
ReadOnly bool `long:"read-only" description:"Disallow scope controls"`
CRIEndpoint string `long:"cri-endpoint" description:"Set custom Container Runtime Interface (CRI) endpoint. e.g.: '/var/run/crio/crio.sock'"`
}
func init() {
// https://sentry.io/weaveworks/launcher-bootstrap/
raven.SetDSN("https://44cf71b08710447888c993011b1302fc:[email protected]/288665")
}
func main() {
raven.CapturePanicAndWait(mainImpl, nil)
}
func mainImpl() {
opts := options{}
// Parse arguments with go-flags so we can forward unknown arguments to kubectl
parser := flags.NewParser(&opts, flags.IgnoreUnknown)
otherArgs, err := parser.Parse()
if err != nil {
exitWithCapture(opts, "%s\n", err)
}
raven.SetTagsContext(map[string]string{
"weave_cloud_scheme": opts.Scheme,
"weave_cloud_launcher": opts.LauncherHostname,
"weave_cloud_hostname": opts.WCHostname,
})
// Due to some users Kubernetes clusters having invalid, e.g. self-signed,
// certificates, we default to skipping the certificate validation.
otherArgs = append(otherArgs, "--insecure-skip-tls-verify")
kubectlClient := kubectl.LocalClient{
GlobalArgs: otherArgs,
}
if !kubectlClient.IsPresent() {
exitWithCapture(opts, "Could not find kubectl in PATH, please install it: https://kubernetes.io/docs/tasks/tools/install-kubectl/\n")
}
// If the user has not passed the container runtime endpoint
// we try our best to guess and log it.
if opts.CRIEndpoint == "" {
opts.CRIEndpoint, err = matchContainerRuntimeEndpoint(kubectlClient)
if err != nil {
log.Fatal("error detecting container runtime endpoint: ", err)
}
if opts.CRIEndpoint != "" {
fmt.Printf("Detected container runtime endpoint: %s. To override the container runtime endpoint set the '--cri-endpoint=<ENDPOINT>' flag.\n", opts.CRIEndpoint)
}
}
agentK8sURL, err := text.ResolveString(agentK8sURLTemplate, opts)
if err != nil {
log.Fatal("invalid URL template:", err)
}
wcOrgLookupURL, err := text.ResolveString(weavecloud.DefaultWCOrgLookupURLTemplate, opts)
if err != nil {
log.Fatal("invalid URL template:", err)
}
// Restore stdin, making fd 0 point at the terminal
if err := syscall.Dup2(1, 0); err != nil {
exitWithCapture(opts, "Could not restore stdin: %s\n", err)
}
fmt.Println("Preparing for Weave Cloud setup")
// Capture the kubernetes version info to help debug issues
checkK8sVersion(kubectlClient, opts) // NB exits on error
InstanceID, InstanceName, err := weavecloud.LookupInstanceByToken(wcOrgLookupURL, opts.Token)
if err != nil {
exitWithCapture(opts, "Error looking up Weave Cloud instance: %s\n", err)
}
raven.SetTagsContext(map[string]string{"instance": InstanceID})
fmt.Printf("Connecting cluster to %q (id: %s) on Weave Cloud\n", InstanceName, InstanceID)
// Display information on the cluster we're about to install the agent onto.
//
// This relies on having a current-context defined and is only to try to be
// user friendly. So, in case of errors (eg. no current-context) we simply
// assume kubectl can reach the API server eg. through a previously set up api
// server proxy with kubectl proxy.
cluster, err := kubectl.GetClusterInfo(kubectlClient)
if err == nil {
fmt.Printf("Installing Weave Cloud agents on %s at %s\n", cluster.Name, cluster.ServerAddress)
}
if opts.GKE {
err := createGKEClusterRoleBinding(kubectlClient)
if err != nil {
raven.SetTagsContext(map[string]string{
"gke_clusterrolebindingError": err.Error(),
})
errText := err.Error()
if strings.Contains(errText, "Forbidden") || strings.Contains(errText, "forbidden") {
exitWithCapture(opts, "Could not create clusterrolebinding. GKE role \"Kubernetes Engine Admin\" (containers.admin) required to create resources.\n%s\n", err)
}
}
}
if !opts.SkipChecks {
// Perform a check to make sure DNS is working correctly.
fmt.Println("Performing a check of the Kubernetes installation setup.")
ok, err := kubectl.TestDNS(kubectlClient, "cloud.weave.works")
if err != nil {
exitWithCapture(opts, "There was an error while performing a DNS check: %s. Please check that your cluster can download images and run pods.", err)
}
// We exit if the DNS pods are not up and running, as the installer needs to be
// able to connect to the server to correctly setup the needed resources.
if !ok {
exitWithCapture(opts, "DNS is not working in this Kubernetes cluster. We require correct DNS setup to continue.")
}
}
secretCreated, err := kubectl.CreateSecretFromLiteral(kubectlClient, "weave", "weave-cloud", "token", opts.Token, opts.AssumeYes)
if err != nil {
exitWithCapture(opts, "There was an error creating the secret: %s\n", err)
}
if !secretCreated {
currentToken, err := kubectl.GetSecretValue(kubectlClient, "weave", "weave-cloud", "token")
if err != nil {
exitWithCapture(opts, "There was an error checking the current secret: %s\n", err)
}
if currentToken != opts.Token {
currentInstanceID, currentInstanceName, errCurrent := weavecloud.LookupInstanceByToken(wcOrgLookupURL, currentToken)
msg := "This cluster is currently connected to "
if errCurrent == nil {
msg += fmt.Sprintf("%q (id: %s) on Weave Cloud", currentInstanceName, currentInstanceID)
} else {
msg += "a different Weave Cloud instance."
}
confirmed, err := askForConfirmation(fmt.Sprintf(
"\n%s\nWould you like to continue and connect this cluster to %q (id: %s) instead?", msg, InstanceName, InstanceID))
if err != nil {
exitWithCapture(opts, "Could not ask for confirmation: %s\n", err)
} else if !confirmed {
exitWithCapture(opts, "Installation cancelled")
}
_, err = kubectl.CreateSecretFromLiteral(kubectlClient, "weave", "weave-cloud", "token", opts.Token, true)
if err != nil {
exitWithCapture(opts, "There was an error creating the secret: %s\n", err)
}
}
}
// Apply the agent
err = kubectl.Apply(kubectlClient, agentK8sURL)
if err != nil {
captureAndSend(opts, 1, "There was an error applying the agent: %s\n", err)
// We've failed to apply the agent. kubectl apply isn't an atomic operation
// can leave some objects behind when encountering an error. Clean things up.
fmt.Println("Rolling back cluster changes")
kubectl.Execute(kubectlClient, "delete", "--ignore-not-found=true", "-f", agentK8sURL)
// Exit with a specific error which will be checked against in install script,
// as a way of deduplicating sending of these errors.
os.Exit(111)
}
fmt.Println("Successfully installed.")
}
func captureAndSend(opts options, skipFrames uint, msg string, args ...interface{}) {
formatted := fmt.Sprintf(msg, args...)
fmt.Fprintf(os.Stderr, formatted)
// Send errors to UI.
if opts.Scheme != "" {
sendError(formatted, opts)
}
if opts.ReportErrors {
sentry.CaptureAndWait(skipFrames, formatted, nil)
}
}
func exitWithCapture(opts options, msg string, args ...interface{}) {
captureAndSend(opts, 2, msg, args...)
// Exit with a specific error which will be checked against in install script,
// as a way of deduplicating sending of these errors.
os.Exit(111)
}
func createGKEClusterRoleBinding(kubectlClient kubectl.Client) error {
if !gcloud.IsPresent() {
return errors.New("Could not find gcloud in PATH, please install it: https://cloud.google.com/sdk/docs/")
}
account, err := gcloud.GetConfigValue("core/account")
if err != nil || account == "" {
return errors.New("Could not find gcloud account. Please run: gcloud auth login `ACCOUNT`")
}
hostUser := os.Getenv("USER")
err = kubectl.CreateClusterRoleBinding(
kubectlClient,
fmt.Sprintf("cluster-admin-%s", hostUser),
"cluster-admin",
account,
)
if err != nil {
return err
}
return nil
}
func askForConfirmation(s string) (bool, error) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
return false, err
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true, nil
} else if response == "n" || response == "no" {
return false, nil
}
}
}
func checkK8sVersion(kubectlClient kubectl.Client, opts options) {
fmt.Println("Checking kubectl & kubernetes versions")
clientVersion, serverVersion, err := kubectl.GetVersionInfo(kubectlClient)
if clientVersion != "" {
raven.SetTagsContext(map[string]string{
"kubectl_clientVersion_gitVersion": clientVersion,
})
if serverVersion == "" {
exitWithCapture(opts, "Error checking your kubernetes server version: %v. Please check that you can connect to your cluster by running \"kubectl version\".\n", err)
} else {
raven.SetTagsContext(map[string]string{
"kubectl_serverVersion_gitVersion": serverVersion,
})
}
} else {
exitWithCapture(opts, "Error checking kubernetes version info: %v. Please check your environment for problems by running \"kubectl version\".\n", err)
}
// Validate cluster version of at least 1.6.0
if !supportedK8sVersion(serverVersion) {
exitWithCapture(opts, "Kubernetes version %s not supported. We require Kubernetes cluster to be version 1.6.0 or newer.\n", serverVersion)
}
}
func supportedK8sVersion(clusterVersion string) bool {
versionStr := strings.TrimLeft(clusterVersion, "v")
version, err := semver.Parse(versionStr)
if err != nil {
return false
}
// We only support clusters 1.6.0 and higher.
if version.Major == 1 && version.Minor < 6 {
return false
}
return true
}
type errorResponse struct {
Type string `json:"type"`
Messages messages `json:"messages"`
}
type messages struct {
Browser browser `json:"browser"`
}
type browser struct {
Type string `json:"type"`
Text string `json:"text"`
}
// sendError sends the error msg to UI.
func sendError(errMsg string, opts options) {
eventTypeFailed := "onboarding_failed"
response := errorResponse{
Type: eventTypeFailed,
Messages: messages{
Browser: browser{
Type: eventTypeFailed,
Text: errMsg,
},
},
}
jr, err := json.Marshal(response)
if err != nil {
return
}
url := fmt.Sprintf("%s://%s/api/notification/external/events", opts.Scheme, opts.WCHostname)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jr))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", opts.Token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
}
// matchContainerRuntimeEndpoint tries to best match the container runtime the node
// is using based on the container runtime name.
func matchContainerRuntimeEndpoint(c kubectl.Client) (string, error) {
name, err := kubectl.GetContainerRuntimeName(c)
if err != nil {
return "", err
}
endpoints := map[string]string{
"cri-o": "/var/run/crio/crio.sock",
"containerd": "/run/containerd/containerd.sock",
// TODO: figure out when docker CR name is the dockershim via CRI
// "docker": "/var/run/dockershim.sock",
}
return endpoints[name], nil
}