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