selvpc.go raw
1 package selvpcclient
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "runtime/debug"
10 "strings"
11 "time"
12
13 "github.com/gophercloud/gophercloud"
14
15 "github.com/selectel/go-selvpcclient/v4/selvpcclient/clients"
16 clientservices "github.com/selectel/go-selvpcclient/v4/selvpcclient/clients/services"
17 )
18
19 var errRequiredClientOptions = errors.New("some of the required options are not set")
20
21 const (
22 AppName = "go-selvpcclient"
23 )
24
25 type Client struct {
26 // Resell - client for Cloud Management API.
27 Resell *clients.ResellClient
28
29 // QuotaManager - client for Cloud Quota Management API.
30 QuotaManager *clients.QuotaManagerClient
31
32 // Catalog - service for simplified resolve regional endpoints from Keystone catalog.
33 Catalog *clientservices.CatalogService
34
35 serviceClient *gophercloud.ServiceClient
36 }
37
38 type ClientOptions struct {
39 Context context.Context
40
41 // Your Account ID, for example: 234567.
42 DomainName string
43
44 // Specify Identity endpoint.
45 AuthURL string
46
47 // Setting a location for auth endpoint like ResellAPI or Keystone.
48 AuthRegion string
49
50 // Credentials of your service user.
51 // Documentation: https://docs.selectel.ru/control-panel-actions/users-and-roles/
52 Username string
53 Password string
54
55 // Optional field, that is used for authentication with project scope.
56 // If you created service user with admin role of project, then this is field for you.
57 ProjectID string
58
59 // Optional field to specify the domain name where the user is located.
60 // Used in private clouds to issue a token not from owned domain.
61 // If this field is not set, then it will be equal to the value of DomainName.
62 UserDomainName string
63 }
64
65 func NewClient(options *ClientOptions) (*Client, error) {
66 requiredAbsent := make([]string, 0)
67 if options.DomainName == "" {
68 requiredAbsent = append(requiredAbsent, "DomainName")
69 }
70
71 if options.Username == "" {
72 requiredAbsent = append(requiredAbsent, "Username")
73 }
74
75 if options.Password == "" {
76 requiredAbsent = append(requiredAbsent, "Password")
77 }
78
79 if options.AuthURL == "" {
80 requiredAbsent = append(requiredAbsent, "AuthURL")
81 }
82
83 if options.AuthRegion == "" {
84 requiredAbsent = append(requiredAbsent, "AuthRegion")
85 }
86
87 if len(requiredAbsent) > 0 {
88 return nil, fmt.Errorf("validation error: %w: %s", errRequiredClientOptions, strings.Join(requiredAbsent, ", "))
89 }
90
91 serviceClientOptions := clientservices.ServiceClientOptions{
92 DomainName: options.DomainName,
93 Username: options.Username,
94 Password: options.Password,
95 AuthURL: options.AuthURL,
96 AuthRegion: options.AuthRegion,
97 ProjectID: options.ProjectID,
98 UserDomainName: options.UserDomainName,
99 UserAgent: fmt.Sprintf("%s/%s", AppName, findModuleVersion()),
100 }
101
102 serviceClient, err := clientservices.NewServiceClient(&serviceClientOptions)
103 if err != nil {
104 return nil, fmt.Errorf("failed to create service client, err: %w", err)
105 }
106
107 serviceClient.ProviderClient.Context = options.Context
108
109 catalogService, err := clientservices.NewCatalogService(serviceClient)
110 if err != nil {
111 return nil, fmt.Errorf("failed to initialize endpoints catalog service, err: %w", err)
112 }
113
114 requestService := clientservices.NewRequestService(serviceClient)
115
116 client := Client{
117 Resell: clients.NewResellClient(requestService, catalogService, options.AuthRegion),
118 QuotaManager: clients.NewQuotaManagerClient(requestService, catalogService),
119 Catalog: catalogService,
120 serviceClient: serviceClient,
121 }
122
123 return &client, nil
124 }
125
126 func findModuleVersion() string {
127 moduleName := "github.com/selectel/" + AppName
128
129 info, ok := debug.ReadBuildInfo()
130 if ok {
131 for _, dep := range info.Deps {
132 // Use prefix, because module has name with major version - github.com/selectel/go-selvpcclient/v4
133 if strings.HasPrefix(dep.Path, moduleName) {
134 return dep.Version
135 }
136 }
137 }
138 return "unknown_version"
139 }
140
141 // GetXAuthToken - returns X-Auth-Token from Service Provider. This method doesn't guarantee that the token is valid.
142 // It returns the last used token from the service provider. Usually the lifetime of the token is 24h. If you use
143 // this token, then you should handle 401 error.
144 func (selvpc *Client) GetXAuthToken() string {
145 return selvpc.serviceClient.Token()
146 }
147
148 // ---------------------------------------------------------------------------------------------------------------------
149
150 // RFC3339NoZ describes a timestamp format used by some SelVPC responses.
151 const RFC3339NoZ = "2006-01-02T15:04:05"
152
153 // JSONRFC3339NoZTimezone is a type for timestamps SelVPC responses with the RFC3339NoZ format.
154 type JSONRFC3339NoZTimezone time.Time
155
156 // UnmarshalJSON helps to unmarshal timestamps from SelVPC responses to the
157 // JSONRFC3339NoZTimezone type.
158 func (jt *JSONRFC3339NoZTimezone) UnmarshalJSON(data []byte) error {
159 b := bytes.NewBuffer(data)
160 dec := json.NewDecoder(b)
161 var s string
162 if err := dec.Decode(&s); err != nil {
163 return err
164 }
165 if s == "" {
166 return nil
167 }
168 t, err := time.Parse(RFC3339NoZ, s)
169 if err != nil {
170 return err
171 }
172 *jt = JSONRFC3339NoZTimezone(t)
173 return nil
174 }
175
176 const (
177 // IPv4 represents IP version 4.
178 IPv4 IPVersion = "ipv4"
179
180 // IPv6 represents IP version 6.
181 IPv6 IPVersion = "ipv6"
182 )
183
184 // IPVersion represents a type for the IP versions of the different Selectel VPC APIs.
185 type IPVersion string
186