github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/doltcore/dbfactory/aws.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dbfactory
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"net/url"
    21  	"os"
    22  	"strings"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/aws/credentials"
    26  	"github.com/aws/aws-sdk-go/aws/session"
    27  	"github.com/aws/aws-sdk-go/service/dynamodb"
    28  	"github.com/aws/aws-sdk-go/service/s3"
    29  
    30  	"github.com/dolthub/dolt/go/store/chunks"
    31  	"github.com/dolthub/dolt/go/store/datas"
    32  	"github.com/dolthub/dolt/go/store/nbs"
    33  	"github.com/dolthub/dolt/go/store/types"
    34  )
    35  
    36  const (
    37  	// AWSRegionParam is a creation parameter that can be used to set the AWS region
    38  	AWSRegionParam = "aws-region"
    39  
    40  	// AWSCredsTypeParam is a creation parameter that can be used to set the type of credentials that should be used.
    41  	// valid values are role, env, auto, and file
    42  	AWSCredsTypeParam = "aws-creds-type"
    43  
    44  	// AWSCredsFileParam is a creation parameter that can be used to specify a credential file to use.
    45  	AWSCredsFileParam = "aws-creds-file"
    46  
    47  	//AWSCredsProfile is a creation parameter that can be used to specify which AWS profile to use.
    48  	AWSCredsProfile = "aws-creds-profile"
    49  )
    50  
    51  // AWSCredentialSource is an enum type representing the different credential sources (auto, role, env, file, or invalid)
    52  type AWSCredentialSource int
    53  
    54  const (
    55  	InvalidCS AWSCredentialSource = iota - 1
    56  
    57  	// Auto will try env first and fall back to role (This is the default)
    58  	AutoCS
    59  
    60  	// Role Uses the AWS IAM role of the instance for auth
    61  	RoleCS
    62  
    63  	// Env uses the credentials stored in the environment variables AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY
    64  	EnvCS
    65  
    66  	// Uses credentials stored in a file
    67  	FileCS
    68  )
    69  
    70  // String returns the string representation of the of an AWSCredentialSource
    71  func (ct AWSCredentialSource) String() string {
    72  	switch ct {
    73  	case RoleCS:
    74  		return "role"
    75  	case EnvCS:
    76  		return "env"
    77  	case AutoCS:
    78  		return "auto"
    79  	case FileCS:
    80  		return "file"
    81  	default:
    82  		return "invalid"
    83  	}
    84  }
    85  
    86  // AWSCredentialSourceFromStr converts a string to an AWSCredentialSource
    87  func AWSCredentialSourceFromStr(str string) AWSCredentialSource {
    88  	strlwr := strings.TrimSpace(strings.ToLower(str))
    89  	switch strlwr {
    90  	case "", "auto":
    91  		return AutoCS
    92  	case "role":
    93  		return RoleCS
    94  	case "env":
    95  		return EnvCS
    96  	case "file":
    97  		return FileCS
    98  	default:
    99  		return InvalidCS
   100  	}
   101  }
   102  
   103  // AWSFactory is a DBFactory implementation for creating AWS backed databases
   104  type AWSFactory struct {
   105  }
   106  
   107  // CreateDB creates an AWS backed database
   108  func (fact AWSFactory) CreateDB(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]string) (datas.Database, error) {
   109  	var db datas.Database
   110  	cs, err := fact.newChunkStore(ctx, nbf, urlObj, params)
   111  
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	db = datas.NewDatabase(cs)
   117  
   118  	return db, nil
   119  }
   120  
   121  func (fact AWSFactory) newChunkStore(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]string) (chunks.ChunkStore, error) {
   122  	parts := strings.SplitN(urlObj.Hostname(), ":", 2) // [table]:[bucket]
   123  	if len(parts) != 2 {
   124  		return nil, errors.New("aws url has an invalid format")
   125  	}
   126  
   127  	opts, err := awsConfigFromParams(params)
   128  
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	dbName, err := validatePath(urlObj.Path)
   134  
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	sess := session.Must(session.NewSessionWithOptions(opts))
   140  	return nbs.NewAWSStore(ctx, nbf.VersionString(), parts[0], dbName, parts[1], s3.New(sess), dynamodb.New(sess), defaultMemTableSize)
   141  }
   142  
   143  func validatePath(path string) (string, error) {
   144  	for len(path) > 0 && path[0] == '/' {
   145  		path = path[1:]
   146  	}
   147  
   148  	pathLen := len(path)
   149  	for pathLen > 0 && path[pathLen-1] == '/' {
   150  		path = path[:pathLen-1]
   151  		pathLen--
   152  	}
   153  
   154  	// Should probably have regex validation of a valid database name here once we decide what valid database names look
   155  	// like.
   156  	if len(path) == 0 || strings.Index(path, "/") != -1 {
   157  		return "", errors.New("invalid database name")
   158  	}
   159  
   160  	return path, nil
   161  }
   162  
   163  func awsConfigFromParams(params map[string]string) (session.Options, error) {
   164  	awsConfig := aws.NewConfig()
   165  	if val, ok := params[AWSRegionParam]; ok {
   166  		awsConfig = awsConfig.WithRegion(val)
   167  	}
   168  
   169  	awsCredsSource := RoleCS
   170  	if val, ok := params[AWSCredsTypeParam]; ok {
   171  		awsCredsSource = AWSCredentialSourceFromStr(val)
   172  		if awsCredsSource == InvalidCS {
   173  			return session.Options{}, errors.New("invalid value for aws-creds-source")
   174  		}
   175  	}
   176  
   177  	opts := session.Options{}
   178  
   179  	profile := ""
   180  	if val, ok := params[AWSCredsProfile]; ok {
   181  		profile = val
   182  		opts.Profile = val
   183  	}
   184  
   185  	switch awsCredsSource {
   186  	case EnvCS:
   187  		awsConfig = awsConfig.WithCredentials(credentials.NewEnvCredentials())
   188  	case FileCS:
   189  		if filePath, ok := params[AWSCredsFileParam]; !ok {
   190  			return opts, os.ErrNotExist
   191  		} else {
   192  			creds := credentials.NewSharedCredentials(filePath, profile)
   193  			awsConfig = awsConfig.WithCredentials(creds)
   194  		}
   195  	case AutoCS:
   196  		// start by trying to get the credentials from the environment
   197  		envCreds := credentials.NewEnvCredentials()
   198  		if _, err := envCreds.Get(); err == nil {
   199  			awsConfig = awsConfig.WithCredentials(envCreds)
   200  		} else {
   201  			// if env credentials don't exist try looking for a credentials file
   202  			if filePath, ok := params[AWSCredsFileParam]; ok {
   203  				if _, err := os.Stat(filePath); err == nil {
   204  					creds := credentials.NewSharedCredentials(filePath, profile)
   205  					awsConfig = awsConfig.WithCredentials(creds)
   206  				}
   207  			}
   208  
   209  			// if file and env do not return valid credentials use the default credentials of the box (same as role)
   210  		}
   211  	case RoleCS:
   212  	default:
   213  	}
   214  
   215  	opts.Config.MergeIn(awsConfig)
   216  
   217  	return opts, nil
   218  }