Merge branch '20531-cwl-log-tail' refs #20531
[arvados.git] / lib / cloud / cloudtest / cmd.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package cloudtest
6
7 import (
8         "bufio"
9         "errors"
10         "flag"
11         "fmt"
12         "io"
13         "os"
14
15         "git.arvados.org/arvados.git/lib/cloud"
16         "git.arvados.org/arvados.git/lib/cmd"
17         "git.arvados.org/arvados.git/lib/config"
18         "git.arvados.org/arvados.git/lib/dispatchcloud"
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20         "git.arvados.org/arvados.git/sdk/go/ctxlog"
21         "golang.org/x/crypto/ssh"
22 )
23
24 var Command command
25
26 type command struct{}
27
28 func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
29         var err error
30         defer func() {
31                 if err != nil {
32                         fmt.Fprintf(stderr, "%s\n", err)
33                 }
34         }()
35
36         flags := flag.NewFlagSet("", flag.ContinueOnError)
37         flags.SetOutput(stderr)
38         configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
39         instanceSetID := flags.String("instance-set-id", "zzzzz-zzzzz-zzzzzzcloudtest", "InstanceSetID tag `value` to use on the test instance")
40         imageID := flags.String("image-id", "", "Image ID to use when creating the test instance (if empty, use cluster config)")
41         instanceType := flags.String("instance-type", "", "Instance type to create (if empty, use cheapest type in config)")
42         destroyExisting := flags.Bool("destroy-existing", false, "Destroy any existing instances tagged with our InstanceSetID, instead of erroring out")
43         shellCommand := flags.String("command", "", "Run an interactive shell command on the test instance when it boots")
44         pauseBeforeDestroy := flags.Bool("pause-before-destroy", false, "Prompt and wait before destroying the test instance")
45         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
46                 return code
47         }
48         logger := ctxlog.New(stderr, "text", "info")
49         defer func() {
50                 if err != nil {
51                         logger.WithError(err).Error("fatal")
52                         // suppress output from the other error-printing func
53                         err = nil
54                 }
55                 logger.Info("exiting")
56         }()
57
58         loader := config.NewLoader(stdin, logger)
59         loader.Path = *configFile
60         cfg, err := loader.Load()
61         if err != nil {
62                 return 1
63         }
64         cluster, err := cfg.GetCluster("")
65         if err != nil {
66                 return 1
67         }
68         key, err := ssh.ParsePrivateKey([]byte(cluster.Containers.DispatchPrivateKey))
69         if err != nil {
70                 err = fmt.Errorf("error parsing configured Containers.DispatchPrivateKey: %s", err)
71                 return 1
72         }
73         driver, ok := dispatchcloud.Drivers[cluster.Containers.CloudVMs.Driver]
74         if !ok {
75                 err = fmt.Errorf("unsupported cloud driver %q", cluster.Containers.CloudVMs.Driver)
76                 return 1
77         }
78         if *imageID == "" {
79                 *imageID = cluster.Containers.CloudVMs.ImageID
80         }
81         it, err := chooseInstanceType(cluster, *instanceType)
82         if err != nil {
83                 return 1
84         }
85         tags := cloud.SharedResourceTags(cluster.Containers.CloudVMs.ResourceTags)
86         tagKeyPrefix := cluster.Containers.CloudVMs.TagKeyPrefix
87         tags[tagKeyPrefix+"CloudTestPID"] = fmt.Sprintf("%d", os.Getpid())
88         if !(&tester{
89                 Logger:              logger,
90                 Tags:                tags,
91                 TagKeyPrefix:        tagKeyPrefix,
92                 SetID:               cloud.InstanceSetID(*instanceSetID),
93                 DestroyExisting:     *destroyExisting,
94                 ProbeInterval:       cluster.Containers.CloudVMs.ProbeInterval.Duration(),
95                 SyncInterval:        cluster.Containers.CloudVMs.SyncInterval.Duration(),
96                 TimeoutBooting:      cluster.Containers.CloudVMs.TimeoutBooting.Duration(),
97                 Driver:              driver,
98                 DriverParameters:    cluster.Containers.CloudVMs.DriverParameters,
99                 ImageID:             cloud.ImageID(*imageID),
100                 InstanceType:        it,
101                 SSHKey:              key,
102                 SSHPort:             cluster.Containers.CloudVMs.SSHPort,
103                 BootProbeCommand:    cluster.Containers.CloudVMs.BootProbeCommand,
104                 InstanceInitCommand: cloud.InitCommand(cluster.Containers.CloudVMs.InstanceInitCommand),
105                 ShellCommand:        *shellCommand,
106                 PauseBeforeDestroy: func() {
107                         if *pauseBeforeDestroy {
108                                 logger.Info("waiting for operator to press Enter")
109                                 fmt.Fprint(stderr, "Press Enter to continue: ")
110                                 bufio.NewReader(stdin).ReadString('\n')
111                         }
112                 },
113         }).Run() {
114                 return 1
115         }
116         return 0
117 }
118
119 // Return the named instance type, or the cheapest type if name=="".
120 func chooseInstanceType(cluster *arvados.Cluster, name string) (arvados.InstanceType, error) {
121         if len(cluster.InstanceTypes) == 0 {
122                 return arvados.InstanceType{}, errors.New("no instance types are configured")
123         } else if name == "" {
124                 first := true
125                 var best arvados.InstanceType
126                 for _, it := range cluster.InstanceTypes {
127                         if first || best.Price > it.Price {
128                                 best = it
129                                 first = false
130                         }
131                 }
132                 return best, nil
133         } else if it, ok := cluster.InstanceTypes[name]; !ok {
134                 return it, fmt.Errorf("requested instance type %q is not configured", name)
135         } else {
136                 return it, nil
137         }
138 }