Skip to content

Commit 8a80fe6

Browse files
authored
Merge branch 'main' into dev-jl-version2
2 parents be15291 + e04978c commit 8a80fe6

File tree

4 files changed

+189
-78
lines changed

4 files changed

+189
-78
lines changed

README.md

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
This Go module offers a sophisticated HTTP client designed for seamless API interactions, with a strong emphasis on concurrency management, robust error handling, extensive logging, and adaptive rate limiting. It's particularly suitable for applications requiring high-throughput API interactions with complex authentication and operational resilience.
44

5-
This client leverages API-specific SDKs to provide a comprehensive and consistent interface for interacting with various APIs, including Microsoft Graph, Jamf Pro, and others. It is designed to be easily extensible to support additional APIs and to be highly configurable to meet specific API requirements. It achieves this through using a modular design, with a core HTTP client and API-specific handlers that encapsulate the unique requirements of each API supported.
5+
This client is designed to be used with targetted SDK's and terraform providers only. As such the http client cannot be used without a supporting SDK and associated api integration plugin [go-api-http-client-integrations](https://github.com/deploymenttheory/go-api-http-client-integrations).
66

7-
This HTTP client is intended to be used with targetted SDK's and terraform providers only. As such the http client cannot be used without a supporting SDK.
7+
The plugin is required to provide the necessary API-specific handlers and configuration to the HTTP client. The plugin approach is designed to provide a consistent interface for interacting with various APIs, including Microsoft Graph, Jamf Pro, and others. It is easily extensible to support additional APIs and highly configurable to meet specific API requirements. It achieves this through using a modular design, with a core HTTP client and API-specific handlers that encapsulate the unique requirements of each API supported. Conseqently the client provides core common HTTP client functionality, such as rate limiting, logging, concurrency and responses while the plugin provides the API-specific logic, such as encoding and decoding requests, managing authentication endpoints, and handling API-specific logic.
88

9-
## Features
9+
## HTTP Client Features
1010

1111
- **Comprehensive Authentication Support**: Robust support for various authentication schemes, including OAuth and Bearer Token, with built-in token management and validation.
1212
- **Advanced Concurrency Management**: An intelligent Concurrency Manager dynamically adjusts concurrent request limits to optimize throughput and adhere to API rate limits.
@@ -20,8 +20,19 @@ This HTTP client is intended to be used with targetted SDK's and terraform provi
2020
- **API Handler Interface**: Provides a flexible and extensible way to interact with different APIs, including encoding and decoding requests and responses, managing authentication endpoints, and handling API-specific logic.
2121
- **Configuration via JSON or Environment Variables**: The Go API HTTP Client supports configuration via JSON files or environment variables, providing flexibility in defining authentication credentials, API endpoints, logging settings, and other parameters.
2222

23-
TBC
23+
## Getting Started
2424

25+
This SDK requires an existing Go environment to build and run. If you do not have Go installed, you can download and install it from the [official Go website](https://golang.org/doc/install).
26+
27+
28+
29+
## SDK Support
30+
31+
This http client is used in conjuction with the following SDKs:
32+
33+
- [go-api-sdk-m365](https://github.com/deploymenttheory/go-api-sdk-m365)
34+
35+
- [go-api-sdk-jamfpro](https://github.com/deploymenttheory/go-api-sdk-jamfpro)
2536

2637
## Reporting Issues and Feedback
2738

@@ -41,10 +52,6 @@ If you would like to become an active contributor to this repository or project,
4152

4253
## Learn More
4354

44-
* [GitHub Documentation][GitHubDocs]
45-
* [Azure DevOps Documentation][AzureDevOpsDocs]
46-
* [Microsoft Azure Documentation][MicrosoftAzureDocs]
47-
4855
<!-- References -->
4956

5057
<!-- Local -->
@@ -53,13 +60,4 @@ If you would like to become an active contributor to this repository or project,
5360
[GitHubDocs]: <https://docs.github.com/>
5461
[AzureDevOpsDocs]: <https://docs.microsoft.com/en-us/azure/devops/?view=azure-devops>
5562
[GitHubIssues]: <https://github.com/segraef/Template/issues>
56-
[Contributing]: CONTRIBUTING.md
57-
58-
<!-- External -->
59-
[Az]: <https://img.shields.io/powershellgallery/v/Az.svg?style=flat-square&label=Az>
60-
[AzGallery]: <https://www.powershellgallery.com/packages/Az/>
61-
[PowerShellCore]: <https://github.com/PowerShell/PowerShell/releases/latest>
62-
63-
<!-- Docs -->
64-
[MicrosoftAzureDocs]: <https://docs.microsoft.com/en-us/azure/>
65-
[PowerShellDocs]: <https://docs.microsoft.com/en-us/powershell/>
63+
[Contributing]: CONTRIBUTING.md

httpclient/config_validation.go

Lines changed: 92 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,97 @@
33
package httpclient
44

55
import (
6+
"encoding/json"
67
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"os"
12+
"strings"
713
"time"
814
)
915

1016
const (
11-
DefaultLogLevelString = "LogLevelInfo"
12-
DefaultLogOutputFormatString = "pretty"
13-
DefaultLogConsoleSeparator = " "
14-
DefaultLogExportPath = "/defaultlogs"
15-
DefaultMaxRetryAttempts = 3
16-
DefaultMaxConcurrentRequests = 1
17-
DefaultExportLogs = false
18-
DefaultHideSensitiveData = false
19-
DefaultEnableDynamicRateLimiting = false
20-
DefaultCustomTimeout = 5 * time.Second
21-
DefaultTokenRefreshBufferPeriod = 2 * time.Minute
22-
DefaultTotalRetryDuration = 5 * time.Minute
23-
DefaultFollowRedirects = false
24-
DefaultMaxRedirects = 5
17+
DefaultLogLevelString = "LogLevelInfo"
18+
DefaultLogOutputFormatString = "pretty"
19+
DefaultLogConsoleSeparator = " "
20+
DefaultLogExportPath = "/defaultlogs"
21+
DefaultMaxRetryAttempts = 3
22+
DefaultMaxConcurrentRequests = 1
23+
DefaultExportLogs = false
24+
DefaultHideSensitiveData = false
25+
DefaultEnableDynamicRateLimiting = false
26+
DefaultCustomTimeout = 5 * time.Second
27+
DefaultTokenRefreshBufferPeriod = 2 * time.Minute
28+
DefaultTotalRetryDuration = 5 * time.Minute
29+
DefaultFollowRedirects = false
30+
DefaultMaxRedirects = 5
31+
DefaultEnableConcurrencyManagement = false
2532
)
2633

27-
// TODO migrate all the loose strings
28-
29-
// TODO LoadConfigFromFile Func
34+
// LoadConfigFromFile loads http client configuration settings from a JSON file.
3035
func LoadConfigFromFile(filepath string) (*ClientConfig, error) {
31-
return nil, nil
36+
absPath, err := validateFilePath(filepath)
37+
if err != nil {
38+
return nil, fmt.Errorf("invalid file path: %v", err)
39+
}
40+
41+
file, err := os.Open(absPath)
42+
if err != nil {
43+
return nil, fmt.Errorf("could not open file: %v", err)
44+
}
45+
defer file.Close()
46+
47+
byteValue, err := io.ReadAll(file)
48+
if err != nil {
49+
return nil, fmt.Errorf("could not read file: %v", err)
50+
}
51+
52+
var config ClientConfig
53+
err = json.Unmarshal(byteValue, &config)
54+
if err != nil {
55+
return nil, fmt.Errorf("could not unmarshal JSON: %v", err)
56+
}
57+
58+
// Set default values for missing fields.
59+
SetDefaultValuesClientConfig(&config)
60+
61+
return &config, nil
3262
}
3363

34-
// TODO LoadConfigFromEnv Func
64+
// LoadConfigFromEnv loads HTTP client configuration settings from environment variables.
65+
// If any environment variables are not set, the default values defined in the constants are used instead.
3566
func LoadConfigFromEnv() (*ClientConfig, error) {
36-
return nil, nil
67+
config := &ClientConfig{
68+
HideSensitiveData: getEnvAsBool("HIDE_SENSITIVE_DATA", DefaultHideSensitiveData),
69+
MaxRetryAttempts: getEnvAsInt("MAX_RETRY_ATTEMPTS", DefaultMaxRetryAttempts),
70+
MaxConcurrentRequests: getEnvAsInt("MAX_CONCURRENT_REQUESTS", DefaultMaxConcurrentRequests),
71+
EnableDynamicRateLimiting: getEnvAsBool("ENABLE_DYNAMIC_RATE_LIMITING", DefaultEnableDynamicRateLimiting),
72+
CustomTimeout: getEnvAsDuration("CUSTOM_TIMEOUT", DefaultCustomTimeout),
73+
TokenRefreshBufferPeriod: getEnvAsDuration("TOKEN_REFRESH_BUFFER_PERIOD", DefaultTokenRefreshBufferPeriod),
74+
TotalRetryDuration: getEnvAsDuration("TOTAL_RETRY_DURATION", DefaultTotalRetryDuration),
75+
FollowRedirects: getEnvAsBool("FOLLOW_REDIRECTS", DefaultFollowRedirects),
76+
MaxRedirects: getEnvAsInt("MAX_REDIRECTS", DefaultMaxRedirects),
77+
EnableConcurrencyManagement: getEnvAsBool("ENABLE_CONCURRENCY_MANAGEMENT", DefaultEnableConcurrencyManagement),
78+
}
79+
80+
// Load custom cookies from environment variables.
81+
customCookies := getEnvAsString("CUSTOM_COOKIES", "")
82+
if customCookies != "" {
83+
cookies := []*http.Cookie{}
84+
for _, cookie := range strings.Split(customCookies, ";") {
85+
parts := strings.SplitN(cookie, "=", 2)
86+
if len(parts) == 2 {
87+
cookies = append(cookies, &http.Cookie{
88+
Name: parts[0],
89+
Value: parts[1],
90+
})
91+
}
92+
}
93+
config.CustomCookies = cookies
94+
}
95+
96+
return config, nil
3797
}
3898

3999
// TODO Review validateClientConfig
@@ -45,7 +105,7 @@ func validateClientConfig(config ClientConfig, populateDefaults bool) error {
45105

46106
// TODO adjust these strings to have links to documentation & centralise them
47107
if config.Integration == nil {
48-
return errors.New("no api integration supplied, please see documentation")
108+
return errors.New("no http client api integration supplied, please see repo documentation for this client and go-api-http-client-integration and provide an implementation")
49109
}
50110

51111
if config.MaxRetryAttempts < 0 {
@@ -77,42 +137,16 @@ func validateClientConfig(config ClientConfig, populateDefaults bool) error {
77137
return nil
78138
}
79139

140+
// SetDefaultValuesClientConfig sets default values for the client configuration. Ensuring that all fields have a valid or minimum value.
80141
func SetDefaultValuesClientConfig(config *ClientConfig) {
81-
82-
if !config.HideSensitiveData {
83-
config.HideSensitiveData = DefaultHideSensitiveData
84-
}
85-
86-
if config.MaxRetryAttempts == 0 {
87-
config.MaxRetryAttempts = DefaultMaxRetryAttempts
88-
}
89-
90-
if config.MaxConcurrentRequests == 0 {
91-
config.MaxRetryAttempts = DefaultMaxConcurrentRequests
92-
}
93-
94-
if !config.EnableDynamicRateLimiting {
95-
config.EnableDynamicRateLimiting = DefaultEnableDynamicRateLimiting
96-
}
97-
98-
if config.CustomTimeout == 0 {
99-
config.CustomTimeout = DefaultCustomTimeout
100-
}
101-
102-
if config.TokenRefreshBufferPeriod == 0 {
103-
config.TokenRefreshBufferPeriod = DefaultTokenRefreshBufferPeriod
104-
}
105-
106-
if config.TotalRetryDuration == 0 {
107-
config.TotalRetryDuration = DefaultTotalRetryDuration
108-
}
109-
110-
if !config.FollowRedirects {
111-
config.FollowRedirects = DefaultFollowRedirects
112-
}
113-
114-
if config.MaxRedirects == 0 {
115-
config.MaxRedirects = DefaultMaxRedirects
116-
}
117-
142+
setDefaultBool(&config.HideSensitiveData, DefaultHideSensitiveData)
143+
setDefaultInt(&config.MaxRetryAttempts, DefaultMaxRetryAttempts, 1)
144+
setDefaultInt(&config.MaxConcurrentRequests, DefaultMaxConcurrentRequests, 1)
145+
setDefaultBool(&config.EnableDynamicRateLimiting, DefaultEnableDynamicRateLimiting)
146+
setDefaultDuration(&config.CustomTimeout, DefaultCustomTimeout)
147+
setDefaultDuration(&config.TokenRefreshBufferPeriod, DefaultTokenRefreshBufferPeriod)
148+
setDefaultDuration(&config.TotalRetryDuration, DefaultTotalRetryDuration)
149+
setDefaultBool(&config.FollowRedirects, DefaultFollowRedirects)
150+
setDefaultInt(&config.MaxRedirects, DefaultMaxRedirects, 0)
151+
setDefaultBool(&config.EnableConcurrencyManagement, DefaultEnableConcurrencyManagement)
118152
}

httpclient/integration.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// apiintegrations/apihandler/apihandler.go
1+
// httpclient/integration.go
22
package httpclient
33

44
import (
55
"net/http"
66
)
77

8-
// TODO comment
8+
// APIIntegration is an interface that defines the methods required for an API integration. These are obtained from go-api-http-client-integrations.
9+
// The methods defined in this interface are used by the HTTP client to authenticate and prepare requests for the API.
910
type APIIntegration interface {
1011
GetFQDN() string
1112
ConstructURL(endpoint string) string

httpclient/utility.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
// httpclient/utility.go
12
package httpclient
23

34
import (
45
"errors"
56
"fmt"
7+
"os"
68
"path/filepath"
79
"regexp"
10+
"strconv"
811
"strings"
12+
"time"
913
)
1014

1115
// TODO all func comments in here
1216

1317
const ConfigFileExtension = ".json"
1418

19+
// validateFilePath checks if a file path is valid.
1520
func validateFilePath(path string) (string, error) {
1621
cleanPath := filepath.Clean(path)
1722

@@ -32,6 +37,7 @@ func validateFilePath(path string) (string, error) {
3237

3338
}
3439

40+
// validateClientID checks if a client ID is a valid UUID.
3541
func validateValidClientID(clientID string) error {
3642
uuidRegex := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
3743
if regexp.MustCompile(uuidRegex).MatchString(clientID) {
@@ -60,6 +66,7 @@ func validateClientSecret(clientSecret string) error {
6066
return nil
6167
}
6268

69+
// validateUsername checks if a username meets the minimum requirements.
6370
func validateUsername(username string) error {
6471
usernameRegex := `^[a-zA-Z0-9!@#$%^&*()_\-\+=\[\]{\}\\|;:'",<.>/?]+$`
6572
if !regexp.MustCompile(usernameRegex).MatchString(username) {
@@ -68,9 +75,80 @@ func validateUsername(username string) error {
6875
return nil
6976
}
7077

78+
// validatePassword checks if a password meets the minimum requirements.
7179
func validatePassword(password string) error {
7280
if len(password) < 8 {
7381
return errors.New("password not long enough")
7482
}
7583
return nil
7684
}
85+
86+
// environment variable mapping helpers
87+
88+
// getEnvAsString reads an environment variable as a string, with a fallback default value.
89+
func getEnvAsString(name string, defaultVal string) string {
90+
if value, exists := os.LookupEnv(name); exists {
91+
return value
92+
}
93+
return defaultVal
94+
}
95+
96+
// getEnvAsBool reads an environment variable as a boolean, with a fallback default value.
97+
func getEnvAsBool(name string, defaultVal bool) bool {
98+
if value, exists := os.LookupEnv(name); exists {
99+
boolValue, err := strconv.ParseBool(value)
100+
if err == nil {
101+
return boolValue
102+
}
103+
}
104+
return defaultVal
105+
}
106+
107+
// getEnvAsInt reads an environment variable as an integer, with a fallback default value.
108+
func getEnvAsInt(name string, defaultVal int) int {
109+
if value, exists := os.LookupEnv(name); exists {
110+
intValue, err := strconv.Atoi(value)
111+
if err == nil {
112+
return intValue
113+
}
114+
}
115+
return defaultVal
116+
}
117+
118+
// getEnvAsDuration reads an environment variable as a duration, with a fallback default value.
119+
func getEnvAsDuration(name string, defaultVal time.Duration) time.Duration {
120+
if value, exists := os.LookupEnv(name); exists {
121+
durationValue, err := time.ParseDuration(value)
122+
if err == nil {
123+
return durationValue
124+
}
125+
}
126+
return defaultVal
127+
}
128+
129+
// http field validation functions
130+
131+
// setDefaultBool sets a boolean field to a default value if it is not already set during http client config field validation.
132+
func setDefaultBool(field *bool, defaultValue bool) {
133+
if !*field {
134+
*field = defaultValue
135+
}
136+
}
137+
138+
// setDefaultInt sets an integer field to a default value if it is not already set during http client config field validation.
139+
func setDefaultInt(field *int, defaultValue, minValue int) {
140+
if *field == 0 {
141+
*field = defaultValue
142+
} else if *field < minValue {
143+
*field = minValue
144+
}
145+
}
146+
147+
// setDefaultDuration sets a duration field to a default value if it is not already set during http client config field validation.
148+
func setDefaultDuration(field *time.Duration, defaultValue time.Duration) {
149+
if *field == 0 {
150+
*field = defaultValue
151+
} else if *field < 0 {
152+
*field = defaultValue
153+
}
154+
}

0 commit comments

Comments
 (0)