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