Skip to content

Commit 92cef35

Browse files
feat: configfile support for v2 (#37)
* feat: add option to config data from file (#27) * fix: create client in config along with transport (#30) * feat: add more useful util funcs * test: add tests and debug prints, discard override if unexpected location --------- Co-authored-by: cguran-ionos <cristian-mihai.guran@ionos.com>
1 parent 9ccadeb commit 92cef35

File tree

6 files changed

+841
-11
lines changed

6 files changed

+841
-11
lines changed

shared/configuration.go

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package shared
66

77
import (
88
"context"
9+
"crypto/tls"
10+
"crypto/x509"
911
"fmt"
12+
"net"
1013
"net/http"
1114
"net/url"
1215
"os"
@@ -17,13 +20,15 @@ import (
1720
var DefaultIonosBasePath = ""
1821

1922
const (
20-
IonosUsernameEnvVar = "IONOS_USERNAME"
21-
IonosPasswordEnvVar = "IONOS_PASSWORD"
22-
IonosTokenEnvVar = "IONOS_TOKEN"
23-
IonosApiUrlEnvVar = "IONOS_API_URL"
24-
IonosPinnedCertEnvVar = "IONOS_PINNED_CERT"
25-
IonosLogLevelEnvVar = "IONOS_LOG_LEVEL"
26-
DefaultIonosServerUrl = "https://api.ionos.com/"
23+
IonosUsernameEnvVar = "IONOS_USERNAME"
24+
IonosPasswordEnvVar = "IONOS_PASSWORD"
25+
IonosTokenEnvVar = "IONOS_TOKEN"
26+
IonosApiUrlEnvVar = "IONOS_API_URL"
27+
IonosPinnedCertEnvVar = "IONOS_PINNED_CERT"
28+
IonosLogLevelEnvVar = "IONOS_LOG_LEVEL"
29+
IonosFilePathEnvVar = "IONOS_CONFIG_FILE"
30+
IonosCurrentProfileEnvVar = "IONOS_CURRENT_PROFILE"
31+
DefaultIonosServerUrl = "https://api.ionos.com/"
2732

2833
defaultMaxRetries = 3
2934
defaultWaitTime = time.Duration(100) * time.Millisecond
@@ -99,7 +104,7 @@ type ServerConfiguration struct {
99104
// ServerConfigurations stores multiple ServerConfiguration items
100105
type ServerConfigurations []ServerConfiguration
101106

102-
// shared.Configuration stores the configuration of the API client
107+
// Configuration stores the configuration of the API client
103108
type Configuration struct {
104109
Host string `json:"host,omitempty"`
105110
Scheme string `json:"scheme,omitempty"`
@@ -145,6 +150,75 @@ func NewConfiguration(username, password, token, hostUrl string) *Configuration
145150
return cfg
146151
}
147152

153+
// ClientOptions is a struct that represents the client options
154+
type ClientOptions struct {
155+
// Endpoint is the endpoint that will be overridden
156+
Endpoint string
157+
// SkipTLSVerify skips tls verification. Not recommended for production!
158+
SkipTLSVerify bool
159+
// Certificate is the certificate that will be used for tls verification
160+
Certificate string
161+
// Credentials are the credentials that will be used for authentication
162+
Credentials Credentials
163+
}
164+
165+
// Credentials are the credentials that will be used for authentication
166+
type Credentials struct {
167+
Username string `yaml:"username"`
168+
Password string `yaml:"password"`
169+
Token string `yaml:"token"`
170+
}
171+
172+
// NewConfigurationFromOptions returns a new shared.Configuration object created from the client options
173+
func NewConfigurationFromOptions(clientOptions ClientOptions) *Configuration {
174+
cfg := &Configuration{
175+
DefaultHeader: make(map[string]string),
176+
DefaultQueryParams: url.Values{},
177+
UserAgent: "shared-sdk-go",
178+
Username: clientOptions.Credentials.Username,
179+
Password: clientOptions.Credentials.Password,
180+
Token: clientOptions.Credentials.Token,
181+
MaxRetries: defaultMaxRetries,
182+
MaxWaitTime: defaultMaxWaitTime,
183+
WaitTime: defaultWaitTime,
184+
Servers: ServerConfigurations{},
185+
OperationServers: map[string]ServerConfigurations{},
186+
HTTPClient: http.DefaultClient,
187+
}
188+
if clientOptions.Endpoint != "" {
189+
cfg.Servers = ServerConfigurations{
190+
{
191+
URL: getServerUrl(clientOptions.Endpoint),
192+
Description: "Production",
193+
},
194+
}
195+
}
196+
cfg.HTTPClient.Transport = CreateTransport(clientOptions.SkipTLSVerify, clientOptions.Certificate)
197+
return cfg
198+
}
199+
200+
func CreateTransport(insecure bool, certificate string) *http.Transport {
201+
dialer := &net.Dialer{
202+
Timeout: 30 * time.Second,
203+
KeepAlive: 30 * time.Second,
204+
}
205+
transport := &http.Transport{
206+
Proxy: http.ProxyFromEnvironment,
207+
DialContext: dialer.DialContext,
208+
DisableKeepAlives: true,
209+
IdleConnTimeout: 30 * time.Second,
210+
TLSHandshakeTimeout: 15 * time.Second,
211+
ExpectContinueTimeout: 1 * time.Second,
212+
MaxIdleConnsPerHost: 3,
213+
MaxConnsPerHost: 3,
214+
}
215+
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: insecure}
216+
if certificate != "" {
217+
transport.TLSClientConfig.RootCAs = AddCertsToClient(certificate)
218+
}
219+
return transport
220+
}
221+
148222
func NewConfigurationFromEnv() *Configuration {
149223
return NewConfiguration(os.Getenv(IonosUsernameEnvVar), os.Getenv(IonosPasswordEnvVar), os.Getenv(IonosTokenEnvVar), os.Getenv(IonosApiUrlEnvVar))
150224
}
@@ -161,7 +235,7 @@ func (c *Configuration) AddDefaultQueryParam(key string, value string) {
161235
// URL formats template on a index using given variables
162236
func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) {
163237
if index < 0 || len(sc) <= index {
164-
return "", fmt.Errorf("Index %v out of range %v", index, len(sc)-1)
238+
return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1)
165239
}
166240
server := sc[index]
167241
url := server.URL
@@ -182,7 +256,7 @@ func (sc ServerConfigurations) URL(index int, variables map[string]string) (stri
182256
}
183257
}
184258
if !found {
185-
return "", fmt.Errorf("The variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues)
259+
return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues)
186260
}
187261
url = strings.Replace(url, "{"+name+"}", value, -1)
188262
} else {
@@ -203,7 +277,7 @@ func getServerIndex(ctx context.Context) (int, error) {
203277
if index, ok := si.(int); ok {
204278
return index, nil
205279
}
206-
return 0, reportError("Invalid type %T should be int", si)
280+
return 0, reportError("invalid type %T should be int", si)
207281
}
208282
return 0, nil
209283
}
@@ -291,3 +365,66 @@ func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint strin
291365

292366
return sc.URL(index, variables)
293367
}
368+
369+
// ConfigProvider is an interface that allows to get the configuration of shared clients
370+
type ConfigProvider interface {
371+
GetConfig() *Configuration
372+
}
373+
374+
// EndpointOverridden is a constant that is used to mark the endpoint as overridden and can be used to search for the location
375+
// in the server configuration.
376+
const EndpointOverridden = "endpoint from config file"
377+
378+
// OverrideLocationFor aims to override the server URL for a given client configuration, based on location and endpoint inputs.
379+
// Mutates the client configuration. It searches for the location in the server configuration and overrides the endpoint.
380+
// If the endpoint is empty, it early exits without making changes.
381+
func OverrideLocationFor(configProvider ConfigProvider, location, endpoint string, replaceServers bool) {
382+
if endpoint == "" {
383+
return
384+
}
385+
// If the replaceServers flag is set, we replace the servers with the new endpoint
386+
if replaceServers {
387+
SdkLogger.Printf("[DEBUG] Replacing all server configurations for location %s", location)
388+
configProvider.GetConfig().Servers = []ServerConfiguration{
389+
{
390+
URL: endpoint,
391+
Description: EndpointOverridden + location,
392+
},
393+
}
394+
return
395+
}
396+
location = strings.TrimSpace(location)
397+
endpoint = strings.TrimSpace(endpoint)
398+
servers := configProvider.GetConfig().Servers
399+
for idx := range servers {
400+
if strings.Contains(servers[idx].URL, location) {
401+
SdkLogger.Printf("[DEBUG] Overriding server configuration for location %s", location)
402+
servers[idx].URL = endpoint
403+
servers[idx].Description = EndpointOverridden + location
404+
return
405+
}
406+
}
407+
SdkLogger.Printf("[DEBUG] Adding new server configuration for location %s", location)
408+
configProvider.GetConfig().Servers = append(configProvider.GetConfig().Servers, ServerConfiguration{
409+
URL: endpoint,
410+
Description: EndpointOverridden + location,
411+
})
412+
}
413+
414+
func SetSkipTLSVerify(configProvider ConfigProvider, skipTLSVerify bool) {
415+
configProvider.GetConfig().HTTPClient.Transport = &http.Transport{
416+
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSVerify},
417+
}
418+
}
419+
420+
// AddCertsToClient adds certificates to the http client
421+
func AddCertsToClient(authorityData string) *x509.CertPool {
422+
rootCAs, _ := x509.SystemCertPool()
423+
if rootCAs == nil {
424+
rootCAs = x509.NewCertPool()
425+
}
426+
if ok := rootCAs.AppendCertsFromPEM([]byte(authorityData)); !ok && SdkLogLevel.Satisfies(Debug) {
427+
SdkLogger.Printf("No certs appended, using system certs only")
428+
}
429+
return rootCAs
430+
}

shared/configuration_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package shared
2+
3+
import (
4+
"crypto/tls"
5+
"github.com/stretchr/testify/assert"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
const testEndpoint = "https://test.endpoint"
11+
12+
func TestNewConfigurationFromOptions(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
clientOptions ClientOptions
16+
expectedConfig *Configuration
17+
}{
18+
{
19+
name: "ValidOptions",
20+
clientOptions: ClientOptions{
21+
Endpoint: testEndpoint,
22+
SkipTLSVerify: true,
23+
Certificate: "",
24+
Credentials: Credentials{
25+
Username: "testUser",
26+
Password: "testPass",
27+
Token: "testToken",
28+
},
29+
},
30+
expectedConfig: &Configuration{
31+
Username: "testUser",
32+
Password: "testPass",
33+
Token: "testToken",
34+
Servers: ServerConfigurations{
35+
{
36+
URL: testEndpoint,
37+
Description: "Production",
38+
},
39+
},
40+
HTTPClient: &http.Client{
41+
Transport: &http.Transport{
42+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
43+
},
44+
},
45+
},
46+
},
47+
{
48+
name: "EmptyEndpoint",
49+
clientOptions: ClientOptions{
50+
SkipTLSVerify: true,
51+
Certificate: "",
52+
Credentials: Credentials{
53+
Username: "testUser",
54+
Password: "testPass",
55+
Token: "testToken",
56+
},
57+
},
58+
expectedConfig: &Configuration{
59+
Username: "testUser",
60+
Password: "testPass",
61+
Token: "testToken",
62+
Servers: ServerConfigurations{},
63+
HTTPClient: &http.Client{
64+
Transport: &http.Transport{
65+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
66+
},
67+
},
68+
},
69+
},
70+
{
71+
name: "NoCredentials",
72+
clientOptions: ClientOptions{
73+
Endpoint: testEndpoint,
74+
SkipTLSVerify: true,
75+
Certificate: "",
76+
},
77+
expectedConfig: &Configuration{
78+
Username: "",
79+
Password: "",
80+
Token: "",
81+
Servers: ServerConfigurations{
82+
{
83+
URL: testEndpoint,
84+
Description: "Production",
85+
},
86+
},
87+
HTTPClient: &http.Client{
88+
Transport: &http.Transport{
89+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
90+
},
91+
},
92+
},
93+
},
94+
{
95+
name: "AddCertificate",
96+
clientOptions: ClientOptions{
97+
Endpoint: testEndpoint,
98+
SkipTLSVerify: true,
99+
Certificate: "testCertData",
100+
Credentials: Credentials{
101+
Username: "testUser",
102+
Password: "testPass",
103+
Token: "testToken",
104+
},
105+
},
106+
expectedConfig: &Configuration{
107+
Username: "testUser",
108+
Password: "testPass",
109+
Token: "testToken",
110+
Servers: ServerConfigurations{
111+
{
112+
URL: testEndpoint,
113+
Description: "Production",
114+
},
115+
},
116+
HTTPClient: &http.Client{
117+
Transport: &http.Transport{
118+
TLSClientConfig: &tls.Config{
119+
InsecureSkipVerify: true,
120+
RootCAs: AddCertsToClient("testCertData"),
121+
},
122+
},
123+
},
124+
},
125+
},
126+
}
127+
128+
for _, tt := range tests {
129+
t.Run(tt.name, func(t *testing.T) {
130+
config := NewConfigurationFromOptions(tt.clientOptions)
131+
assert.Equal(t, tt.expectedConfig.Username, config.Username)
132+
assert.Equal(t, tt.expectedConfig.Password, config.Password)
133+
assert.Equal(t, tt.expectedConfig.Token, config.Token)
134+
assert.Equal(t, tt.expectedConfig.Servers, config.Servers)
135+
assert.NotNil(t, config.HTTPClient)
136+
assert.Equal(t, tt.expectedConfig.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify,
137+
config.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify)
138+
assert.True(t, config.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Equal(tt.expectedConfig.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs))
139+
})
140+
}
141+
}

0 commit comments

Comments
 (0)