github.com/snowflakedb/gosnowflake@v1.9.0/client_configuration.go (about)

     1  // Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
     2  
     3  package gosnowflake
     4  
     5  import (
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  )
    15  
    16  // log levels for easy logging
    17  const (
    18  	levelOff   string = "OFF"   // log level for logging switched off
    19  	levelError string = "ERROR" // error log level
    20  	levelWarn  string = "WARN"  // warn log level
    21  	levelInfo  string = "INFO"  // info log level
    22  	levelDebug string = "DEBUG" // debug log level
    23  	levelTrace string = "TRACE" // trace log level
    24  )
    25  
    26  const (
    27  	defaultConfigName = "sf_client_config.json"
    28  	clientConfEnvName = "SF_CLIENT_CONFIG_FILE"
    29  )
    30  
    31  func getClientConfig(filePathFromConnectionString string) (*ClientConfig, string, error) {
    32  	configPredefinedDirPaths := clientConfigPredefinedDirs()
    33  	filePath, err := findClientConfigFilePath(filePathFromConnectionString, configPredefinedDirPaths)
    34  	if err != nil {
    35  		return nil, "", err
    36  	}
    37  	if filePath == "" { // we did not find a config file
    38  		return nil, "", nil
    39  	}
    40  	config, err := parseClientConfiguration(filePath)
    41  	return config, filePath, err
    42  }
    43  
    44  func findClientConfigFilePath(filePathFromConnectionString string, configPredefinedDirs []string) (string, error) {
    45  	if filePathFromConnectionString != "" {
    46  		logger.Infof("Using client configuration path from a connection string: %s", filePathFromConnectionString)
    47  		return filePathFromConnectionString, nil
    48  	}
    49  	envConfigFilePath := os.Getenv(clientConfEnvName)
    50  	if envConfigFilePath != "" {
    51  		logger.Infof("Using client configuration path from an environment variable: %s", envConfigFilePath)
    52  		return envConfigFilePath, nil
    53  	}
    54  	return searchForConfigFile(configPredefinedDirs)
    55  }
    56  
    57  func searchForConfigFile(directories []string) (string, error) {
    58  	for _, dir := range directories {
    59  		filePath := path.Join(dir, defaultConfigName)
    60  		exists, err := existsFile(filePath)
    61  		if err != nil {
    62  			return "", fmt.Errorf("error while searching for client config in directory: %s, err: %s", dir, err)
    63  		}
    64  		if exists {
    65  			logger.Infof("Using client configuration from a default directory: %s", filePath)
    66  			return filePath, nil
    67  		}
    68  		logger.Debugf("No client config found in directory: %s", dir)
    69  	}
    70  	logger.Info("No client config file found in default directories")
    71  	return "", nil
    72  }
    73  
    74  func existsFile(filePath string) (bool, error) {
    75  	_, err := os.Stat(filePath)
    76  	if err == nil {
    77  		return true, nil
    78  	}
    79  	if errors.Is(err, os.ErrNotExist) {
    80  		return false, nil
    81  	}
    82  	return false, err
    83  }
    84  
    85  func clientConfigPredefinedDirs() []string {
    86  	var predefinedDirs []string
    87  	exeFile, err := os.Executable()
    88  	if err != nil {
    89  		logger.Warnf("Unable to access the application directory for client configuration search, err: %v", err)
    90  	} else {
    91  		predefinedDirs = append(predefinedDirs, filepath.Dir(exeFile))
    92  	}
    93  	homeDir, err := os.UserHomeDir()
    94  	if err != nil {
    95  		logger.Warnf("Unable to access Home directory for client configuration search, err: %v", err)
    96  	} else {
    97  		predefinedDirs = append(predefinedDirs, homeDir)
    98  	}
    99  	if predefinedDirs == nil {
   100  		return []string{}
   101  	}
   102  	return predefinedDirs
   103  }
   104  
   105  // ClientConfig config root
   106  type ClientConfig struct {
   107  	Common *ClientConfigCommonProps `json:"common"`
   108  }
   109  
   110  // ClientConfigCommonProps properties from "common" section
   111  type ClientConfigCommonProps struct {
   112  	LogLevel string `json:"log_level,omitempty"`
   113  	LogPath  string `json:"log_path,omitempty"`
   114  }
   115  
   116  func parseClientConfiguration(filePath string) (*ClientConfig, error) {
   117  	if filePath == "" {
   118  		return nil, nil
   119  	}
   120  	fileContents, err := os.ReadFile(filePath)
   121  	if err != nil {
   122  		return nil, parsingClientConfigError(err)
   123  	}
   124  	err = validateCfgPerm(filePath)
   125  	if err != nil {
   126  		return nil, parsingClientConfigError(err)
   127  	}
   128  	var clientConfig ClientConfig
   129  	err = json.Unmarshal(fileContents, &clientConfig)
   130  	if err != nil {
   131  		return nil, parsingClientConfigError(err)
   132  	}
   133  	unknownValues := getUnknownValues(fileContents)
   134  	if len(unknownValues) > 0 {
   135  		for val := range unknownValues {
   136  			logger.Warnf("Unknown configuration entry: %s with value: %s", val, unknownValues[val])
   137  		}
   138  	}
   139  	err = validateClientConfiguration(&clientConfig)
   140  	if err != nil {
   141  		return nil, parsingClientConfigError(err)
   142  	}
   143  	return &clientConfig, nil
   144  }
   145  
   146  func getUnknownValues(fileContents []byte) map[string]interface{} {
   147  	var values map[string]interface{}
   148  	err := json.Unmarshal(fileContents, &values)
   149  	if err != nil {
   150  		return nil
   151  	}
   152  	if values["common"] == nil {
   153  		return nil
   154  	}
   155  	commonValues := values["common"].(map[string]interface{})
   156  	lowercaseCommonValues := make(map[string]interface{}, len(commonValues))
   157  	for k, v := range commonValues {
   158  		lowercaseCommonValues[strings.ToLower(k)] = v
   159  	}
   160  	delete(lowercaseCommonValues, "log_level")
   161  	delete(lowercaseCommonValues, "log_path")
   162  	return lowercaseCommonValues
   163  }
   164  
   165  func parsingClientConfigError(err error) error {
   166  	return fmt.Errorf("parsing client config failed: %w", err)
   167  }
   168  
   169  func validateClientConfiguration(clientConfig *ClientConfig) error {
   170  	if clientConfig == nil {
   171  		return errors.New("client config not found")
   172  	}
   173  	if clientConfig.Common == nil {
   174  		return errors.New("common section in client config not found")
   175  	}
   176  	return validateLogLevel(*clientConfig)
   177  }
   178  
   179  func validateLogLevel(clientConfig ClientConfig) error {
   180  	var logLevel = clientConfig.Common.LogLevel
   181  	if logLevel != "" {
   182  		_, err := toLogLevel(logLevel)
   183  		if err != nil {
   184  			return err
   185  		}
   186  	}
   187  	return nil
   188  }
   189  
   190  func validateCfgPerm(filePath string) error {
   191  	if runtime.GOOS == "windows" {
   192  		return nil
   193  	}
   194  	stat, err := os.Stat(filePath)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	perm := stat.Mode()
   199  	// Check if group (5th LSB) or others (2nd LSB) have a write permission to the file
   200  	if perm&(1<<4) != 0 || perm&(1<<1) != 0 {
   201  		return fmt.Errorf("configuration file: %s can be modified by group or others", filePath)
   202  	}
   203  	return nil
   204  }
   205  
   206  func toLogLevel(logLevelString string) (string, error) {
   207  	var logLevel = strings.ToUpper(logLevelString)
   208  	switch logLevel {
   209  	case levelOff, levelError, levelWarn, levelInfo, levelDebug, levelTrace:
   210  		return logLevel, nil
   211  	default:
   212  		return "", errors.New("unknown log level: " + logLevelString)
   213  	}
   214  }