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