github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/io/providers/providers.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package providers
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/url"
    24  	"strings"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/aws/credentials"
    28  	"github.com/aws/aws-sdk-go/aws/session"
    29  	"gocloud.dev/blob"
    30  	_ "gocloud.dev/blob/memblob"
    31  	"gocloud.dev/blob/s3blob"
    32  
    33  	prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    34  )
    35  
    36  const (
    37  	S3 = "s3"
    38  	GS = "gs"
    39  	// TODO(danilo-gemoli): complete the implementation since at this time only opener.Writer()
    40  	// is supported
    41  	File = "file"
    42  )
    43  
    44  // DisplayName turns canonical provider IDs into displayable names.
    45  func DisplayName(provider string) string {
    46  	switch provider {
    47  	case GS:
    48  		return "GCS"
    49  	case S3:
    50  		return "S3"
    51  	case File:
    52  		return "File"
    53  	}
    54  	return provider
    55  }
    56  
    57  // GetBucket opens and returns a gocloud blob.Bucket based on credentials and a path.
    58  // The path is used to discover which storageProvider should be used.
    59  //
    60  // If the storageProvider file is detected, we don't need any credentials and just open a file bucket
    61  // If no credentials are given, we just fall back to blob.OpenBucket which tries to auto discover credentials
    62  // e.g. via environment variables. For more details, see: https://gocloud.dev/howto/blob/
    63  //
    64  // If we specify credentials and an 3:// path is used, credentials must be given in one of the
    65  // following formats:
    66  //   - AWS S3 (s3://):
    67  //     {
    68  //     "region": "us-east-1",
    69  //     "s3_force_path_style": true,
    70  //     "access_key": "access_key",
    71  //     "secret_key": "secret_key"
    72  //     }
    73  //   - S3-compatible service, e.g. self-hosted Minio (s3://):
    74  //     {
    75  //     "region": "minio",
    76  //     "endpoint": "https://minio-hl-svc.minio-operator-ns:9000",
    77  //     "s3_force_path_style": true,
    78  //     "access_key": "access_key",
    79  //     "secret_key": "secret_key"
    80  //     }
    81  func GetBucket(ctx context.Context, s3Credentials []byte, path string) (*blob.Bucket, error) {
    82  	storageProvider, bucket, _, err := ParseStoragePath(path)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	if storageProvider == S3 && len(s3Credentials) > 0 {
    87  		return getS3Bucket(ctx, s3Credentials, bucket)
    88  	}
    89  
    90  	bkt, err := blob.OpenBucket(ctx, fmt.Sprintf("%s://%s", storageProvider, bucket))
    91  	if err != nil {
    92  		return nil, fmt.Errorf("error opening file bucket: %w", err)
    93  	}
    94  	return bkt, nil
    95  }
    96  
    97  // s3Credentials are credentials used to access S3 or an S3-compatible storage service
    98  // Endpoint is an optional property. Default is the AWS S3 endpoint. If set, the specified
    99  // endpoint will be used instead.
   100  type s3Credentials struct {
   101  	Region           string `json:"region"`
   102  	Endpoint         string `json:"endpoint"`
   103  	Insecure         bool   `json:"insecure"`
   104  	S3ForcePathStyle bool   `json:"s3_force_path_style"`
   105  	AccessKey        string `json:"access_key"`
   106  	SecretKey        string `json:"secret_key"`
   107  }
   108  
   109  // getS3Bucket opens a gocloud blob.Bucket based on given credentials in the format the
   110  // struct s3Credentials defines (see documentation of GetBucket for an example)
   111  func getS3Bucket(ctx context.Context, creds []byte, bucketName string) (*blob.Bucket, error) {
   112  	s3Creds := &s3Credentials{}
   113  	if err := json.Unmarshal(creds, s3Creds); err != nil {
   114  		return nil, fmt.Errorf("error getting S3 credentials from JSON: %w", err)
   115  	}
   116  
   117  	cfg := &aws.Config{}
   118  
   119  	//  Use the default credential chain if no credentials are specified
   120  	if s3Creds.AccessKey != "" && s3Creds.SecretKey != "" {
   121  		staticCredentials := credentials.StaticProvider{
   122  			Value: credentials.Value{
   123  				AccessKeyID:     s3Creds.AccessKey,
   124  				SecretAccessKey: s3Creds.SecretKey,
   125  			},
   126  		}
   127  
   128  		cfg.Credentials = credentials.NewChainCredentials([]credentials.Provider{&staticCredentials})
   129  	}
   130  
   131  	cfg.Endpoint = aws.String(s3Creds.Endpoint)
   132  	cfg.DisableSSL = aws.Bool(s3Creds.Insecure)
   133  	cfg.S3ForcePathStyle = aws.Bool(s3Creds.S3ForcePathStyle)
   134  	cfg.Region = aws.String(s3Creds.Region)
   135  
   136  	sess, err := session.NewSession(cfg)
   137  	if err != nil {
   138  		return nil, fmt.Errorf("error creating S3 Session: %w", err)
   139  	}
   140  
   141  	bkt, err := s3blob.OpenBucket(ctx, sess, bucketName, nil)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("error opening S3 bucket: %w", err)
   144  	}
   145  	return bkt, nil
   146  }
   147  
   148  // HasStorageProviderPrefix returns true if the given string starts with
   149  // any of the known storageProviders and a slash, e.g.
   150  // * gs/kubernetes-jenkins returns true
   151  // * kubernetes-jenkins returns false
   152  func HasStorageProviderPrefix(path string) bool {
   153  	return strings.HasPrefix(path, GS+"/") || strings.HasPrefix(path, S3+"/")
   154  }
   155  
   156  // ParseStoragePath parses storagePath and returns the storageProvider, bucket and relativePath
   157  // For example gs://prow-artifacts/test.log results in (gs, prow-artifacts, test.log)
   158  // Currently detected storageProviders are GS, S3 and file.
   159  // Paths with a leading / instead of a storageProvider prefix are treated as file paths for backwards
   160  // compatibility reasons.
   161  // File paths are split into a directory and a file. Directory is returned as bucket, file is returned.
   162  // as relativePath.
   163  // For all other paths the first part is treated as storageProvider prefix, the second segment as bucket
   164  // and everything after the bucket as relativePath.
   165  func ParseStoragePath(storagePath string) (storageProvider, bucket, relativePath string, err error) {
   166  	parsedPath, err := url.Parse(storagePath)
   167  	if err != nil {
   168  		return "", "", "", fmt.Errorf("unable to parse path %q: %w", storagePath, err)
   169  	}
   170  
   171  	storageProvider = parsedPath.Scheme
   172  	bucket, relativePath = parsedPath.Host, parsedPath.Path
   173  	relativePath = strings.TrimPrefix(relativePath, "/")
   174  
   175  	if bucket == "" {
   176  		return "", "", "", fmt.Errorf("could not find bucket in storagePath %q", storagePath)
   177  	}
   178  	return storageProvider, bucket, relativePath, nil
   179  }
   180  
   181  // StoragePath is the reverse of ParseStoragePath.
   182  func StoragePath(bucket, path string) (string, error) {
   183  	pp, err := prowv1.ParsePath(bucket)
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  	return fmt.Sprintf("%s://%s/%s", pp.StorageProvider(), pp.Bucket(), path), nil
   188  }