diff --git a/go.mod b/go.mod index 4b0a64b..30013cb 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,28 @@ module github.com/nudibranches-tech/bifrost-hyperfluid-sdk-dev go 1.25.1 -require github.com/joho/godotenv v1.5.1 +require ( + github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2/config v1.32.6 + github.com/aws/aws-sdk-go-v2/credentials v1.19.6 + github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/smithy-go v1.24.0 // indirect +) diff --git a/go.sum b/go.sum index d61b19e..221930e 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,40 @@ +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= +github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/sdk/builders/fluent/s3_builder.go b/sdk/builders/fluent/s3_builder.go new file mode 100644 index 0000000..c81b6df --- /dev/null +++ b/sdk/builders/fluent/s3_builder.go @@ -0,0 +1,448 @@ +package fluent + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/nudibranches-tech/bifrost-hyperfluid-sdk-dev/sdk/utils" +) + +// S3Builder provides a fluent interface for S3/MinIO operations using OIDC STS +type S3Builder struct { + client interface { + GetConfig() utils.Configuration + } + + errors []error + + bucket string + key string + s3Client *s3.Client + stsClient *sts.Client + + idToken string + sessionName string + roleArn string + + stsMethod string // "oidc" or "" + oidcEnabled bool +} + +// NewS3Builder creates a new S3Builder instance configured for MinIO +func NewS3Builder(client interface { + GetConfig() utils.Configuration +}) (*S3Builder, error) { + cfg := client.GetConfig() + err := verifyBasicConfig(cfg) + if err != nil { + return nil, err + } + + // Check if we should use OIDC or static credentials + useOIDC := getEnvOrConfig(cfg, "MINIO_USE_OIDC", "false") == "true" + + if useOIDC { + return newS3BuilderWithOIDC(client) + } + + return newS3BuilderWithStaticCreds(client) +} + +func verifyBasicConfig(cfg utils.Configuration) error { + if cfg.MinIOEndpoint == "" { + return fmt.Errorf("MINIO_ENDPOINT is required") + } + if cfg.MinIORegion == "" { + return fmt.Errorf("MINIO_REGION is required") + } + return nil +} + +// newS3BuilderWithStaticCreds creates S3Builder with static MinIO credentials +func newS3BuilderWithStaticCreds(client interface { + GetConfig() utils.Configuration +}) (*S3Builder, error) { + cfg := client.GetConfig() + + if cfg.MinIOAccessKey == "" { + return nil, fmt.Errorf("MINIO_ACCESS_KEY is required") + } + if cfg.MinIOSecretKey == "" { + return nil, fmt.Errorf("MINIO_SECRET_KEY is required") + } + + ctx := context.Background() + awsCfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion(cfg.MinIORegion), + config.WithBaseEndpoint(cfg.MinIOEndpoint), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.MinIOAccessKey, + cfg.MinIOSecretKey, + "", + )), + ) + if err != nil { + return nil, fmt.Errorf("failed to load MinIO config: %w", err) + } + + s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + }) + + return &S3Builder{ + client: client, + s3Client: s3Client, + errors: []error{}, + oidcEnabled: false, + }, nil +} + +// newS3BuilderWithOIDC creates S3Builder configured for OIDC STS +func newS3BuilderWithOIDC(client interface { + GetConfig() utils.Configuration +}) (*S3Builder, error) { + cfg := client.GetConfig() + ctx := context.Background() + + // Create base config with anonymous credentials for STS + awsCfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion(cfg.MinIORegion), + config.WithCredentialsProvider(aws.AnonymousCredentials{}), + ) + if err != nil { + return nil, fmt.Errorf("failed to load base config: %w", err) + } + + isHttps, err := isHTTPS(cfg.MinIOEndpoint) + if err != nil { + return nil, fmt.Errorf("MinIO endpoint incorecctly formatted") + } + + // Create STS client pointing to MinIO's STS endpoint + stsClient := sts.NewFromConfig(awsCfg, func(o *sts.Options) { + // MinIO STS endpoint is typically at the base endpoint + o.BaseEndpoint = aws.String(cfg.MinIOEndpoint) + o.EndpointOptions.DisableHTTPS = !isHttps + }) + + // Create S3 client (will be updated after STS) + s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(cfg.MinIOEndpoint) + o.UsePathStyle = true + o.EndpointOptions.DisableHTTPS = !isHttps + }) + + return &S3Builder{ + client: client, + s3Client: s3Client, + stsClient: stsClient, + errors: []error{}, + oidcEnabled: true, + }, nil +} + +// isHTTPS checks if endpoint uses HTTPS +func isHTTPS(endpoint string) (bool, error) { + URL, err := url.Parse(endpoint) + if err != nil { + return false, err + } + return URL.Scheme == "https", nil +} + +// OIDC sets OIDC JWT token for AssumeRoleWithWebIdentity +func (s *S3Builder) OIDC(idToken string) *S3Builder { + if !s.oidcEnabled { + s.errors = append( + s.errors, + fmt.Errorf("OIDC cannot be used with static credentials; enable MINIO_USE_OIDC=true"), + ) + return s + } + + if idToken == "" { + s.errors = append(s.errors, fmt.Errorf("OIDC token cannot be empty")) + } + + s.idToken = idToken + s.stsMethod = "oidc" + return s +} + +// RoleArn sets the role ARN for AssumeRoleWithWebIdentity +func (s *S3Builder) RoleArn(roleArn string) *S3Builder { + if roleArn == "" { + s.errors = append(s.errors, fmt.Errorf("role ARN cannot be empty")) + } + s.roleArn = roleArn + return s +} + +// SessionName sets the session name +func (s *S3Builder) SessionName(sessionName string) *S3Builder { + s.sessionName = sessionName + return s +} + +// assumeRoleWithWebIdentity calls MinIO STS and updates the S3 client +func (s *S3Builder) assumeRoleWithWebIdentity(ctx context.Context) error { + if s.idToken == "" { + return fmt.Errorf("OIDC token is required for STS") + } + + // Build session name if not provided + sessionName := s.sessionName + if sessionName == "" { + sessionName = fmt.Sprintf("minio-session-%d", time.Now().Unix()) + } + + // Build input for AssumeRoleWithWebIdentity + // Note: RoleArn is optional for MinIO. MinIO determines permissions from JWT claims + // when RoleArn is not provided or uses RolePolicy when it is provided + input := &sts.AssumeRoleWithWebIdentityInput{ + WebIdentityToken: aws.String(s.idToken), + RoleSessionName: aws.String(sessionName), + DurationSeconds: aws.Int32(3600), // 1 hour + } + + // RoleArn is optional for MinIO but required by AWS SDK + if s.roleArn != "" { + // User explicitly provided a RoleArn + input.RoleArn = aws.String(s.roleArn) + } else { + // Use placeholder - MinIO ignores this and uses JWT claims for authorization + input.RoleArn = aws.String("arn:xxx:xxx:xxx:xxxx") + } + + // Call STS + output, err := s.stsClient.AssumeRoleWithWebIdentity(ctx, input) + if err != nil { + return fmt.Errorf("AssumeRoleWithWebIdentity failed: %w", err) + } + + if output.Credentials == nil { + return fmt.Errorf("STS returned no credentials") + } + + // Extract temporary credentials + creds := output.Credentials + accessKey := aws.ToString(creds.AccessKeyId) + secretKey := aws.ToString(creds.SecretAccessKey) + sessionToken := aws.ToString(creds.SessionToken) + + // Create new credentials provider with STS credentials + staticCreds := credentials.NewStaticCredentialsProvider( + accessKey, + secretKey, + sessionToken, + ) + + // Get MinIO config + cfg := s.client.GetConfig() + // Recreate AWS config with new credentials + ctx2 := context.Background() + awsCfg, err := config.LoadDefaultConfig(ctx2, + config.WithRegion(cfg.MinIORegion), + config.WithBaseEndpoint(cfg.MinIOEndpoint), + config.WithCredentialsProvider(staticCreds), + ) + if err != nil { + return fmt.Errorf("failed to create config with STS credentials: %w", err) + } + + isHttps, err := isHTTPS(cfg.MinIOEndpoint) + if err != nil { + return fmt.Errorf("MinIO endpoint incorecctly formatted") + } + + // Recreate S3 client with STS credentials + s.s3Client = s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + o.EndpointOptions.DisableHTTPS = !isHttps + }) + + return nil +} + +// Helper function to get config from environment or Configuration struct +func getEnvOrConfig(cfg utils.Configuration, key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + + switch key { + case "MINIO_ENDPOINT": + if cfg.MinIOEndpoint != "" { + return cfg.MinIOEndpoint + } + case "MINIO_ACCESS_KEY": + if cfg.MinIOAccessKey != "" { + return cfg.MinIOAccessKey + } + case "MINIO_SECRET_KEY": + if cfg.MinIOSecretKey != "" { + return cfg.MinIOSecretKey + } + case "MINIO_REGION": + if cfg.MinIORegion != "" { + return cfg.MinIORegion + } + case "MINIO_USE_OIDC": + if cfg.MinIOUseOIDC != "" { + return cfg.MinIOUseOIDC + } + } + + return fallback +} + +// Bucket sets the S3 bucket name +func (s *S3Builder) Bucket(bucket string) *S3Builder { + if bucket == "" { + s.errors = append(s.errors, fmt.Errorf("bucket name cannot be empty")) + } + s.bucket = bucket + return s +} + +// Key sets the S3 object key (file path) +func (s *S3Builder) Key(key string) *S3Builder { + if key == "" { + s.errors = append(s.errors, fmt.Errorf("object key cannot be empty")) + } + s.key = key + return s +} + +// validate checks that all required fields are set and runs STS if needed +func (s *S3Builder) validate(ctx context.Context) error { + if len(s.errors) > 0 { + return fmt.Errorf("validation failed: %s", s.errors[0].Error()) + } + if s.bucket == "" { + return fmt.Errorf("%w: bucket required", utils.ErrInvalidRequest) + } + if s.key == "" { + return fmt.Errorf("%w: key required", utils.ErrInvalidRequest) + } + + // If OIDC method is set, assume role before proceeding + if s.stsMethod == "oidc" { + return s.assumeRoleWithWebIdentity(ctx) + } + + return nil +} + +// S3Object represents a downloaded object from MinIO/S3 +type S3Object struct { + Bucket string + Key string + Size *int64 + ContentType string + LastModified *time.Time + Metadata map[string]string + Body io.ReadCloser // stream the content +} + +// Get retrieves the object from MinIO and returns a stream +func (s *S3Builder) Get(ctx context.Context) (*S3Object, error) { + if err := s.validate(ctx); err != nil { + return nil, err + } + + result, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s.key), + }) + if err != nil { + return nil, fmt.Errorf("failed to get object from MinIO: %w", err) + } + + // Return a struct with Body as io.ReadCloser for streaming + obj := &S3Object{ + Bucket: s.bucket, + Key: s.key, + Size: result.ContentLength, + ContentType: aws.ToString(result.ContentType), + LastModified: result.LastModified, + Metadata: result.Metadata, + Body: result.Body, // caller is responsible for closing + } + + return obj, nil +} + +// validateList checks validation errors and runs STS if needed (no key required) +func (s *S3Builder) validateList(ctx context.Context) error { + if len(s.errors) > 0 { + return fmt.Errorf("validation failed: %s", s.errors[0].Error()) + } + if s.bucket == "" { + return fmt.Errorf("%w: bucket required", utils.ErrInvalidRequest) + } + + if s.stsMethod == "oidc" { + return s.assumeRoleWithWebIdentity(ctx) + } + + return nil +} + +// List lists objects in the bucket with optional prefix +func (s *S3Builder) List(ctx context.Context, prefix string) (*utils.Response, error) { + if err := s.validateList(ctx); err != nil { + return nil, err + } + + input := &s3.ListObjectsV2Input{ + Bucket: aws.String(s.bucket), + } + if prefix != "" { + input.Prefix = aws.String(prefix) + } + + result, err := s.s3Client.ListObjectsV2(ctx, input) + if err != nil { + return &utils.Response{ + Status: utils.StatusError, + Error: fmt.Sprintf("failed to list objects from MinIO: %v", err), + HTTPCode: http.StatusInternalServerError, + }, err + } + + objects := make([]map[string]interface{}, 0, len(result.Contents)) + for _, obj := range result.Contents { + var lastModified *string + if obj.LastModified != nil { + s := obj.LastModified.Format(time.RFC3339) + lastModified = &s + } + + objects = append(objects, map[string]interface{}{ + "key": aws.ToString(obj.Key), + "size": obj.Size, + "last_modified": lastModified, // nil-safe + }) + } + + return &utils.Response{ + Status: utils.StatusOK, + Data: map[string]interface{}{ + "bucket": s.bucket, + "objects": objects, + "count": len(objects), + }, + HTTPCode: http.StatusOK, + }, nil +} diff --git a/sdk/client.go b/sdk/client.go index 296656e..c8b4b46 100644 --- a/sdk/client.go +++ b/sdk/client.go @@ -51,6 +51,10 @@ func (c *Client) Query() *fluent.QueryBuilder { return fluent.NewQueryBuilder(c) } +func (c *Client) S3() (*fluent.S3Builder, error) { + return fluent.NewS3Builder(c) +} + // Catalog starts a new fluent query with the catalog name. // This is a shortcut for client.Query().DataDock(defaultID).Catalog(name). // Uses DataDockID from config if available. diff --git a/sdk/utils/types.go b/sdk/utils/types.go index 5d23844..01d6d6a 100644 --- a/sdk/utils/types.go +++ b/sdk/utils/types.go @@ -20,6 +20,13 @@ type Configuration struct { KeycloakClientSecret string KeycloakUsername string KeycloakPassword string + + MinIORegion string + MinIOEndpoint string + MinIOAccessKey string + MinIOSecretKey string + MinIOUseSSL string + MinIOUseOIDC string } type Response struct { diff --git a/usage_examples/fluent_api_examples.go b/usage_examples/fluent_api_examples.go index a96cb8a..8533f00 100644 --- a/usage_examples/fluent_api_examples.go +++ b/usage_examples/fluent_api_examples.go @@ -13,45 +13,48 @@ import ( // This file demonstrates the new fluent API for the Bifrost SDK. // The fluent API provides a more intuitive and user-friendly way to interact with the SDK. -func runSearchExample() { - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Println("🎯 Search Example: Full-Text Search") - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - - config := getConfig() - client := sdk.NewClient(config) +/* + func runSearchExample() { + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Println("🎯 Search Example: Full-Text Search") + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + + config := getConfig() + client := sdk.NewClient(config) + + projectID := getEnv("BIFROST_DATADOCK_ID", "") + catalog := getEnv("BIFROST_TEST_CATALOG", "iceberg") + schema := getEnv("BIFROST_TEST_SCHEMA", "public") + table := getEnv("BIFROST_TEST_TABLE", "text_files") + + if projectID == "" { + fmt.Println("⚠️ Skipping: BIFROST_DATADOCK_ID not set") + fmt.Println() + return + } - projectID := getEnv("BIFROST_DATADOCK_ID", "") - catalog := getEnv("BIFROST_TEST_CATALOG", "iceberg") - schema := getEnv("BIFROST_TEST_SCHEMA", "public") - table := getEnv("BIFROST_TEST_TABLE", "text_files") + fmt.Println("📝 Full-text search query:") + fmt.Printf(" DataDock: %s\n", projectID) + fmt.Printf(" Searching in: %s.%s.%s\n", catalog, schema, table) + fmt.Println() - if projectID == "" { - fmt.Println("⚠️ Skipping: BIFROST_DATADOCK_ID not set") + // Search for content + resp, _ := client.Search(). + Query("rapport ventes"). + DataDock(projectID). // ✅ Use the actual UUID variable + Catalog(catalog). // ✅ Use actual catalog + Schema(schema). // ✅ Use actual schema + Table(table). // ✅ Use actual table + Columns("content", "summary"). // Adjust columns based on your table + Limit(10). + Execute(context.Background()) + + fmt.Println(resp.Results) fmt.Println() - return } +*/ - fmt.Println("📝 Full-text search query:") - fmt.Printf(" DataDock: %s\n", projectID) - fmt.Printf(" Searching in: %s.%s.%s\n", catalog, schema, table) - fmt.Println() - - // Search for content - resp, _ := client.Search(). - Query("rapport ventes"). - DataDock(projectID). // ✅ Use the actual UUID variable - Catalog(catalog). // ✅ Use actual catalog - Schema(schema). // ✅ Use actual schema - Table(table). // ✅ Use actual table - Columns("content", "summary"). // Adjust columns based on your table - Limit(10). - Execute(context.Background()) - - fmt.Println(resp.Results) - fmt.Println() -} - +/* func runFluentAPISimpleExample() { fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") fmt.Println("🎯 Fluent API Example 1: Simple Query") @@ -86,7 +89,9 @@ func runFluentAPISimpleExample() { handleResponse(resp, err) fmt.Println() } +*/ +/* func runFluentAPIWithSelectExample() { fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") fmt.Println("🎯 Fluent API Example 2: Query with SELECT") @@ -128,7 +133,9 @@ func runFluentAPIWithSelectExample() { handleResponse(resp, err) fmt.Println() } +*/ +/* func runFluentAPIComplexExample() { fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") fmt.Println("🎯 Fluent API Example 3: Complex Query") @@ -169,9 +176,9 @@ func runFluentAPIComplexExample() { handleResponse(resp, err) fmt.Println() } - +*/ // Helper functions - +/* func handleResponse(resp *utils.Response, err error) { if err != nil { fmt.Printf("❌ Error: %s\n", err.Error()) @@ -198,13 +205,84 @@ func handleResponse(resp *utils.Response, err error) { fmt.Printf("📦 Data: %v\n", dataMap) } } +*/ + +func runS3Example() { + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Println("🎯 S3 File Retrieval (SSO Auth)") + fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + + config := getConfig() + client := sdk.NewClient(config) + + bucket := getEnv("S3_BUCKET", "") + fileKey := getEnv("S3_FILE_KEY", "example/file.txt") + + if bucket == "" { + fmt.Println("⚠️ Skipping: S3_BUCKET not set") + fmt.Println() + return + } + + if config.KeycloakClientID == "" || config.KeycloakClientSecret == "" { + fmt.Println("⚠️ Skipping: Keycloak SSO credentials not configured") + fmt.Println() + return + } + + fmt.Printf("🔐 Using SSO authentication (Keycloak)\n") + fmt.Printf("📥 Retrieving: s3://%s/%s\n", bucket, fileKey) + + // Get file from S3 using SSO + s3Builder, err := client.S3() + if err != nil { + fmt.Printf("❌ Failed to create S3 builder: %v\n", err) + return + } + + obj, err := s3Builder. + Bucket(bucket). + Key(fileKey). + Get(context.Background()) + if err != nil { + fmt.Printf("❌ Failed to get object: %v\n", err) + return + } + + // Ensure Body is closed and check error + defer func() { + if cerr := obj.Body.Close(); cerr != nil { + fmt.Fprintf(os.Stderr, "warning: failed to close S3 object body: %v\n", cerr) + } + }() + + if obj.Size != nil { + fmt.Printf("📄 File size: %d bytes\n", *obj.Size) + } else { + fmt.Printf("📄 File size: unknown\n") + } + + fmt.Printf("📄 Content type: %s\n", obj.ContentType) + if obj.LastModified != nil { + fmt.Printf("📄 Last modified: %s\n", obj.LastModified.Format(time.RFC3339)) + } + + // Optionally read first 100 bytes of content + preview := make([]byte, 100) + n, _ := obj.Body.Read(preview) + if n > 0 { + fmt.Printf("📄 Preview: %s...\n", string(preview[:n])) + } + + fmt.Println() +} func getConfig() utils.Configuration { return utils.Configuration{ BaseURL: getEnv("HYPERFLUID_BASE_URL", ""), OrgID: getEnv("HYPERFLUID_ORG_ID", ""), Token: getEnv("HYPERFLUID_TOKEN", ""), - DataDockID: getEnv("HYPERFLUID_DATADOCK_ID", ""), // Add this, + DataDockID: getEnv("HYPERFLUID_DATADOCK_ID", ""), RequestTimeout: 30 * time.Second, MaxRetries: 3, @@ -214,6 +292,8 @@ func getConfig() utils.Configuration { KeycloakClientSecret: getEnv("KEYCLOAK_CLIENT_SECRET", ""), KeycloakUsername: getEnv("KEYCLOAK_USERNAME", ""), KeycloakPassword: getEnv("KEYCLOAK_PASSWORD", ""), + MinIOEndpoint: getEnv("MINIO_ENDPOINT", ""), + MinIOAccessKey: getEnv("MINIO_ACCESS_KEY", ""), } } @@ -224,6 +304,7 @@ func getEnv(key, fallback string) string { return fallback } +/* func splitColumns(cols string) []string { if cols == "" { return []string{} @@ -246,3 +327,4 @@ func splitColumns(cols string) []string { } return result } +*/ diff --git a/usage_examples/main.go b/usage_examples/main.go index c7ecf48..90528cd 100644 --- a/usage_examples/main.go +++ b/usage_examples/main.go @@ -26,10 +26,7 @@ func main() { fmt.Println("══════════════════════════════════════════════") fmt.Println() - runFluentAPISimpleExample() - runFluentAPIComplexExample() - runSearchExample() - runFluentAPIWithSelectExample() + runS3Example() fmt.Println() fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")