Skip to content

Commit

Permalink
feat(provider): allow iamtoken and crn in lieu of servicekey in provider
Browse files Browse the repository at this point in the history
For IBM envs, we will allow provider configs to contain iamtoken and
cloud_resource_name to authorize with the API in IBM envs in lieu of
servicekey

Semver: minor
Ref: LOG-18869
  • Loading branch information
ligerzero459 committed Dec 18, 2023
1 parent 3ce7609 commit f9fc0b0
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 24 deletions.
30 changes: 23 additions & 7 deletions logdna/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,30 @@ import (
)

type providerConfig struct {
serviceKey string
baseURL string
httpClient *http.Client
serviceKey string
iamtoken string
cloud_resource_name string
baseURL string
httpClient *http.Client
}

// Provider initializes the schema with a service key and hooks for our resources
func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"servicekey": {
Type: schema.TypeString,
Optional: true,
ExactlyOneOf: []string{"iamtoken"},
},
"iamtoken": {
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"cloud_resource_name"},
},
"cloud_resource_name": {
Type: schema.TypeString,
Required: true,
Optional: true,
},
"url": {
Type: schema.TypeString,
Expand Down Expand Up @@ -48,11 +60,15 @@ func Provider() *schema.Provider {

func providerConfigure(d *schema.ResourceData) (interface{}, error) {
serviceKey := d.Get("servicekey").(string)
iamtoken := d.Get("iamtoken").(string)
cloud_resource_name := d.Get("cloud_resource_name").(string)
url := d.Get("url").(string)

return &providerConfig{
serviceKey: serviceKey,
baseURL: url,
httpClient: &http.Client{Timeout: 15 * time.Second},
serviceKey: serviceKey,
iamtoken: iamtoken,
cloud_resource_name: cloud_resource_name,
baseURL: url,
httpClient: &http.Client{Timeout: 15 * time.Second},
}, nil
}
50 changes: 33 additions & 17 deletions logdna/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,31 @@ type httpClientInterface interface {

// Configuration for the HTTP client used to make requests to remote resources
type requestConfig struct {
serviceKey string
httpClient httpClientInterface
apiURL string
method string
body interface{}
httpRequest httpRequest
bodyReader bodyReader
jsonMarshal jsonMarshal
serviceKey string
iamtoken string
cloud_resource_name string
httpClient httpClientInterface
apiURL string
method string
body interface{}
httpRequest httpRequest
bodyReader bodyReader
jsonMarshal jsonMarshal
}

// newRequestConfig abstracts the struct creation to allow for mocking
func newRequestConfig(pc *providerConfig, method string, uri string, body interface{}, mutators ...func(*requestConfig)) *requestConfig {
rc := &requestConfig{
serviceKey: pc.serviceKey,
httpClient: pc.httpClient,
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
method: method,
body: body,
httpRequest: http.NewRequest,
bodyReader: io.ReadAll,
jsonMarshal: json.Marshal,
serviceKey: pc.serviceKey,
iamtoken: pc.iamtoken,
cloud_resource_name: pc.cloud_resource_name,
httpClient: pc.httpClient,
apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/)
method: method,
body: body,
httpRequest: http.NewRequest,
bodyReader: io.ReadAll,
jsonMarshal: json.Marshal,
}

// Used during testing only; Allow mutations passed in by tests
Expand All @@ -64,7 +68,19 @@ func (c *requestConfig) MakeRequest() ([]byte, error) {
if payloadBuf.Len() > 0 {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("servicekey", c.serviceKey)

// Set the correct authorization headers depending on what has been passed in
// the provider config
if c.serviceKey != "" {
req.Header.Set("servicekey", c.serviceKey)
} else if c.iamtoken != "" && c.cloud_resource_name != "" {
req.Header.Set("Authorization", "Bearer "+c.iamtoken)
req.Header.Set("cloud-resource-name", c.cloud_resource_name)
} else {
err := fmt.Errorf("expected either servicekey or iamtoken to be set")
return nil, err
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error during HTTP request: %s", err)
Expand Down
97 changes: 97 additions & 0 deletions logdna/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,44 @@ func TestRequest_MakeRequest(t *testing.T) {
assert.Nil(err, "No errors")
})

t.Run("Server receives proper method, URL, and headers for request with a body, iamtoken", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal("GET", r.Method, "method is correct")
assert.Equal(fmt.Sprintf("/someapi/%s", resourceID), r.URL.String(), "URL is correct")
_, ok := r.Header["Servicekey"]
assert.Equal(false, ok, "servicekey header is not set")
key, ok := r.Header["Authorization"]
assert.Equal(true, ok, "Authorization header is set")
assert.Equal("Bearer testtoken", key[0], "Authorization header is correct")
key, ok = r.Header["Cloud-Resource-Name"]
assert.Equal(true, ok, "cloud-resource-name header is set")
assert.Equal("testibmcrn", key[0], "cloud_resource_name header is correct")
key = r.Header["Content-Type"]
assert.Equal("application/json", key[0], "content-type header is correct")
}))
defer ts.Close()

pc.baseURL = ts.URL
tempServiceKey := pc.serviceKey
pc.serviceKey = ""
pc.iamtoken = "testtoken"
pc.cloud_resource_name = "testibmcrn"

req := newRequestConfig(
&pc,
"GET",
fmt.Sprintf("/someapi/%s", resourceID),
testRequest{},
)

_, err := req.MakeRequest()
assert.Nil(err, "No errors")

pc.serviceKey = tempServiceKey
pc.iamtoken = ""
pc.cloud_resource_name = ""
})

t.Run("Server receives proper method, URL, and headers for request without a body", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal("GET", r.Method, "method is correct")
Expand Down Expand Up @@ -120,6 +158,39 @@ func TestRequest_MakeRequest(t *testing.T) {
)
})

t.Run("Reads and decodes response from the server, iamtoken", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(viewResponse{ViewID: "test123456"})
assert.Nil(err, "No errors")
}))
defer ts.Close()

pc.baseURL = ts.URL
tempServiceKey := pc.serviceKey
pc.serviceKey = ""
pc.iamtoken = "testtoken"
pc.cloud_resource_name = "testibmcrn"

req := newRequestConfig(
&pc,
"GET",
fmt.Sprintf("/someapi/%s", resourceID),
nil,
)

body, err := req.MakeRequest()
assert.Nil(err, "No errors")
assert.Equal(
`{"viewID":"test123456"}`,
strings.TrimSpace(string(body)),
"Returned body is correct",
)

pc.serviceKey = tempServiceKey
pc.iamtoken = ""
pc.cloud_resource_name = ""
})

t.Run("Successfully marshals a provided body", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
postedBody, _ := io.ReadAll(r.Body)
Expand Down Expand Up @@ -260,4 +331,30 @@ func TestRequest_MakeRequest(t *testing.T) {
"Expected error message",
)
})

t.Run("Returns error if servicekey and iamtoken are blank", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
}))
defer ts.Close()

pc.baseURL = ts.URL
pc.serviceKey = ""
pc.iamtoken = ""

req := newRequestConfig(
&pc,
"POST",
fmt.Sprintf("/someapi/%s", resourceID),
nil,
)

_, err := req.MakeRequest()
assert.Error(err, "Expected error")
assert.Equal(
true,
strings.Contains(err.Error(), "expected either servicekey or iamtoken to be set"),
"Expected error message",
)
})
}

0 comments on commit f9fc0b0

Please sign in to comment.