This repository has been archived by the owner on Feb 4, 2022. It is now read-only.
forked from juju/juju
-
Notifications
You must be signed in to change notification settings - Fork 1
/
api.go
370 lines (330 loc) · 11.4 KB
/
api.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
// Copyright 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package juju
import (
"net"
"reflect"
"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/names/v4"
"github.com/juju/juju/api"
"github.com/juju/juju/core/network"
"github.com/juju/juju/jujuclient"
"github.com/juju/juju/proxy"
)
var logger = loggo.GetLogger("juju.juju")
// NewAPIConnectionParams contains the parameters for creating a new Juju API
// connection.
type NewAPIConnectionParams struct {
// ControllerName is the name of the controller to connect to.
ControllerName string
// Store is the jujuclient.ClientStore from which the controller's
// details will be fetched, and updated on address changes.
Store jujuclient.ClientStore
// OpenAPI is the function that will be used to open API connections.
OpenAPI api.OpenFunc
// DialOpts contains the options used to dial the API connection.
DialOpts api.DialOpts
// AccountDetails contains the account details to use for logging
// in to the Juju API. If this is nil, then no login will take
// place. If AccountDetails.Password and AccountDetails.Macaroon
// are zero, the login will be as an external user.
AccountDetails *jujuclient.AccountDetails
// ModelUUID is an optional model UUID. If specified, the API connection
// will be scoped to the model with that UUID; otherwise it will be
// scoped to the controller.
ModelUUID string
}
var errNoAddresses = errors.New("no API addresses")
// IsNoAddressesError reports whether the error (from NewAPIConnection) is an
// error due to the controller having no API addresses yet (likely because a
// bootstrap is still in progress).
func IsNoAddressesError(err error) bool {
return errors.Cause(err) == errNoAddresses
}
// NewAPIConnection returns an api.Connection to the specified Juju controller,
// with specified account credentials, optionally scoped to the specified model
// name.
func NewAPIConnection(args NewAPIConnectionParams) (_ api.Connection, err error) {
if args.OpenAPI == nil {
args.OpenAPI = api.Open
}
apiInfo, controller, err := connectionInfo(args)
if err != nil {
return nil, errors.Annotatef(err, "cannot work out how to connect")
}
if len(apiInfo.Addrs) == 0 {
return nil, errNoAddresses
}
// Copy the cache so we'll know whether it's changed so that
// we'll update the entry correctly.
dnsCache := dnsCacheMap(controller.DNSCache).copy()
args.DialOpts.DNSCache = dnsCache
logger.Infof("connecting to API addresses: %v", apiInfo.Addrs)
st, err := args.OpenAPI(apiInfo, args.DialOpts)
if err != nil {
redirErr, ok := errors.Cause(err).(*api.RedirectError)
if !ok || !redirErr.FollowRedirect {
return nil, errors.Trace(err)
}
// We've been told to connect to a different API server,
// so do so. Note that we don't copy the account details
// because the account on the redirected server may well
// be different - we'll use macaroon authentication
// directly without sending account details.
// Copy the API info because it's possible that the
// apiConfigConnect is still using it concurrently.
apiInfo = &api.Info{
ModelTag: apiInfo.ModelTag,
Addrs: usableHostPorts(redirErr.Servers).Strings(),
CACert: redirErr.CACert,
ControllerUUID: apiInfo.ControllerUUID,
}
st, err = args.OpenAPI(apiInfo, args.DialOpts)
if err != nil {
return nil, errors.Annotatef(err, "cannot connect to redirected address")
}
// TODO(rog) update cached model addresses.
// TODO(rog) should we do something with the logged-in username?
return st, nil
}
defer func() {
if err != nil {
_ = st.Close()
}
}()
// Update API addresses if they've changed. Error is non-fatal.
// Note that in the redirection case, we won't update the addresses
// of the controller we first connected to. This shouldn't be
// a problem in practice because the intended scenario for
// controllers that redirect involves them having well known
// public addresses that won't change over time.
hostPorts := st.APIHostPorts()
agentVersion := ""
if v, ok := st.ServerVersion(); ok {
agentVersion = v.String()
}
params := UpdateControllerParams{
AgentVersion: agentVersion,
AddrConnectedTo: st.Addr(),
IPAddrConnectedTo: st.IPAddr(),
CurrentHostPorts: hostPorts,
DNSCache: dnsCache,
}
if host := st.PublicDNSName(); host != "" {
params.PublicDNSName = &host
}
err = updateControllerDetailsFromLogin(args.Store, args.ControllerName, controller, params)
if err != nil {
logger.Errorf("cannot cache API addresses: %v", err)
}
// Process the account details obtained from login.
var accountDetails *jujuclient.AccountDetails
user, ok := st.AuthTag().(names.UserTag)
if !apiInfo.SkipLogin {
if ok {
if accountDetails, err = args.Store.AccountDetails(args.ControllerName); err != nil {
if !errors.IsNotFound(err) {
logger.Errorf("cannot load local account information: %v", err)
}
} else {
accountDetails.LastKnownAccess = st.ControllerAccess()
}
}
if ok && !user.IsLocal() && apiInfo.Tag == nil {
// We used macaroon auth to login; save the username
// that we've logged in as.
accountDetails = &jujuclient.AccountDetails{
User: user.Id(),
LastKnownAccess: st.ControllerAccess(),
}
} else if apiInfo.Tag == nil {
logger.Errorf("unexpected logged-in username %v", st.AuthTag())
}
}
if accountDetails != nil {
if err := args.Store.UpdateAccount(args.ControllerName, *accountDetails); err != nil {
logger.Errorf("cannot update account information: %v", err)
}
}
return st, nil
}
// connectionInfo returns connection information suitable for
// connecting to the controller and model specified in the given
// parameters. If there are no addresses known for the controller,
// it may return a *api.Info with no APIEndpoints, but all other
// information will be populated.
func connectionInfo(args NewAPIConnectionParams) (*api.Info, *jujuclient.ControllerDetails, error) {
controller, err := args.Store.ControllerByName(args.ControllerName)
if err != nil {
return nil, nil, errors.Annotate(err, "cannot get controller details")
}
apiInfo := &api.Info{
Addrs: controller.APIEndpoints,
CACert: controller.CACert,
ControllerUUID: controller.ControllerUUID,
}
if controller.Proxy != nil {
apiInfo.Proxier = controller.Proxy.Proxier
}
if args.ModelUUID != "" {
apiInfo.ModelTag = names.NewModelTag(args.ModelUUID)
}
if controller.PublicDNSName != "" {
apiInfo.SNIHostName = controller.PublicDNSName
}
if args.AccountDetails == nil {
apiInfo.SkipLogin = true
return apiInfo, controller, nil
}
account := args.AccountDetails
if account.User != "" {
userTag := names.NewUserTag(account.User)
if userTag.IsLocal() {
apiInfo.Tag = userTag
}
}
if args.AccountDetails.Password != "" {
// If a password is available, we always use that.
// If no password is recorded, we'll attempt to
// authenticate using macaroons.
apiInfo.Password = account.Password
} else {
// Optionally the account may have macaroons to use.
apiInfo.Macaroons = account.Macaroons
}
return apiInfo, controller, nil
}
// usableHostPorts returns the input MachineHostPort slice as DialAddresses
// with unusable and non-unique values filtered out.
func usableHostPorts(hps []network.MachineHostPorts) network.HostPorts {
collapsed := network.CollapseToHostPorts(hps)
return collapsed.FilterUnusable().Unique()
}
// addrsChanged reports whether the two slices
// are different. Order is important.
func addrsChanged(a, b []string) bool {
if len(a) != len(b) {
return true
}
for i := range a {
if a[i] != b[i] {
return true
}
}
return false
}
// UpdateControllerParams holds values used to update a controller details
// after bootstrap or a login operation.
type UpdateControllerParams struct {
// AgentVersion is the version of the controller agent.
AgentVersion string
// CurrentHostPorts are the available api addresses.
CurrentHostPorts []network.MachineHostPorts
// AddrConnectedTo (when set) is an API address that has been recently
// connected to.
AddrConnectedTo string
// IPAddrConnected to (when set) is the IP address of AddrConnectedTo
// that has been recently connected to.
IPAddrConnectedTo string
// Proxier
Proxier proxy.Proxier
// DNSCache holds entries in the DNS cache.
DNSCache map[string][]string
// PublicDNSName (when set) holds the public host name of the controller.
PublicDNSName *string
// ControllerMachineCount (when set) is the total number of controller machines in the environment.
ControllerMachineCount *int
// MachineCount (when set) is the total number of machines in the models.
MachineCount *int
}
// UpdateControllerDetailsFromLogin writes any new api addresses and other relevant details
// to the client controller file.
// Controller may be specified by a UUID or name, and must already exist.
func UpdateControllerDetailsFromLogin(
store jujuclient.ControllerStore, controllerName string,
params UpdateControllerParams,
) error {
controllerDetails, err := store.ControllerByName(controllerName)
if err != nil {
return errors.Trace(err)
}
return updateControllerDetailsFromLogin(store, controllerName, controllerDetails, params)
}
func updateControllerDetailsFromLogin(
store jujuclient.ControllerStore,
controllerName string, details *jujuclient.ControllerDetails,
params UpdateControllerParams,
) error {
hostPorts := usableHostPorts(params.CurrentHostPorts).Strings()
// Move the connected-to host (if present) to the front of the address list.
host, _, err := net.SplitHostPort(params.AddrConnectedTo)
if err == nil {
moveToFront(host, hostPorts)
}
// Move the IP address used to the front of the DNS cache entry
// (if present) so that it will be the first address dialed.
ipHost, _, err := net.SplitHostPort(params.IPAddrConnectedTo)
if err == nil {
moveToFront(ipHost, params.DNSCache[host])
}
newDetails := new(jujuclient.ControllerDetails)
*newDetails = *details
if params.Proxier != nil {
newDetails.Proxy = &jujuclient.ProxyConfWrapper{
Proxier: params.Proxier,
}
}
newDetails.AgentVersion = params.AgentVersion
newDetails.APIEndpoints = hostPorts
newDetails.DNSCache = params.DNSCache
if params.MachineCount != nil {
newDetails.MachineCount = params.MachineCount
}
if params.ControllerMachineCount != nil {
newDetails.ControllerMachineCount = *params.ControllerMachineCount
}
if params.PublicDNSName != nil {
newDetails.PublicDNSName = *params.PublicDNSName
}
if reflect.DeepEqual(newDetails, details) {
// Nothing has changed - no need to update the controller details.
return nil
}
if addrsChanged(newDetails.APIEndpoints, details.APIEndpoints) {
logger.Infof("API endpoints changed from %v to %v", details.APIEndpoints, newDetails.APIEndpoints)
}
err = store.UpdateController(controllerName, *newDetails)
return errors.Trace(err)
}
// dnsCacheMap implements api.DNSCache by
// caching entries in a map.
type dnsCacheMap map[string][]string
func (m dnsCacheMap) Lookup(host string) []string {
return m[host]
}
func (m dnsCacheMap) copy() dnsCacheMap {
m1 := make(dnsCacheMap)
for host, ips := range m {
m1[host] = append([]string{}, ips...)
}
return m1
}
func (m dnsCacheMap) Add(host string, ips []string) {
m[host] = append([]string{}, ips...)
}
// moveToFront moves the given item (if present)
// to the front of the given slice.
func moveToFront(item string, xs []string) {
for i, x := range xs {
if x != item {
continue
}
if i == 0 {
return
}
copy(xs[1:], xs[0:i])
xs[0] = item
return
}
}