14291: Generate key fingerprints that work with AWS. Filter out terminated instances
[arvados.git] / lib / cloud / interfaces.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package cloud
6
7 import (
8         "encoding/json"
9         "errors"
10         "io"
11         "time"
12
13         "git.curoverse.com/arvados.git/sdk/go/arvados"
14         "github.com/sirupsen/logrus"
15         "golang.org/x/crypto/ssh"
16 )
17
18 // A RateLimitError should be returned by an InstanceSet when the
19 // cloud service indicates it is rejecting all API calls for some time
20 // interval.
21 type RateLimitError interface {
22         // Time before which the caller should expect requests to
23         // fail.
24         EarliestRetry() time.Time
25         error
26 }
27
28 // A QuotaError should be returned by an InstanceSet when the cloud
29 // service indicates the account cannot create more VMs than already
30 // exist.
31 type QuotaError interface {
32         // If true, don't create more instances until some existing
33         // instances are destroyed. If false, don't handle the error
34         // as a quota error.
35         IsQuotaError() bool
36         error
37 }
38
39 type InstanceSetID string
40 type InstanceTags map[string]string
41 type InstanceID string
42 type ImageID string
43
44 // An Executor executes commands on an ExecutorTarget.
45 type Executor interface {
46         // Update the set of private keys used to authenticate to
47         // targets.
48         SetSigners(...ssh.Signer)
49
50         // Set the target used for subsequent command executions.
51         SetTarget(ExecutorTarget)
52
53         // Return the current target.
54         Target() ExecutorTarget
55
56         // Execute a shell command and return the resulting stdout and
57         // stderr. stdin can be nil.
58         Execute(cmd string, stdin io.Reader) (stdout, stderr []byte, err error)
59 }
60
61 var ErrNotImplemented = errors.New("not implemented")
62
63 // An ExecutorTarget is a remote command execution service.
64 type ExecutorTarget interface {
65         // SSH server hostname or IP address, or empty string if
66         // unknown while instance is booting.
67         Address() string
68
69         // Remote username to send during SSH authentication.
70         RemoteUser() string
71
72         // Return nil if the given public key matches the instance's
73         // SSH server key. If the provided Dialer is not nil,
74         // VerifyHostKey can use it to make outgoing network
75         // connections from the instance -- e.g., to use the cloud's
76         // "this instance's metadata" API.
77         //
78         // Return ErrNotImplemented if no verification mechanism is
79         // available.
80         VerifyHostKey(ssh.PublicKey, *ssh.Client) error
81 }
82
83 // Instance is implemented by the provider-specific instance types.
84 type Instance interface {
85         ExecutorTarget
86
87         // ID returns the provider's instance ID. It must be stable
88         // for the life of the instance.
89         ID() InstanceID
90
91         // String typically returns the cloud-provided instance ID.
92         String() string
93
94         // Cloud provider's "instance type" ID. Matches a ProviderType
95         // in the cluster's InstanceTypes configuration.
96         ProviderType() string
97
98         // Get current tags
99         Tags() InstanceTags
100
101         // Replace tags with the given tags
102         SetTags(InstanceTags) error
103
104         // Shut down the node
105         Destroy() error
106 }
107
108 // An InstanceSet manages a set of VM instances created by an elastic
109 // cloud provider like AWS, GCE, or Azure.
110 //
111 // All public methods of an InstanceSet, and all public methods of the
112 // instances it returns, are goroutine safe.
113 type InstanceSet interface {
114         // Create a new instance with the given type, image, and
115         // initial set of tags. If supported by the driver, add the
116         // provided public key to /root/.ssh/authorized_keys.
117         //
118         // The given InitCommand should be executed on the newly
119         // created instance. This is optional for a driver whose
120         // instances' VerifyHostKey() method never returns
121         // ErrNotImplemented. InitCommand will be under 1 KiB.
122         //
123         // The returned error should implement RateLimitError and
124         // QuotaError where applicable.
125         Create(arvados.InstanceType, ImageID, InstanceTags, InitCommand, ssh.PublicKey) (Instance, error)
126
127         // Return all instances, including ones that are booting or
128         // shutting down. Optionally, filter out nodes that don't have
129         // all of the given InstanceTags (the caller will ignore these
130         // anyway).
131         //
132         // An instance returned by successive calls to Instances() may
133         // -- but does not need to -- be represented by the same
134         // Instance object each time. Thus, the caller is responsible
135         // for de-duplicating the returned instances by comparing the
136         // InstanceIDs returned by the instances' ID() methods.
137         Instances(InstanceTags) ([]Instance, error)
138
139         // Stop any background tasks and release other resources.
140         Stop()
141 }
142
143 type InitCommand string
144
145 // A Driver returns an InstanceSet that uses the given InstanceSetID
146 // and driver-dependent configuration parameters.
147 //
148 // The supplied id will be of the form "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
149 // where each z can be any alphanum. The returned InstanceSet must use
150 // this id to tag long-lived cloud resources that it creates, and must
151 // assume control of any existing resources that are tagged with the
152 // same id. Tagging can be accomplished by including the ID in
153 // resource names, using the cloud provider's tagging feature, or any
154 // other mechanism. The tags must be visible to another instance of
155 // the same driver running on a different host.
156 //
157 // The returned InstanceSet must ignore existing resources that are
158 // visible but not tagged with the given id, except that it should log
159 // a summary of such resources -- only once -- when it starts
160 // up. Thus, two identically configured InstanceSets running on
161 // different hosts with different ids should log about the existence
162 // of each other's resources at startup, but will not interfere with
163 // each other.
164 //
165 // Example:
166 //
167 //      type exampleInstanceSet struct {
168 //              ownID     string
169 //              AccessKey string
170 //      }
171 //
172 //      type exampleDriver struct {}
173 //
174 //      func (*exampleDriver) InstanceSet(config json.RawMessage, id InstanceSetID) (InstanceSet, error) {
175 //              var is exampleInstanceSet
176 //              if err := json.Unmarshal(config, &is); err != nil {
177 //                      return nil, err
178 //              }
179 //              is.ownID = id
180 //              return &is, nil
181 //      }
182 //
183 //      var _ = registerCloudDriver("example", &exampleDriver{})
184 type Driver interface {
185         InstanceSet(config json.RawMessage, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)
186 }
187
188 // DriverFunc makes a Driver using the provided function as its
189 // InstanceSet method. This is similar to http.HandlerFunc.
190 func DriverFunc(fn func(config json.RawMessage, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)) Driver {
191         return driverFunc(fn)
192 }
193
194 type driverFunc func(config json.RawMessage, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)
195
196 func (df driverFunc) InstanceSet(config json.RawMessage, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error) {
197         return df(config, id, logger)
198 }