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

Configure apiKey, additionalHeaders, and host through environment variables #22

Merged
merged 11 commits into from
May 16, 2024

Conversation

austin-denoble
Copy link
Contributor

@austin-denoble austin-denoble commented May 8, 2024

Problem

There was a request to add additional configuration validation to the Go client as you could make requests with an empty apiKey. An empty check and early error return were added in a previous PR when passing apiKey directly: https://github.com/pinecone-io/go-pinecone/pull/18/files#diff-37c3a14781b4b6e74bcc0cfc8b2462ee2ae243a720a42669256248a3cb005f01R442-R444

We're also lacking the ability to specify an api key or headers through environment variables like the Python client, which can be a nice UX tweak for options in how you configure the client. Additionally, there's no way to specify a control plane host override, which is useful in situations where we need to hit a staging URL.

Solution

  • Refactor buildClientOptions() into a method on the NewClientParams struct rather than a function which receives the struct.
  • Add ability to specify PINECONE_API_KEY, PINECONE_ADDITIONAL_HEADERS, and PINECONE_CONTROLLER_HOST through environment variables. I kept the naming aligned with Python for now.
    • For these values, if the corresponding field has been passed through NewClientParams directly, the passed value will override anything set in the environment.
  • Add unit tests to validate override behavior, and headers being added as expected.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Infrastructure change (CI configs, etc)
  • Non-code change (docs, etc)
  • None of the above: (explain here)

Test Plan

CI and new unit tests.


…ent variables, add logic for using passed params over environment variables where applicable, add new unit tests
@austin-denoble austin-denoble changed the title Configure API Key and headers through environment variables Configure apiKey, additionalHeaders, and host through environment variables May 9, 2024
Copy link
Contributor

@haruska haruska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good improvement. It might be more clear to have multiple constructors vs ApiKey xor an Authorization header as a part of a headers array.

Technically, the ApiKey option is also just setting an Authorization header.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have two different constructors? One that requires an ApiKey and the other that requires a different auth mechanism (bearer token? oauth token?). It feels clunky that the "ApiKey is required unless..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does seem reasonable. I'm less clear on what this would look like though since we don't officially offer alternative methods of authentication quite yet. The other clients require an API key for instance.

If the user is able to supply headers directly then they could technically always pass their own auth headers. I think this would also be an issue on Python and TypeScript since they don't do any checking of the headers that are provided. You could also consider this a user-error that they'd need to resolve themselves.

I'm a bit unsure of what would make the most sense here to be honest. 🤔

@austin-denoble
Copy link
Contributor Author

Good improvement. It might be more clear to have multiple constructors vs ApiKey xor an Authorization header as a part of a headers array.
Technically, the ApiKey option is also just setting an Authorization header.

True, responded here: #22 (comment)

INTEGRATION_PINECONE_API_KEY: ${{ secrets.API_KEY }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to match up with .env.example above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split things out a bit and there are 2 environment keys for setting the API key:

  • PINECONE_API_KEY
  • INTEGRATION_PINECONE_API_KEY

Let me know if that's too confusing. Trying to test properly while one env value was used in both situations (the client vs. the tests) felt trickier than just having explicit values for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the "test" key usage more clear, I updated the name to TEST_PINECONE_API_KEY.

.env.example should also be updated:

PINECONE_API_KEY="<Project API Key>"
TEST_PINECONE_API_KEY="<Test Project API Key>"
TEST_POD_INDEX_NAME="<Pod based Index name>"
TEST_SERVERLESS_INDEX_NAME="<Serverless based Index name>"

if err != nil {
return nil, err
}

client, err := control.NewClient("https://api.pinecone.io", clientOptions...)
controlHostOverride := valueOrFallback(in.Host, os.Getenv("PINECONE_CONTROLLER_HOST"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"CONTROL" vs "CONTROLLER"? (Assuming this is talking about the control plane?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with PINECONE_CONTROLLER_HOST because that's the env key that's used in the Python and TypeScript clients for the same config value:

I suppose it is a bit confusing given it's applied as an override to the control plane. I think staying consistent makes sense, although the variable name here is confusing. What do you think?

@@ -16,25 +18,41 @@ import (

type Client struct {
apiKey string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is apiKey still needed in the Client struct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from Client as well as IndexConnection.

Headers map[string]string
Host string
RestClient *http.Client
SourceTag string
}

func NewClient(in NewClientParams) (*Client, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect this func to just call NewClientBase and configure the ApiKey as an auth header

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It now ultimately calls NewClientBase and passes the ApiKey through as part of the Headers map.

@@ -421,39 +460,82 @@ func derefOrDefault[T any](ptr *T, defaultValue T) T {
return *ptr
}

func buildClientOptions(in NewClientParams) ([]control.ClientOption, error) {
func (ncp *NewClientParams) buildClientOptions() ([]control.ClientOption, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could buildClientOptions to be a wrapper around buildClientBaseOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I slimmed things down to just buildClientBaseOptions, lemme know if that makes sense.

os.Unsetenv("PINECONE_ADDITIONAL_HEADERS")
}

// func (ts *ClientTests) TestAuthorizationHeaderOverridesApiKey() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meant to comment out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant to remove entirely, the PR wasn't really in a finished state. 😅 The client no longer does any explicit juggling of ApiKey and any auth header you may have passed.

…unc and add NewClientBase, update tests to support the new interface for creating clients, remove apiKey from Client and IndexConnection, handle turning apiKey into a header early, and allowing for complete control over headers without using apiKey
Copy link
Contributor

@haruska haruska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestions for your consideration. Good work!

.env.example Outdated
@@ -1,3 +1,4 @@
API_KEY="<Project API Key>"
PINECONE_API_KEY="<Project API Key>"
TEST_PINECONE_API_KEY="<Test Project API Key>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. 😄

if err != nil {
return nil, err
osApiKey := os.Getenv("PINECONE_API_KEY")
hasApiKey := (in.ApiKey != "" || osApiKey != "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality (of having an environment variable) instead of requiring the in.ApiKey seems unclear. On L26 it says that the ApiKey is required. Here, it seems it's optional if I set an environment variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I updated the comment to call that out, and also the error that's returned if you don't pass a key. I think adding docstrings we can make this a bit more clear and explicit.

return nil, fmt.Errorf("no API key provided, please pass an API key for authorization")
}

apiKeyHeader := struct{ Key, Value string }{"Api-Key", valueOrFallback(in.ApiKey, osApiKey)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that valueOrFallback could be utilized for the "hasApiKey" check?

func minOne(x int32) int32 {
if x < 1 {
return 1
func ensureHTTPS(inputURL string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add the option to disable https? (i'm thinking for local dev)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it's written currently means if you provide a Host or PINECONE_CONTROLLER_HOST, you're able to use whatever scheme as long as url.Parse() can read it out. https is only applied if there's no scheme, do you think that's sufficient?

For example, you should be able to pass http://localhost:9999 properly without "https" being added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup that works. The function name is a bit confusing...

clientOptions := buildClientBaseOptions(in)
var err error

controlHostOverride := valueOrFallback(in.Host, os.Getenv("PINECONE_CONTROLLER_HOST"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job throwing this in for consistency with other clients.

@austin-denoble austin-denoble merged commit c7eda0a into main May 16, 2024
3 checks passed
@austin-denoble austin-denoble deleted the adenoble/configuration-validation branch May 16, 2024 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants