-
Notifications
You must be signed in to change notification settings - Fork 1
/
account.go
230 lines (208 loc) · 6.97 KB
/
account.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
/******************************************************************************
*
* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package schwift
import (
"context"
"encoding/json"
"fmt"
"net/http"
"regexp"
"sync"
)
// Account represents a Swift account. Instances are usually obtained by
// connecting to a backend (see package-level documentation), or by traversing
// upwards from a container with Container.Account().
type Account struct {
backend Backend
// URL parts
baseURL string
name string
// cache
headers *AccountHeaders
caps *Capabilities
capsMutex sync.Mutex
}
// IsEqualTo returns true if both Account instances refer to the same account.
func (a *Account) IsEqualTo(other *Account) bool {
return other.baseURL == a.baseURL && other.name == a.name
}
var endpointURLRegexp = regexp.MustCompile(`^(.*/)v1/(.*)/$`)
// InitializeAccount takes something that implements the Backend interface, and
// returns the Account instance corresponding to the account/project that this
// backend is connected to.
func InitializeAccount(backend Backend) (*Account, error) {
match := endpointURLRegexp.FindStringSubmatch(backend.EndpointURL())
if match == nil {
return nil, fmt.Errorf(`schwift.InitializeAccount(): invalid Swift endpoint URL: cannot find "/v1/" in %q`, backend.EndpointURL())
}
return &Account{
backend: backend,
baseURL: match[1],
name: match[2],
}, nil
}
// SwitchAccount returns a handle to a different account on the same server. Note
// that you need reseller permissions to access accounts other than that where
// you originally authenticated. This method does not check whether the account
// actually exists.
//
// The account name is usually the Keystone project ID with an additional "AUTH_"
// prefix.
func (a *Account) SwitchAccount(accountName string) *Account {
newEndpointURL := a.baseURL + "v1/" + accountName + "/"
return &Account{
backend: a.backend.Clone(newEndpointURL),
baseURL: a.baseURL,
name: accountName,
}
}
// Name returns the name of the account (usually the prefix "AUTH_" followed by
// the Keystone project ID).
func (a *Account) Name() string {
return a.name
}
// Backend returns the backend which is used to make requests against this
// account.
func (a *Account) Backend() Backend {
return a.backend
}
// Headers returns the AccountHeaders for this account. If the AccountHeaders
// has not been cached yet, a HEAD request is issued on the account.
//
// This operation fails with http.StatusNotFound if the account does not exist.
//
// WARNING: This method is not thread-safe. Calling it concurrently on the same
// object results in undefined behavior.
func (a *Account) Headers(ctx context.Context) (AccountHeaders, error) {
if a.headers != nil {
return *a.headers, nil
}
resp, err := Request{
Method: "HEAD",
ExpectStatusCodes: []int{204},
}.Do(ctx, a.backend)
if err != nil {
return AccountHeaders{}, err
}
defer resp.Body.Close()
headers := AccountHeaders{headersFromHTTP(resp.Header)}
err = headers.Validate()
if err != nil {
return headers, err
}
a.headers = &headers
return *a.headers, nil
}
// Invalidate clears the internal cache of this Account instance. The next call
// to Headers() on this instance will issue a HEAD request on the account.
//
// WARNING: This method is not thread-safe. Calling it concurrently on the same
// object results in undefined behavior.
func (a *Account) Invalidate() {
a.headers = nil
}
// Update updates the account using a POST request. The headers in the headers
// attribute take precedence over those in opts.Headers.
//
// A successful POST request implies Invalidate() since it may change metadata.
func (a *Account) Update(ctx context.Context, headers AccountHeaders, opts *RequestOptions) error {
resp, err := Request{
Method: "POST",
Options: cloneRequestOptions(opts, headers.Headers),
ExpectStatusCodes: []int{204},
}.Do(ctx, a.backend)
if err == nil {
a.Invalidate()
resp.Body.Close()
}
return err
}
// Create creates the account using a PUT request. This operation is only
// available to reseller admins, not to regular users.
//
// A successful PUT request implies Invalidate() since it may change metadata.
func (a *Account) Create(ctx context.Context, opts *RequestOptions) error {
resp, err := Request{
Method: "PUT",
Options: opts,
ExpectStatusCodes: []int{201, 202},
DrainResponseBody: true,
}.Do(ctx, a.backend)
if err == nil {
a.Invalidate()
resp.Body.Close()
}
return err
}
// Containers returns a ContainerIterator that lists the containers in this
// account. The most common use case is:
//
// containers, err := account.Containers().Collect()
//
// You can extend this by configuring the iterator before collecting the results:
//
// iter := account.Containers()
// iter.Prefix = "test-"
// containers, err := iter.Collect()
//
// Or you can use a different iteration method:
//
// err := account.Containers().ForeachDetailed(func (ci ContainerInfo) error {
// log.Printf("container %s contains %d objects!\n",
// ci.Container.Name(), ci.ObjectCount)
// })
func (a *Account) Containers() *ContainerIterator {
return &ContainerIterator{Account: a}
}
// Capabilities queries the GET /info endpoint of the Swift server providing
// this account. Capabilities are cached, so the GET request will only be sent
// once during the first call to this method.
func (a *Account) Capabilities(ctx context.Context) (Capabilities, error) {
a.capsMutex.Lock()
defer a.capsMutex.Unlock()
if a.caps != nil {
return *a.caps, nil
}
buf, err := a.RawCapabilities(ctx)
if err != nil {
return Capabilities{}, err
}
var caps Capabilities
err = json.Unmarshal(buf, &caps)
if err != nil {
return caps, err
}
a.caps = &caps
return caps, nil
}
// RawCapabilities queries the GET /info endpoint of the Swift server providing
// this account, and returns the response body. Unlike Account.Capabilities,
// this method does not employ any caching.
func (a *Account) RawCapabilities(ctx context.Context) ([]byte, error) {
// This method is the only one in Schwift that bypasses struct Request since
// the request URL is not below the endpoint URL.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"info", http.NoBody)
if err != nil {
return nil, err
}
resp, err := a.backend.Do(req)
if err != nil {
return nil, err
}
return collectResponseBody(resp)
}