Skip to content
Draft
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
14 changes: 7 additions & 7 deletions packages/automl/api/openapi/automl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ paths:
- 'displayName': Extracted from 'openshift.io/display-name' annotation if present
- 'description': Extracted from 'openshift.io/description' annotation if present

/api/v1/s3/file:
summary: Path used to get or upload a file in S3.
/api/v1/s3/files/{key}:
summary: Endpoints for working with a specific file from an S3-compatible connection.
description: >-
The REST endpoint/path used to retrieve or upload files in S3 storage.
GET returns an arbitrary file with transfer-encoding: chunked for efficient streaming.
Expand Down Expand Up @@ -225,7 +225,7 @@ paths:
type: string
example: my-bucket
- name: key
in: query
in: path
required: true
description: The S3 object key to retrieve
schema:
Expand Down Expand Up @@ -318,7 +318,7 @@ paths:
pattern: '^\S(.*\S)?$'
example: my-bucket
- name: key
in: query
in: path
required: true
description: >-
Requested S3 object key for the upload. If an object already exists at this key,
Expand Down Expand Up @@ -380,8 +380,8 @@ paths:
Returns 409 if the object key chosen after collision resolution still conflicts at upload time
(e.g. concurrent writer); the client should retry the upload.

/api/v1/s3/file/schema:
summary: Path used to get the schema (column names and types) of a CSV file from S3.
/api/v1/s3/files/{key}/schema:
summary: Endpoint to get the schema (column names and types) of a CSV file from an S3-compatible connection.
description: >-
The REST endpoint/path used to retrieve the schema of a CSV file from S3 storage.
Reads the header and a minimum of 100 data rows to determine column names and infer data types.
Expand Down Expand Up @@ -448,7 +448,7 @@ paths:
type: string
example: my-bucket
- name: key
in: query
in: path
required: true
description: The S3 object key (CSV file) to retrieve schema from
schema:
Expand Down
8 changes: 4 additions & 4 deletions packages/automl/bff/internal/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const (
UserPath = ApiPathPrefix + "/user"
NamespacePath = ApiPathPrefix + "/namespaces"
SecretsPath = ApiPathPrefix + "/secrets"
S3FilePath = ApiPathPrefix + "/s3/file"
S3FileSchemaPath = ApiPathPrefix + "/s3/file/schema"
S3FilePath = ApiPathPrefix + "/s3/files/:key"
S3FileSchemaPath = ApiPathPrefix + "/s3/files/:key/schema"
S3FilesPath = ApiPathPrefix + "/s3/files"
PipelineRunsPath = ApiPathPrefix + "/pipeline-runs"
ModelRegistriesPath = ApiPathPrefix + "/model-registries"
Expand Down Expand Up @@ -262,8 +262,8 @@ func (app *App) Routes() http.Handler {
apiRouter.GET(S3FileSchemaPath, app.AttachNamespace(app.RequireAccessToPipelineServers(app.attachPipelineClientIfNeeded(app.GetS3FileSchemaHandler))))
apiRouter.GET(S3FilePath, app.AttachNamespace(app.RequireAccessToPipelineServers(app.attachPipelineClientIfNeeded(app.GetS3FileHandler))))
apiRouter.GET(S3FilesPath, app.AttachNamespace(app.RequireAccessToPipelineServers(app.attachPipelineClientIfNeeded(app.GetS3FilesHandler))))
// POST /s3/file deliberately omits attachPipelineClientIfNeeded: secretName is required; there is
// no DSPA fallback (creation flow uses an explicitly chosen input/target data secret).
// POST /s3/files/:key deliberately omits attachPipelineClientIfNeeded: secretName is required;
// there is no DSPA fallback (creation flow uses an explicitly chosen input/target data secret).
apiRouter.POST(S3FilePath, app.AttachNamespace(app.rejectDeclaredOversizedS3Post(app.RequireAccessToPipelineServers(app.PostS3FileHandler))))

// Model Registry - register model binary (target registry via path param + discovered ServerURL)
Expand Down
31 changes: 18 additions & 13 deletions packages/automl/bff/internal/api/s3_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,14 @@ func s3ConnectivityErrorMessage(bucket string) string {
}

// GetS3FileHandler retrieves a file from S3 storage.
// Path parameters:
// - key (required): S3 object key to retrieve.
//
// Query parameters:
// - secretName (optional): Kubernetes secret with S3 credentials.
// If omitted, credentials are taken from the DSPA associated with the namespace.
// - bucket (optional): S3 bucket; ignored on the DSPA path.
// - key (required): S3 object key to retrieve.
func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
queryParams := r.URL.Query()

secretName := queryParams.Get("secretName")
Expand All @@ -232,9 +234,9 @@ func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, _ httpr
return
}

key := strings.TrimSpace(queryParams.Get("key"))
key := strings.TrimSpace(ps.ByName("key"))
if key == "" {
app.badRequestResponse(w, r, errors.New("query parameter 'key' is required and cannot be empty"))
app.badRequestResponse(w, r, errors.New("path parameter 'key' is required and cannot be empty"))
return
}

Expand Down Expand Up @@ -300,14 +302,15 @@ func (app *App) effectivePostS3CollisionAttempts() int {
}

// PostS3FileHandler uploads a CSV file to S3 storage using credentials from a Kubernetes secret.
// Query parameters: namespace, secretName, key (required); bucket (optional, uses AWS_S3_BUCKET from secret if not provided).
// Path parameters: key (required).
// Query parameters: namespace, secretName (required); bucket (optional, uses AWS_S3_BUCKET from secret if not provided).
// Request body: multipart/form-data with a file part named "file". Only CSV uploads are allowed: Content-Type
// text/csv, or application/octet-stream with a .csv filename (or empty Content-Type with a .csv filename).
// Candidate keys are chosen via HeadObject; the file is streamed to S3 once with If-None-Match (no full-file buffer).
// If HeadObject and PUT disagree (concurrent writer), the handler returns 409 Conflict without retrying.
//
// Note: namespace is provided via the AttachNamespace middleware
func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
queryParams := r.URL.Query()

secretName := queryParams.Get("secretName")
Expand All @@ -320,9 +323,9 @@ func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, _ http
return
}

key := strings.TrimSpace(queryParams.Get("key"))
key := strings.TrimSpace(ps.ByName("key"))
if key == "" {
app.badRequestResponse(w, r, errors.New("query parameter 'key' is required and cannot be empty"))
app.badRequestResponse(w, r, errors.New("path parameter 'key' is required and cannot be empty"))
return
}

Expand Down Expand Up @@ -596,12 +599,14 @@ func s3GetResponseTypeAllowsInlineViewing(sanitizedContentType string) bool {
}

// GetS3FileSchemaHandler retrieves the schema (column names and types) from a CSV file in S3.
// Path parameters:
// - key (required): S3 object key (must be a .csv file).
//
// Query parameters:
// - secretName (optional): Kubernetes secret with S3 credentials.
// If omitted, credentials are taken from the DSPA associated with the namespace.
// - bucket (optional): S3 bucket; ignored on the DSPA path.
// - key (required): S3 object key (must be a .csv file).
func (app *App) GetS3FileSchemaHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (app *App) GetS3FileSchemaHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
queryParams := r.URL.Query()

secretName := queryParams.Get("secretName")
Expand All @@ -610,9 +615,9 @@ func (app *App) GetS3FileSchemaHandler(w http.ResponseWriter, r *http.Request, _
return
}

key := strings.TrimSpace(queryParams.Get("key"))
key := strings.TrimSpace(ps.ByName("key"))
if key == "" {
app.badRequestResponse(w, r, errors.New("query parameter 'key' is required and cannot be empty"))
app.badRequestResponse(w, r, errors.New("path parameter 'key' is required and cannot be empty"))
return
}

Expand Down Expand Up @@ -679,7 +684,7 @@ func (app *App) GetS3FileSchemaHandler(w http.ResponseWriter, r *http.Request, _
}
}

// S3FilesEnvelope is the response envelope for GET /s3/files.
// S3FilesEnvelope is the response envelope for GET /api/v1/s3/files.
type S3FilesEnvelope Envelope[models.S3ListObjectsResponse, None]

// GetS3FilesHandler lists objects in an S3 bucket.
Expand Down
5 changes: 2 additions & 3 deletions packages/automl/frontend/src/app/api/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type GetFilesOptions = {
// Public --------------------------------------------------------------------->

/**
* Uploads a file to S3 via the BFF POST /api/v1/s3/file endpoint.
* Uploads a file to S3 via the BFF POST /api/v1/s3/files/:key endpoint.
* Uses the given secret for credentials and the file's key (path) in the bucket.
*
* @param hostPath - Base path for API requests (e.g. '' for same-origin)
Expand All @@ -83,7 +83,6 @@ export async function uploadFileToS3(
const queryParams: Record<string, string> = {
namespace: params.namespace,
secretName: params.secretName,
key: params.key,
};
if (params.bucket !== undefined && params.bucket !== '') {
queryParams.bucket = params.bucket;
Expand All @@ -92,7 +91,7 @@ export async function uploadFileToS3(
const formData = new FormData();
formData.append('file', file, file.name);

const path = `${URL_PREFIX}/api/${BFF_API_VERSION}/s3/file`;
const path = `${URL_PREFIX}/api/${BFF_API_VERSION}/s3/files/${encodeURIComponent(params.key)}`;

const response = await handleRestFailures(restCREATE(hostPath, path, formData, queryParams));

Expand Down
2 changes: 1 addition & 1 deletion packages/automl/frontend/src/app/hooks/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type S3FileUploadMutationVariables = UploadFileToS3Params & {
};

/**
* React Query mutation for uploading a file to S3 via POST /api/v1/s3/file.
* React Query mutation for uploading a file to S3 via POST /api/v1/s3/files/:key.
* Uses hostPath '' for same-origin requests by default.
*/
export function useS3FileUploadMutation(
Expand Down
14 changes: 8 additions & 6 deletions packages/automl/frontend/src/app/hooks/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ export async function fetchS3File(
const { secretName, bucket, signal } = options ?? {};
const params = new URLSearchParams({
namespace,
key,
...(secretName && { secretName }),
...(bucket && { bucket }),
});

const response = await fetch(`${URL_PREFIX}/api/v1/s3/file?${params.toString()}`, { signal });
const response = await fetch(
`${URL_PREFIX}/api/v1/s3/files/${encodeURIComponent(key)}?${params.toString()}`,
{ signal },
);

if (!response.ok) {
let errorMessage = response.statusText;
Expand Down Expand Up @@ -127,13 +129,13 @@ export function useS3GetFileSchemaQuery(
const params = new URLSearchParams({
namespace,
secretName,
key,
...(bucket && { bucket }),
});

const response = await fetch(`${URL_PREFIX}/api/v1/s3/file/schema?${params.toString()}`, {
signal,
});
const response = await fetch(
`${URL_PREFIX}/api/v1/s3/files/${encodeURIComponent(key)}/schema?${params.toString()}`,
{ signal },
);

if (!response.ok) {
let errorMessage = response.statusText;
Expand Down
11 changes: 5 additions & 6 deletions packages/autorag/api/openapi/autorag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ paths:
# S3 Endpoints
# =============================================================================

/api/v1/s3/file:
summary: Path used to get or upload a file in S3.
/api/v1/s3/files/{key}:
summary: Endpoints for working with a specific file from an S3-compatible connection.
description: >-
The REST endpoint/path used to retrieve or upload files in S3 storage.
Uses the credentials from a specified Kubernetes secret to access the S3 bucket.
Expand Down Expand Up @@ -192,7 +192,7 @@ paths:
pattern: '^\S(.*\S)?$'
example: my-bucket
- name: key
in: query
in: path
required: true
description: The S3 object key to retrieve
schema:
Expand Down Expand Up @@ -275,7 +275,7 @@ paths:
pattern: '^\S(.*\S)?$'
example: my-bucket
- name: key
in: query
in: path
required: true
description: The S3 object key where the file will be uploaded
schema:
Expand Down Expand Up @@ -337,9 +337,8 @@ paths:
Returns 409 if the object key chosen after collision resolution still conflicts at upload time
(e.g. concurrent writer); the client should retry the upload.

# TODO [ Gustavo ] Ongoing discussion on what this endpoint and the other S3/file endpoint should be structured as
/api/v1/s3/files:
summary: Endpoints for dealing with multiple files within an S3-compatible connection
summary: Endpoints for working with files from an S3-compatible connection.
get:
operationId: getS3Files
summary: Get files from S3
Expand Down
6 changes: 3 additions & 3 deletions packages/autorag/bff/internal/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const (
UserPath = ApiPathPrefix + "/user"
NamespacePath = ApiPathPrefix + "/namespaces"
SecretsPath = ApiPathPrefix + "/secrets"
S3FilePath = ApiPathPrefix + "/s3/file"
S3FilePath = ApiPathPrefix + "/s3/files/:key"
S3FilesPath = ApiPathPrefix + "/s3/files"
LSDModelsPath = ApiPathPrefix + "/lsd/models"
LSDVectorStoresPath = ApiPathPrefix + "/lsd/vector-stores"
Expand Down Expand Up @@ -258,8 +258,8 @@ func (app *App) Routes() http.Handler {
// secretName (the handler resolves credentials directly in that case).
apiRouter.GET(S3FilePath, app.AttachNamespace(app.RequireAccessToService(app.attachPipelineClientIfNeeded(app.GetS3FileHandler))))
apiRouter.GET(S3FilesPath, app.AttachNamespace(app.RequireAccessToService(app.attachPipelineClientIfNeeded(app.GetS3FilesHandler))))
// POST /s3/file deliberately omits attachPipelineClientIfNeeded: secretName is required; there is
// no DSPA fallback (creation flow uses an explicitly chosen input/target data secret).
// POST /s3/files/:key deliberately omits attachPipelineClientIfNeeded: secretName is required;
// there is no DSPA fallback (creation flow uses an explicitly chosen input/target data secret).
apiRouter.POST(S3FilePath, app.AttachNamespace(app.rejectDeclaredOversizedS3Post(app.RequireAccessToService(app.PostS3FileHandler))))

// LLamaStack
Expand Down
19 changes: 11 additions & 8 deletions packages/autorag/bff/internal/api/s3_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,16 @@ func (app *App) resolveS3Client(w http.ResponseWriter, r *http.Request, secretNa
}

// GetS3FileHandler retrieves a file from S3 storage using credentials from a Kubernetes secret.
// Path parameters:
// - key (required): The S3 object key to retrieve
//
// Query parameters:
// - namespace (required): The Kubernetes namespace containing the secret
// - secretName (optional): Name of the Kubernetes secret holding S3 credentials.
// If omitted, the secret name is taken from the DSPA associated with the
// Pipeline Server in this namespace (set by AttachPipelineServerClient middleware).
// - bucket (optional): The S3 bucket name; if not provided, will use AWS_S3_BUCKET from the secret
// - key (required): The S3 object key to retrieve
func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
queryParams := r.URL.Query()

// Resolve S3 credentials from one of two sources:
Expand All @@ -247,9 +249,9 @@ func (app *App) GetS3FileHandler(w http.ResponseWriter, r *http.Request, _ httpr
return
}

key := strings.TrimSpace(queryParams.Get("key"))
key := strings.TrimSpace(ps.ByName("key"))
if key == "" {
app.badRequestResponse(w, r, errors.New("query parameter 'key' is required and cannot be empty"))
app.badRequestResponse(w, r, errors.New("path parameter 'key' is required and cannot be empty"))
return
}

Expand Down Expand Up @@ -325,13 +327,14 @@ func (app *App) effectivePostS3CollisionAttempts() int {
}

// PostS3FileHandler uploads a file to S3 storage using credentials from a Kubernetes secret.
// Query parameters: namespace, secretName, key (required); bucket (optional, uses AWS_S3_BUCKET from secret if not provided).
// Path parameters: key (required).
// Query parameters: namespace, secretName (required); bucket (optional, uses AWS_S3_BUCKET from secret if not provided).
// Request body: multipart/form-data with a file part named "file". Streams the file to S3 without buffering.
// Candidate keys are chosen via HeadObject; the file is streamed to S3 once with If-None-Match (no full-file buffer).
// If HeadObject and PUT disagree (concurrent writer), the handler returns 409 Conflict without retrying.
//
// Note: namespace is provided via the AttachNamespace middleware
func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
queryParams := r.URL.Query()

secretName := queryParams.Get("secretName")
Expand All @@ -344,9 +347,9 @@ func (app *App) PostS3FileHandler(w http.ResponseWriter, r *http.Request, _ http
return
}

key := strings.TrimSpace(queryParams.Get("key"))
key := strings.TrimSpace(ps.ByName("key"))
if key == "" {
app.badRequestResponse(w, r, errors.New("query parameter 'key' is required and cannot be empty"))
app.badRequestResponse(w, r, errors.New("path parameter 'key' is required and cannot be empty"))
return
}

Expand Down
5 changes: 2 additions & 3 deletions packages/autorag/frontend/src/app/api/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type GetFilesOptions = {
// Public --------------------------------------------------------------------->

/**
* Uploads a file to S3 via the BFF POST /api/v1/s3/file endpoint.
* Uploads a file to S3 via the BFF POST /api/v1/s3/files/:key endpoint.
* Uses the given secret for credentials and the file's key (path) in the bucket.
*
* @param hostPath - Base path for API requests (e.g. '' for same-origin)
Expand All @@ -83,7 +83,6 @@ export async function uploadFileToS3(
const queryParams: Record<string, string> = {
namespace: params.namespace,
secretName: params.secretName,
key: params.key,
};
if (params.bucket !== undefined && params.bucket !== '') {
queryParams.bucket = params.bucket;
Expand All @@ -92,7 +91,7 @@ export async function uploadFileToS3(
const formData = new FormData();
formData.append('file', file, file.name);

const path = `${URL_PREFIX}/api/${BFF_API_VERSION}/s3/file`;
const path = `${URL_PREFIX}/api/${BFF_API_VERSION}/s3/files/${encodeURIComponent(params.key)}`;

const response = await handleRestFailures(restCREATE(hostPath, path, formData, queryParams));

Expand Down
Loading
Loading