github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/dbfactory/oss.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  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"net/url"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/aliyun/aliyun-oss-go-sdk/oss"
    27  
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
    29  	"github.com/dolthub/dolt/go/store/blobstore"
    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/prolly/tree"
    34  	"github.com/dolthub/dolt/go/store/types"
    35  )
    36  
    37  const (
    38  
    39  	// OSSCredsFileParam is a creation parameter that can be used to specify a credential file to use.
    40  	OSSCredsFileParam = "oss-creds-file"
    41  
    42  	// OSSCredsProfile is a creation parameter that can be used to specify which OSS profile to use.
    43  	OSSCredsProfile = "oss-creds-profile"
    44  )
    45  
    46  var (
    47  	emptyOSSCredential = ossCredential{}
    48  )
    49  
    50  type ossParams map[string]interface{}
    51  type ossCredentials map[string]ossCredential
    52  
    53  type ossCredential struct {
    54  	Endpoint        string `json:"endpoint,omitempty"`
    55  	AccessKeyID     string `json:"accessKeyID,omitempty"`
    56  	AccessKeySecret string `json:"accessKeySecret,omitempty"`
    57  }
    58  
    59  // OSSFactory is a DBFactory implementation for creating OSS backed databases
    60  type OSSFactory struct {
    61  }
    62  
    63  // PrepareDB prepares an OSS backed database
    64  func (fact OSSFactory) PrepareDB(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) error {
    65  	// nothing to prepare
    66  	return nil
    67  }
    68  
    69  // CreateDB creates an OSS backed database
    70  func (fact OSSFactory) CreateDB(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) (datas.Database, types.ValueReadWriter, tree.NodeStore, error) {
    71  	ossStore, err := fact.newChunkStore(ctx, nbf, urlObj, params)
    72  	if err != nil {
    73  		return nil, nil, nil, err
    74  	}
    75  
    76  	vrw := types.NewValueStore(ossStore)
    77  	ns := tree.NewNodeStore(ossStore)
    78  	db := datas.NewTypesDatabase(vrw, ns)
    79  
    80  	return db, vrw, ns, nil
    81  }
    82  
    83  func (fact OSSFactory) newChunkStore(ctx context.Context, nbf *types.NomsBinFormat, urlObj *url.URL, params map[string]interface{}) (chunks.ChunkStore, error) {
    84  	// oss://[bucket]/[key]
    85  	bucket := urlObj.Hostname()
    86  	prefix := urlObj.Path
    87  
    88  	opts := ossConfigFromParams(params)
    89  
    90  	ossClient, err := getOSSClient(opts)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("failed to initialize oss err: %s", err)
    93  	}
    94  	bs, err := blobstore.NewOSSBlobstore(ossClient, bucket, prefix)
    95  	if err != nil {
    96  		return nil, errors.New("failed to initialize oss blob store")
    97  	}
    98  
    99  	q := nbs.NewUnlimitedMemQuotaProvider()
   100  	return nbs.NewBSStore(ctx, nbf.VersionString(), bs, defaultMemTableSize, q)
   101  }
   102  
   103  func ossConfigFromParams(params map[string]interface{}) ossCredential {
   104  	// then we look for config from oss-creds-file
   105  	p := ossParams(params)
   106  	credFile, err := p.getCredFile()
   107  	if err != nil {
   108  		return emptyOSSCredential
   109  	}
   110  	creds, err := readOSSCredentialsFromFile(credFile)
   111  	if err != nil {
   112  		return emptyOSSCredential
   113  	}
   114  	// if there is only 1 cred in the file, just use this cred regardless the profile is
   115  	if len(creds) == 1 {
   116  		return creds.First()
   117  	}
   118  	// otherwise, we try to get cred by profile from cred file
   119  	if res, ok := creds[p.getCredProfile()]; ok {
   120  		return res
   121  	}
   122  	return emptyOSSCredential
   123  }
   124  
   125  func getOSSClient(opts ossCredential) (*oss.Client, error) {
   126  	var (
   127  		endpoint, accessKeyID, accessKeySecret string
   128  		err                                    error
   129  	)
   130  	if endpoint, err = opts.getEndPoint(); err != nil {
   131  		return nil, err
   132  	}
   133  	if accessKeyID, err = opts.getAccessKeyID(); err != nil {
   134  		return nil, err
   135  	}
   136  	if accessKeySecret, err = opts.getAccessKeySecret(); err != nil {
   137  		return nil, err
   138  	}
   139  	return oss.New(
   140  		endpoint,
   141  		accessKeyID,
   142  		accessKeySecret,
   143  	)
   144  }
   145  
   146  func (opt ossCredential) getEndPoint() (string, error) {
   147  	if opt.Endpoint != "" {
   148  		return opt.Endpoint, nil
   149  	}
   150  	if v := os.Getenv(dconfig.EnvOssEndpoint); v != "" {
   151  		return v, nil
   152  	}
   153  	return "", fmt.Errorf("failed to find endpoint from cred file or env %s", dconfig.EnvOssEndpoint)
   154  }
   155  
   156  func (opt ossCredential) getAccessKeyID() (string, error) {
   157  	if opt.AccessKeyID != "" {
   158  		return opt.AccessKeyID, nil
   159  	}
   160  	if v := os.Getenv(dconfig.EnvOssAccessKeyID); v != "" {
   161  		return v, nil
   162  	}
   163  	return "", fmt.Errorf("failed to find accessKeyID from cred file or env %s", dconfig.EnvOssAccessKeyID)
   164  }
   165  
   166  func (opt ossCredential) getAccessKeySecret() (string, error) {
   167  	if opt.AccessKeySecret != "" {
   168  		return opt.AccessKeySecret, nil
   169  	}
   170  	if v := os.Getenv(dconfig.EnvOssAccessKeySecret); v != "" {
   171  		return v, nil
   172  	}
   173  	return "", fmt.Errorf("failed to find accessKeySecret from cred file or env %s", dconfig.EnvOssAccessKeySecret)
   174  }
   175  
   176  func readOSSCredentialsFromFile(credFile string) (ossCredentials, error) {
   177  	data, err := os.ReadFile(credFile)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("failed to read oss cred file %s, err: %s", credFile, err)
   180  	}
   181  	var res map[string]ossCredential
   182  	if err = json.Unmarshal(data, &res); err != nil {
   183  		return nil, fmt.Errorf("invalid oss credential file %s, err: %s", credFile, err)
   184  	}
   185  	if len(res) == 0 {
   186  		return nil, errors.New("empty credential file is not allowed")
   187  	}
   188  	return res, nil
   189  }
   190  
   191  func (oc ossCredentials) First() ossCredential {
   192  	var res ossCredential
   193  	for _, c := range oc {
   194  		res = c
   195  		break
   196  	}
   197  	return res
   198  }
   199  
   200  func (p ossParams) getCredFile() (string, error) {
   201  	// then we look for config from oss-creds-file
   202  	credFile, ok := p[OSSCredsFileParam]
   203  	if !ok {
   204  		// if oss-creds-files is
   205  		homeDir, err := os.UserHomeDir()
   206  		if err != nil {
   207  			return "", fmt.Errorf("failed to find oss cred file from home dir, err: %s", err)
   208  		}
   209  		credFile = filepath.Join(homeDir, ".oss", "dolt_oss_credentials")
   210  	}
   211  	return credFile.(string), nil
   212  }
   213  
   214  func (p ossParams) getCredProfile() string {
   215  	credProfile, ok := p[OSSCredsProfile]
   216  	if !ok {
   217  		credProfile = "default"
   218  	}
   219  	return credProfile.(string)
   220  }