15397: Merge branch 'main'
[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.arvados.org/arvados.git/sdk/go/arvados"
14         "github.com/prometheus/client_golang/prometheus"
15         "github.com/sirupsen/logrus"
16         "golang.org/x/crypto/ssh"
17 )
18
19 // A RateLimitError should be returned by an InstanceSet when the
20 // cloud service indicates it is rejecting all API calls for some time
21 // interval.
22 type RateLimitError interface {
23         // Time before which the caller should expect requests to
24         // fail.
25         EarliestRetry() time.Time
26         error
27 }
28
29 // A QuotaError should be returned by an InstanceSet when the cloud
30 // service indicates the account cannot create more VMs than already
31 // exist.
32 type QuotaError interface {
33         // If true, don't create more instances until some existing
34         // instances are destroyed. If false, don't handle the error
35         // as a quota error.
36         IsQuotaError() bool
37         error
38 }
39
40 // A CapacityError should be returned by an InstanceSet's Create
41 // method when the cloud service indicates it has insufficient
42 // capacity to create new instances -- i.e., we shouldn't retry right
43 // away.
44 type CapacityError interface {
45         // If true, wait before trying to create more instances.
46         IsCapacityError() bool
47         // If true, the condition is specific to the requested
48         // instance types.  Wait before trying to create more
49         // instances of that same type.
50         IsInstanceTypeSpecific() bool
51         error
52 }
53
54 type SharedResourceTags map[string]string
55 type InstanceSetID string
56 type InstanceTags map[string]string
57 type InstanceID string
58 type ImageID string
59
60 // An Executor executes commands on an ExecutorTarget.
61 type Executor interface {
62         // Update the set of private keys used to authenticate to
63         // targets.
64         SetSigners(...ssh.Signer)
65
66         // Set the target used for subsequent command executions.
67         SetTarget(ExecutorTarget)
68
69         // Return the current target.
70         Target() ExecutorTarget
71
72         // Execute a shell command and return the resulting stdout and
73         // stderr. stdin can be nil.
74         Execute(cmd string, stdin io.Reader) (stdout, stderr []byte, err error)
75 }
76
77 var ErrNotImplemented = errors.New("not implemented")
78
79 // An ExecutorTarget is a remote command execution service.
80 type ExecutorTarget interface {
81         // SSH server hostname or IP address, or empty string if
82         // unknown while instance is booting.
83         Address() string
84
85         // Remote username to send during SSH authentication.
86         RemoteUser() string
87
88         // Return nil if the given public key matches the instance's
89         // SSH server key. If the provided Dialer is not nil,
90         // VerifyHostKey can use it to make outgoing network
91         // connections from the instance -- e.g., to use the cloud's
92         // "this instance's metadata" API.
93         //
94         // Return ErrNotImplemented if no verification mechanism is
95         // available.
96         VerifyHostKey(ssh.PublicKey, *ssh.Client) error
97 }
98
99 // Instance is implemented by the provider-specific instance types.
100 type Instance interface {
101         ExecutorTarget
102
103         // ID returns the provider's instance ID. It must be stable
104         // for the life of the instance.
105         ID() InstanceID
106
107         // String typically returns the cloud-provided instance ID.
108         String() string
109
110         // Cloud provider's "instance type" ID. Matches a ProviderType
111         // in the cluster's InstanceTypes configuration.
112         ProviderType() string
113
114         // Get current tags
115         Tags() InstanceTags
116
117         // Replace tags with the given tags
118         SetTags(InstanceTags) error
119
120         // Get recent price history, if available. The InstanceType is
121         // supplied as an argument so the driver implementation can
122         // account for AddedScratch cost without requesting the volume
123         // attachment information from the provider's API.
124         PriceHistory(arvados.InstanceType) []InstancePrice
125
126         // Shut down the node
127         Destroy() error
128 }
129
130 // An InstanceSet manages a set of VM instances created by an elastic
131 // cloud provider like AWS, GCE, or Azure.
132 //
133 // All public methods of an InstanceSet, and all public methods of the
134 // instances it returns, are goroutine safe.
135 type InstanceSet interface {
136         // Create a new instance with the given type, image, and
137         // initial set of tags. If supported by the driver, add the
138         // provided public key to /root/.ssh/authorized_keys.
139         //
140         // The given InitCommand should be executed on the newly
141         // created instance. This is optional for a driver whose
142         // instances' VerifyHostKey() method never returns
143         // ErrNotImplemented. InitCommand will be under 1 KiB.
144         //
145         // The returned error should implement RateLimitError and
146         // QuotaError where applicable.
147         Create(arvados.InstanceType, ImageID, InstanceTags, InitCommand, ssh.PublicKey) (Instance, error)
148
149         // Return all instances, including ones that are booting or
150         // shutting down. Optionally, filter out nodes that don't have
151         // all of the given InstanceTags (the caller will ignore these
152         // anyway).
153         //
154         // An instance returned by successive calls to Instances() may
155         // -- but does not need to -- be represented by the same
156         // Instance object each time. Thus, the caller is responsible
157         // for de-duplicating the returned instances by comparing the
158         // InstanceIDs returned by the instances' ID() methods.
159         Instances(InstanceTags) ([]Instance, error)
160
161         // Stop any background tasks and release other resources.
162         Stop()
163 }
164
165 type InstancePrice struct {
166         StartTime time.Time
167         Price     float64
168 }
169
170 type InitCommand string
171
172 // A Driver returns an InstanceSet that uses the given InstanceSetID
173 // and driver-dependent configuration parameters.
174 //
175 // If the driver creates cloud resources that aren't attached to a
176 // single VM instance (like SSH key pairs on AWS) and support tagging,
177 // they should be tagged with the provided SharedResourceTags.
178 //
179 // The supplied id will be of the form "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
180 // where each z can be any alphanum. The returned InstanceSet must use
181 // this id to tag long-lived cloud resources that it creates, and must
182 // assume control of any existing resources that are tagged with the
183 // same id. Tagging can be accomplished by including the ID in
184 // resource names, using the cloud provider's tagging feature, or any
185 // other mechanism. The tags must be visible to another instance of
186 // the same driver running on a different host.
187 //
188 // The returned InstanceSet must not modify or delete cloud resources
189 // unless they are tagged with the given InstanceSetID or the caller
190 // (dispatcher) calls Destroy() on them. It may log a summary of
191 // untagged resources once at startup, though. Thus, two identically
192 // configured InstanceSets running on different hosts with different
193 // ids should log about the existence of each other's resources at
194 // startup, but will not interfere with each other.
195 //
196 // The dispatcher always passes the InstanceSetID as a tag when
197 // calling Create() and Instances(), so the driver does not need to
198 // tag/filter VMs by InstanceSetID itself.
199 //
200 // Example:
201 //
202 //      type exampleInstanceSet struct {
203 //              ownID     string
204 //              AccessKey string
205 //      }
206 //
207 //      type exampleDriver struct {}
208 //
209 //      func (*exampleDriver) InstanceSet(config json.RawMessage, id cloud.InstanceSetID, tags cloud.SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (cloud.InstanceSet, error) {
210 //              var is exampleInstanceSet
211 //              if err := json.Unmarshal(config, &is); err != nil {
212 //                      return nil, err
213 //              }
214 //              is.ownID = id
215 //              return &is, nil
216 //      }
217 type Driver interface {
218         InstanceSet(config json.RawMessage, id InstanceSetID, tags SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (InstanceSet, error)
219 }
220
221 // DriverFunc makes a Driver using the provided function as its
222 // InstanceSet method. This is similar to http.HandlerFunc.
223 func DriverFunc(fn func(config json.RawMessage, id InstanceSetID, tags SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (InstanceSet, error)) Driver {
224         return driverFunc(fn)
225 }
226
227 type driverFunc func(config json.RawMessage, id InstanceSetID, tags SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (InstanceSet, error)
228
229 func (df driverFunc) InstanceSet(config json.RawMessage, id InstanceSetID, tags SharedResourceTags, logger logrus.FieldLogger, reg *prometheus.Registry) (InstanceSet, error) {
230         return df(config, id, tags, logger, reg)
231 }