Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Insert middleware to allow org and prod id on requests #142

Merged
merged 18 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/142.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:improvement
Add middleware support to httpclient package
```

```release-note:improvement
Add middleware that gets org ID and project ID from user profile and sets on request
```
6 changes: 6 additions & 0 deletions config/hcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ func (c *hcpConfig) validate() error {
return fmt.Errorf("either client credentials or oauth2 client ID must be provided")
}

// Ensure profile contains both org ID and project ID
if (c.profile.OrganizationID == "" && c.profile.ProjectID != "") ||
(c.profile.OrganizationID != "" && c.profile.ProjectID == "") {
return fmt.Errorf("when setting a user profile, both organization ID and project ID must be provided")
}

// Ensure the auth URL is valid
if c.authURL.Host == "" {
return fmt.Errorf("the auth URL has to be non-empty")
Expand Down
20 changes: 20 additions & 0 deletions config/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ func TestNew_Invalid(t *testing.T) {
},
expectedError: "the configuration is not valid: the SCADA address has to be non-empty",
},
{
name: "empty project ID with populated org ID",
options: []HCPConfigOption{
WithClientCredentials("my-client-id", "my-client-secret"),
WithProfile(&profile.UserProfile{
OrganizationID: "abc123",
}),
},
expectedError: "the configuration is not valid: when setting a user profile, both organization ID and project ID must be provided",
},
{
name: "empty org ID with populated project ID",
options: []HCPConfigOption{
WithClientCredentials("my-client-id", "my-client-secret"),
WithProfile(&profile.UserProfile{
ProjectID: "abc123",
}),
},
expectedError: "the configuration is not valid: when setting a user profile, both organization ID and project ID must be provided",
},
}

for _, testCase := range testCases {
Expand Down
26 changes: 15 additions & 11 deletions httpclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,6 @@ type Config struct {
// Deprecated: HCPConfig should be used instead
Client *http.Client
}
type roundTripperWithSourceChannel struct {
OriginalRoundTripper http.RoundTripper
SourceChannel string
}

func (rt *roundTripperWithSourceChannel) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("X-HCP-Source-Channel", rt.SourceChannel)
return rt.OriginalRoundTripper.RoundTrip(req)
}

// New creates a client with the right base path to connect to any HCP API
func New(cfg Config) (runtime *httptransport.Runtime, err error) {
Expand All @@ -78,10 +69,23 @@ func New(cfg Config) (runtime *httptransport.Runtime, err error) {
Source: cfg,
}

var opts []MiddlewareOption

if cfg.SourceChannel != "" {
// Use custom transport in order to set the source channel header when it is present.
sourceChannel := fmt.Sprintf("%s hcp-go-sdk/%s", cfg.SourceChannel, version.Version)
transport = &roundTripperWithSourceChannel{OriginalRoundTripper: transport, SourceChannel: sourceChannel}
sc := fmt.Sprintf("%s hcp-go-sdk/%s", cfg.SourceChannel, version.Version)

opts = append(opts, withSourceChannel(sc))
}

if cfg.Profile().OrganizationID != "" && cfg.Profile().ProjectID != "" {

opts = append(opts, withOrgAndProjectIDs(cfg.Profile().OrganizationID, cfg.Profile().ProjectID))
}

transport = &roundTripperWithMiddleware{
OriginalRoundTripper: transport,
MiddlewareOptions: opts,
}

// Set the scheme based on the TLS configuration.
Expand Down
41 changes: 41 additions & 0 deletions httpclient/httpclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

consul "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-service/stable/2021-02-04/client/consul_service"
Expand Down Expand Up @@ -151,4 +152,44 @@ func TestNew(t *testing.T) {
// just skip all the assertions!
require.Equal(t, uint32(2), atomic.LoadUint32(&numRequests))
})

}

func TestMiddleware(t *testing.T) {

// Start with a plain request.
request, err := http.NewRequest("GET", "api.cloud.hashicorp.com/consul/2021-02-04/organizations//projects//clusters", httptest.NewRecorder().Body)
require.NoError(t, err)

// Prepare header is unset.
require.Equal(t, request.Header.Get("X-HCP-Source-Channel"), "")

// Prepare middleware function.
expectedSourceChannel := "source_channel_foo"
sourceChannelMiddleware := withSourceChannel(expectedSourceChannel)

// Apply middleware function.
err = sourceChannelMiddleware(request)
require.NoError(t, err)

// Assert request is modified as expected.
assert.Equal(t, request.Header.Get("X-HCP-Source-Channel"), expectedSourceChannel)

// Assert path is unmodified.
expectedOrgID := "org_id_77"
expectedProjID := "proj_id_123"
assert.NotContains(t, request.URL.Path, expectedOrgID)
assert.NotContains(t, request.URL.Path, expectedProjID)

// Prepare middleware function.
profileMiddleware := withOrgAndProjectIDs(expectedOrgID, expectedProjID)

// Apply middleware function.
err = profileMiddleware(request)
require.NoError(t, err)

// Assert request is modified as expected.
assert.Contains(t, request.URL.Path, expectedOrgID)
assert.Contains(t, request.URL.Path, expectedProjID)
assert.Equal(t, request.Header.Get("X-HCP-Source-Channel"), expectedSourceChannel)
}
49 changes: 49 additions & 0 deletions httpclient/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package httpclient

import (
"fmt"
"net/http"
"strings"
)

// MiddlewareOption is a function that modifies an HTTP request.
type MiddlewareOption = func(req *http.Request) error

// roundTripperWithMiddleware takes a plain Roundtripper and an array of MiddlewareOptions to apply to the Roundtripper's request.
type roundTripperWithMiddleware struct {
OriginalRoundTripper http.RoundTripper
MiddlewareOptions []MiddlewareOption
}

// withSourceChannel updates the request header to include the HCP Go SDK source channel stamp.
func withSourceChannel(sourceChannel string) MiddlewareOption {
return func(req *http.Request) error {
req.Header.Set("X-HCP-Source-Channel", sourceChannel)
return nil
}
}

// withProfile takes the user profile's org ID and project ID and sets them in the request path if needed.
func withOrgAndProjectIDs(orgID, projID string) MiddlewareOption {
return func(req *http.Request) error {
path := req.URL.Path
path = strings.Replace(path, "organizations//", fmt.Sprintf("organizations/%s/", orgID), 1)
path = strings.Replace(path, "projects//", fmt.Sprintf("projects/%s/", projID), 1)
req.URL.Path = path
return nil
}
}

// RoundTrip attaches MiddlewareOption modifications to the request before sending along.
func (rt *roundTripperWithMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {

for _, mw := range rt.MiddlewareOptions {
if err := mw(req); err != nil {
// Failure to apply middleware should not fail the request
fmt.Printf("failed to apply middleware: %#v", mw(req))
continue
}
}

return rt.OriginalRoundTripper.RoundTrip(req)
}