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

Return an informative error message when using 'api/v1' for S3 GW endpoint #7828

Merged

Conversation

itaigilo
Copy link
Contributor

@itaigilo itaigilo commented Jun 3, 2024

Closes #3082

Change Description

Quite a few users are trying sometimes to access the S3 Gateway using an endpoint starting with /api/v1.
This used to lead to a NoSuchBucket 404 error.

After the fix, such scenario leads to a 404 error, but with a different message:

Using lakeFS API URI as the endpoint. Try removing the 'api/v1/' part from the endpoint.

@itaigilo itaigilo requested a review from arielshaqed June 3, 2024 13:37
@itaigilo itaigilo added include-changelog PR description should be included in next release changelog area/gateway Changes to the gateway labels Jun 3, 2024
Copy link

github-actions bot commented Jun 3, 2024

E2E Test Results - DynamoDB Local - Local Block Adapter

13 passed

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

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

Thanks, great improvement! Requesting changes to help users who legitimately have a bucket "api" and a tag "v1".

@@ -341,6 +342,11 @@ var Codes = errorCodeMap{
Description: "The bucket lifecycle configuration does not exist",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchBucketPossibleAPIEndpoint: {
Code: "ErrNoSuchBucketPossibleAPIEndpoint",
Description: "Using lakeFS API URI as the endpoint. Try removing the 'api/v1/' part from the endpoint.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps afford people an explanation? I'm thinking of the poor user who has a repo named "api" and manages to mitype a branch name "v1"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you squeeze more into an error message?
If so - any suggestions on how to improve this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm... good thing they don't deduct bandwidth for errors from my paycheck. I'd go with a definitive error message, followed by a suggestion:

Suggested change
Description: "Using lakeFS API URI as the endpoint. Try removing the 'api/v1/' part from the endpoint.",
Description: `Repository "api" not found; this can happen if your endpoint URL mistakenly ends in "` + apiutil.BaseURL + `".`,

(the ` is an alternative quotation mark that reduces the number of backslashes needed here.)

@@ -184,6 +184,13 @@ func EnrichWithRepositoryOrFallback(c *catalog.Catalog, authService auth.Gateway
fallbackProxy.ServeHTTP(w, req)
return
}

// see: github.com/treeverse/lakeFS/issues/3082
Copy link
Contributor

Choose a reason for hiding this comment

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

Or just state what this is. If I want to know who added this code, GitHub has excellent blame.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Done.

@@ -184,6 +184,13 @@ func EnrichWithRepositoryOrFallback(c *catalog.Catalog, authService auth.Gateway
fallbackProxy.ServeHTTP(w, req)
return
}

// see: github.com/treeverse/lakeFS/issues/3082
if strings.HasPrefix(req.RequestURI, "/api/v1") {
Copy link
Contributor

Choose a reason for hiding this comment

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

This has to be defined on some constant. Can we use that constant here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. Done.

@@ -184,6 +184,13 @@ func EnrichWithRepositoryOrFallback(c *catalog.Catalog, authService auth.Gateway
fallbackProxy.ServeHTTP(w, req)
return
}

// see: github.com/treeverse/lakeFS/issues/3082
if strings.HasPrefix(req.RequestURI, "/api/v1") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need to fail to find a bucket first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. As far as I understand this, it's the last line before turning to the ErrNoSuchBucket error.

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

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

Thanks!

How was this tested? OK to pull with only a manual test, but if you do then please also add a test in a later PR.

@@ -341,6 +342,11 @@ var Codes = errorCodeMap{
Description: "The bucket lifecycle configuration does not exist",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchBucketPossibleAPIEndpoint: {
Code: "ErrNoSuchBucketPossibleAPIEndpoint",
Description: "Using lakeFS API URI as the endpoint. Try removing the 'api/v1/' part from the endpoint.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm... good thing they don't deduct bandwidth for errors from my paycheck. I'd go with a definitive error message, followed by a suggestion:

Suggested change
Description: "Using lakeFS API URI as the endpoint. Try removing the 'api/v1/' part from the endpoint.",
Description: `Repository "api" not found; this can happen if your endpoint URL mistakenly ends in "` + apiutil.BaseURL + `".`,

(the ` is an alternative quotation mark that reduces the number of backslashes needed here.)

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

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

You added the test -- yay! -- but I suspect that it is broken because /api/v1 needs to be a prefix of the path of the URL. So now I'm requesting that change.


accessKeyID := viper.GetString("access_key_id")
secretAccessKey := viper.GetString("secret_access_key")
endpoint := "/api/v1" + viper.GetString("s3_endpoint")
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this is backwards!?

Copy link
Member

@N-o-Z N-o-Z left a comment

Choose a reason for hiding this comment

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

Thanks!
The code looks good, some comments on the test


accessKeyID := viper.GetString("access_key_id")
secretAccessKey := viper.GetString("secret_access_key")
endpoint := "/api/v1" + viper.GetString("s3_endpoint")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
endpoint := "/api/v1" + viper.GetString("s3_endpoint")
endpoint := viper.GetString("s3_endpoint") + apiutil.BaseURL


t.Run("use_api_v1_for_client_endpoint", func(t *testing.T) {
_, err := minioClient.GetObject(ctx, repo, "main/some", minio.GetObjectOptions{})
require.Contains(t, err.Error(), "Using lakeFS API URI as the endpoint")
Copy link
Member

Choose a reason for hiding this comment

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

This will panic if err is nil. You need to check

Copy link
Member

Choose a reason for hiding this comment

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

also, not sure but I think you can use require.ErrorIs

t.Fatalf("minio.New: %s", err)
}

t.Run("use_api_v1_for_client_endpoint", func(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Better for forward compatibility

Suggested change
t.Run("use_api_v1_for_client_endpoint", func(t *testing.T) {
t.Run("use_open_api_for_client_endpoint", func(t *testing.T) {

@itaigilo
Copy link
Contributor Author

itaigilo commented Jun 4, 2024

Well, the test I've added fails, for:

minio.New: Endpoint url cannot have fully qualified paths.

I'm using s3.local.lakefs.io:8000/api/v1 for the endpoint of minio, but this isn't allowed (no extra path is allowed, only hostname).

@arielshaqed @N-o-Z Can you think of another way to test this, or should I release it without the test?

@N-o-Z
Copy link
Member

N-o-Z commented Jun 4, 2024

Well, the test I've added fails, for:

minio.New: Endpoint url cannot have fully qualified paths.

I'm using s3.local.lakefs.io:8000/api/v1 for the endpoint of minio, but this isn't allowed (no extra path is allowed, only hostname).

@arielshaqed @N-o-Z Can you think of another way to test this, or should I release it without the test?

One possible option is not using a minio client.
Bottom line you are testing an http server and want to check that when you initiate a request with a certain path prefix you get a specific error. There are several ways to test that I assume

@itaigilo
Copy link
Contributor Author

itaigilo commented Jun 4, 2024

One possible option is not using a minio client. Bottom line you are testing an http server and want to check that when you initiate a request with a certain path prefix you get a specific error. There are several ways to test that I assume

Yeah this makes sense -
But I don't think it's worth the effort for this case.
Unless you think it'll be a useful infra for the future (I don't think so, but I probably don't have enough context)?

@N-o-Z
Copy link
Member

N-o-Z commented Jun 4, 2024

One possible option is not using a minio client. Bottom line you are testing an http server and want to check that when you initiate a request with a certain path prefix you get a specific error. There are several ways to test that I assume

Yeah this makes sense - But I don't think it's worth the effort for this case. Unless you think it'll be a useful infra for the future (I don't think so, but I probably don't have enough context)?

If there's a scenario, we need to test it.
Adding an automated test protects us from regression

Copy link
Member

@N-o-Z N-o-Z left a comment

Choose a reason for hiding this comment

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

Some additional comments

Comment on lines 674 to 675
require.NotNil(t, listErr)
require.Contains(t, listErr.Error(), `Repository "api" not found`)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
require.NotNil(t, listErr)
require.Contains(t, listErr.Error(), `Repository "api" not found`)
require.ErrorContains(t, listErr, gtwerrors.ErrNoSuchBucketPossibleAPIEndpoint.Error())

s3Client, err := testutil.SetupTestS3Client(endpoint, accessKeyID, secretAccessKey)
require.NoError(t, err, "failed creating s3 client")

_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("test")})
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("test")})
_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("not-exists")})

Comment on lines 683 to 685
_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("test")})
require.NotNil(t, listErr)
require.NotContains(t, listErr.Error(), `Repository "api" not found`)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("test")})
require.NotNil(t, listErr)
require.NotContains(t, listErr.Error(), `Repository "api" not found`)
_, listErr := s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String("not-exist")})
require.ErrorContains(t, listErr, gtwerrors.ErrNoSuchBucket.Error())

@itaigilo
Copy link
Contributor Author

itaigilo commented Jun 5, 2024

Rewrote the tests, PTAL 🙏

Copy link
Member

@N-o-Z N-o-Z left a comment

Choose a reason for hiding this comment

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

Excellent work! I have one suggestion but not blocking

Copy link
Member

Choose a reason for hiding this comment

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

Nice one

@@ -658,30 +658,27 @@ func TestS3ReadObjectRedirect(t *testing.T) {
})
}

func createS3Client(endpoint string, t *testing.T) *s3.Client {
Copy link
Member

Choose a reason for hiding this comment

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

Just as thought,
There are several places who use testutil.SetupTestS3Client to change the endpoint (as you already noticed).
Suggest to put this func under the testutil package and use it in all the places where testutil.SetupTestS3Client is called.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer not to do this. viper state is, by definition, global. So I want all uses of viper to appear in the test, rather than in a "don't worry about it" setup function.

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

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

Thanks!

Would really like not to read config in testutil.

@@ -177,7 +177,7 @@ func testSymlinkS3Exporter(t *testing.T, ctx context.Context, repo string, tmplD
storageURL, err := url.Parse(symlinksPrefix)
require.NoError(t, err, "failed extracting bucket name")

s3Client, err := testutil.SetupTestS3Client("https://s3.amazonaws.com", testData.AWSAccessKeyID, testData.AWSSecretAccessKey)
s3Client, err := testutil.SetupTestS3Client("https://s3.amazonaws.com", testData.AWSAccessKeyID, testData.AWSSecretAccessKey, viper.GetBool("force_path_style"))
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 prefer the same style as in delete_objects_test.go, below. But what I really want is for both calls to look the same.

@@ -658,30 +658,27 @@ func TestS3ReadObjectRedirect(t *testing.T) {
})
}

func createS3Client(endpoint string, t *testing.T) *s3.Client {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer not to do this. viper state is, by definition, global. So I want all uses of viper to appear in the test, rather than in a "don't worry about it" setup function.

@itaigilo itaigilo merged commit 7ef5337 into master Jun 6, 2024
35 checks passed
@itaigilo itaigilo deleted the feature/show-informative-error-for-s3-gw-api-endpoint-usage branch June 6, 2024 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/gateway Changes to the gateway include-changelog PR description should be included in next release changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

gateway should identify wrong endpoints requests and return a friendly error
3 participants