Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

290 lines
8.8 KiB
Go

// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dbfactory
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/dolthub/dolt/go/libraries/doltcore/memlimit"
"github.com/dolthub/dolt/go/libraries/utils/awsrefreshcreds"
"github.com/dolthub/dolt/go/store/chunks"
"github.com/dolthub/dolt/go/store/datas"
"github.com/dolthub/dolt/go/store/nbs"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/dolt/go/store/types"
)
const (
// AWSRegionParam is a creation parameter that can be used to set the AWS region
AWSRegionParam = "aws-region"
// AWSCredsTypeParam is a creation parameter that can be used to set the type of credentials that should be used.
// valid values are role, env, auto, and file
AWSCredsTypeParam = "aws-creds-type"
// AWSCredsFileParam is a creation parameter that can be used to specify a credential file to use.
AWSCredsFileParam = "aws-creds-file"
//AWSCredsProfile is a creation parameter that can be used to specify which AWS profile to use.
AWSCredsProfile = "aws-creds-profile"
)
var AWSFileCredsRefreshDuration = time.Minute
var AWSCredTypes = []string{RoleCS.String(), EnvCS.String(), FileCS.String()}
// AWSCredentialSource is an enum type representing the different credential sources (auto, role, env, file, or invalid)
type AWSCredentialSource int
const (
InvalidCS AWSCredentialSource = iota - 1
// Auto will try env first and fall back to role (This is the default)
AutoCS
// Role Uses the AWS IAM role of the instance for auth
RoleCS
// Env uses the credentials stored in the environment variables AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY
EnvCS
// Uses credentials stored in a file
FileCS
)
// String returns the string representation of the of an AWSCredentialSource
func (ct AWSCredentialSource) String() string {
switch ct {
case RoleCS:
return "role"
case EnvCS:
return "env"
case AutoCS:
return "auto"
case FileCS:
return "file"
default:
return "invalid"
}
}
// AWSCredentialSourceFromStr converts a string to an AWSCredentialSource
func AWSCredentialSourceFromStr(str string) AWSCredentialSource {
strlwr := strings.TrimSpace(strings.ToLower(str))
switch strlwr {
case "", "auto":
return AutoCS
case "role":
return RoleCS
case "env":
return EnvCS
case "file":
return FileCS
default:
return InvalidCS
}
}
// AWSFactory is a DBFactory implementation for creating AWS backed databases
type AWSFactory struct {
}
func (fact AWSFactory) PrepareDB(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) error {
// nothing to prepare
return nil
}
// CreateDB creates an AWS backed database
func (fact AWSFactory) CreateDB(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) (datas.Database, types.ValueReadWriter, tree.NodeStore, error) {
var db datas.Database
cs, err := fact.newChunkStore(ctx, nbf, urlObj, params)
if err != nil {
return nil, nil, nil, err
}
vrw := types.NewValueStore(cs)
ns := tree.NewNodeStore(cs)
db = datas.NewTypesDatabase(vrw, ns)
return db, vrw, ns, nil
}
func (fact AWSFactory) newChunkStore(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) (chunks.ChunkStore, error) {
parts := strings.SplitN(urlObj.Hostname(), ":", 2) // [table]:[bucket]
if len(parts) != 2 {
return nil, errors.New("aws url has an invalid format")
}
cfg, err := awsConfigFromParams(ctx, params)
if err != nil {
return nil, err
}
dbName, err := validatePath(urlObj.Path)
if err != nil {
return nil, err
}
// Sanity check that we have credentials...
_, err = cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, err
}
q := nbs.NewUnlimitedMemQuotaProvider()
// DisableLogOutputChecksumValidationSkipped silences the per-GetObject WARN emitted when objects have no stored checksum.
s3c := s3.NewFromConfig(cfg, func(o *s3.Options) { o.DisableLogOutputChecksumValidationSkipped = true })
return nbs.NewAWSStore(ctx, nbf.VersionString(), parts[0], dbName, parts[1], s3c, dynamodb.NewFromConfig(cfg), memlimit.MemtableSize(), q)
}
func validatePath(path string) (string, error) {
for len(path) > 0 && path[0] == '/' {
path = path[1:]
}
pathLen := len(path)
for pathLen > 0 && path[pathLen-1] == '/' {
path = path[:pathLen-1]
pathLen--
}
if len(path) == 0 {
return "", errors.New("invalid database name")
}
return path, nil
}
func awsConfigFromParams(ctx context.Context, params map[string]interface{}) (aws.Config, error) {
var opts []func(*config.LoadOptions) error
// aws-region always sets the region. Otherwise it comes from AWS_REGION or AWS_DEFAULT_REGION.
if val, ok := params[AWSRegionParam]; ok {
opts = append(opts, config.WithRegion(val.(string)))
}
awsCredsSource := RoleCS
if val, ok := params[AWSCredsTypeParam]; ok {
awsCredsSource = AWSCredentialSourceFromStr(val.(string))
if awsCredsSource == InvalidCS {
return aws.Config{}, errors.New("invalid value for aws-creds-source")
}
}
profile := ""
if val, ok := params[AWSCredsProfile]; ok {
profile = val.(string)
opts = append(opts, config.WithSharedConfigProfile(val.(string)))
}
filePath, ok := params[AWSCredsFileParam]
if ok && len(filePath.(string)) != 0 && awsCredsSource == RoleCS {
awsCredsSource = FileCS
}
switch awsCredsSource {
case EnvCS:
// Credentials can only come directly from AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY...
creds := awsrefreshcreds.LoadEnvCredentials()
opts = append(opts, config.WithCredentialsProvider(aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
if !creds.HasKeys() {
return aws.Credentials{}, errors.New("error loading env creds; did not find AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variable.")
} else {
return creds, nil
}
})))
case FileCS:
if filePath, ok := params[AWSCredsFileParam]; !ok {
return aws.Config{}, os.ErrNotExist
} else {
provider := awsrefreshcreds.LoadINICredentialsProvider(filePath.(string), profile)
provider = awsrefreshcreds.NewRefreshingCredentialsProvider(provider, AWSFileCredsRefreshDuration)
opts = append(opts, config.WithCredentialsProvider(provider))
}
case AutoCS:
if envCreds := awsrefreshcreds.LoadEnvCredentials(); envCreds.HasKeys() {
opts = append(opts, config.WithCredentialsProvider(aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
return envCreds, nil
})))
} else {
// if env credentials don't exist try looking for a credentials file
if filePath, ok := params[AWSCredsFileParam]; ok {
if _, err := os.Stat(filePath.(string)); err == nil {
provider := awsrefreshcreds.LoadINICredentialsProvider(filePath.(string), profile)
opts = append(opts, config.WithCredentialsProvider(provider))
}
}
}
// if file and env do not return valid credentials use the default credentials of the box (same as role)
case RoleCS:
default:
}
cfg, err := config.LoadDefaultConfig(ctx, opts...)
var profileErr config.SharedConfigProfileNotExistError
if errors.As(err, &profileErr) {
// XXX: Dolt was originally using aws-sdk-go, which was
// happy to load the specified shared profile from
// places like AWS_CONFIG_FILE or $HOME/.aws/config,
// but did not complain if it could not find it.
//
// We preserve that behavior here with this gross
// hack. We write a shared config file with an empty
// profile, and we point to that config file when
// loading the config.
if profile == "" {
profile = os.Getenv("AWS_PROFILE")
}
if profile == "" {
profile = os.Getenv("AWS_DEFAULT_PROFILE")
}
path, ferr := makeTempEmptyProfileConfig(profile)
if path != "" {
defer os.Remove(path)
}
if ferr == nil {
opts = append(opts, config.WithSharedConfigFiles([]string{path}))
cfg, err = config.LoadDefaultConfig(ctx, opts...)
}
}
return cfg, err
}
func makeTempEmptyProfileConfig(profile string) (string, error) {
f, err := os.CreateTemp("", "dolt_aws_empty_profile-*")
if err != nil {
return "", err
}
_, err = fmt.Fprintf(f, "[profile %s]\n", profile)
if err != nil {
return f.Name(), errors.Join(err, f.Close())
}
return f.Name(), f.Close()
}