diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3268943 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Encryption key is used to encrypt and decrypt the sensitive data stored +# in the database such as database credentials, secret keys, etc. +PBW_ENCRYPTION_KEY="" + +# Database connection string for a PostgreSQL database where the pgbackweb +# will store its data. +PBW_POSTGRES_CONN_STRING="" diff --git a/internal/config/env.go b/internal/config/env.go new file mode 100644 index 0000000..b5d6212 --- /dev/null +++ b/internal/config/env.go @@ -0,0 +1,34 @@ +package config + +import ( + "github.com/joho/godotenv" +) + +type Env struct { + PBW_ENCRYPTION_KEY *string + PBW_POSTGRES_CONN_STRING *string +} + +// GetEnv returns the environment variables. +// +// If there is an error, it will log it and exit the program. +func GetEnv() *Env { + err := godotenv.Load() + if err == nil { + logInfo("using .env file") + } + + env := &Env{ + PBW_ENCRYPTION_KEY: getEnvAsString(getEnvAsStringParams{ + name: "PBW_ENCRYPTION_KEY", + isRequired: true, + }), + PBW_POSTGRES_CONN_STRING: getEnvAsString(getEnvAsStringParams{ + name: "PBW_POSTGRES_CONN_STRING", + isRequired: true, + }), + } + + validateEnv(env) + return env +} diff --git a/internal/config/env_validate.go b/internal/config/env_validate.go new file mode 100644 index 0000000..20241cb --- /dev/null +++ b/internal/config/env_validate.go @@ -0,0 +1,4 @@ +package config + +// validateEnv runs additional validations on the environment variables. +func validateEnv(env *Env) {} diff --git a/internal/config/helpers.go b/internal/config/helpers.go new file mode 100644 index 0000000..6baf622 --- /dev/null +++ b/internal/config/helpers.go @@ -0,0 +1,154 @@ +package config + +import ( + "errors" + "os" + "strconv" +) + +type getEnvAsStringParams struct { + name string + defaultValue *string + isRequired bool +} + +// defaultValue returns a pointer to the given value. +func newDefaultValue[T any](value T) *T { + return &value +} + +// getEnvAsString returns the value of the environment variable with the given name. +func getEnvAsString(params getEnvAsStringParams) *string { //nolint:all + value, err := getEnvAsStringFunc(params) + + if err != nil { + logFatalError( + "error getting env variable", + "name", params.name, + "error", err, + ) + } + + return value +} + +// getEnvAsStringFunc is the outlying function for getEnvAsString. +func getEnvAsStringFunc(params getEnvAsStringParams) (*string, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + value, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + return nil, nil + } + + return &value, nil +} + +type getEnvAsIntParams struct { + name string + defaultValue *int + isRequired bool +} + +// getEnvAsInt returns the value of the environment variable with the given name. +func getEnvAsInt(params getEnvAsIntParams) *int { //nolint:all + value, err := getEnvAsIntFunc(params) + + if err != nil { + logFatalError( + "error getting env variable", + "name", params.name, + "error", err, + ) + } + + return value +} + +// getEnvAsIntFunc is the outlying function for getEnvAsInt. +func getEnvAsIntFunc(params getEnvAsIntParams) (*int, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + valueStr, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + return nil, nil + } + + value, err := strconv.Atoi(valueStr) + + if err != nil { + return nil, errors.New("env variable is not an integer") + } + + return &value, nil +} + +type getEnvAsBoolParams struct { + name string + defaultValue *bool + isRequired bool +} + +// getEnvAsBool returns the value of the environment variable with the given name. +func getEnvAsBool(params getEnvAsBoolParams) *bool { //nolint:all + value, err := getEnvAsBoolFunc(params) + + if err != nil { + logFatalError( + "error getting env variable", + "name", params.name, + "error", err, + ) + } + + return value +} + +// getEnvAsBoolFunc is the outlying function for getEnvAsBool. +func getEnvAsBoolFunc(params getEnvAsBoolParams) (*bool, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + valueStr, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + f := false + return &f, nil + } + + value, err := strconv.ParseBool(valueStr) + + if err != nil { + return nil, errors.New("env variable is not a boolean, must be true or false") + } + + return &value, nil +} diff --git a/internal/config/helpers_test.go b/internal/config/helpers_test.go new file mode 100644 index 0000000..67b1b09 --- /dev/null +++ b/internal/config/helpers_test.go @@ -0,0 +1,190 @@ +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEnvAsStringFunc(t *testing.T) { + // Test when environment variable exists + os.Setenv("TEST_ENV", "test_value") + value, err := getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, default value is provided, and is not required + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue("default_value"), + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, "default_value", *value) + + // Test when environment variable does not exist, no default value is provided, and is required + // This should return an error + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + + // Test when environment variable exists, default value is provided, and is required + os.Setenv("TEST_ENV", "test_value") + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + defaultValue: newDefaultValue("default_value"), + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "test_value") + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Nil(t, value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue("default_value"), + isRequired: true, + }) + assert.Error(t, err) +} + +func TestGetEnvAsIntFunc(t *testing.T) { + // Test when environment variable exists and is an integer + os.Setenv("TEST_ENV", "123") + value, err := getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, 123, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, default value is provided, and is not required + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(456), + }) + assert.NoError(t, err) + assert.Equal(t, 456, *value) + + // Test when environment variable does not exist, no default value is provided, and is required + // This should return an error + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + + // Test when environment variable exists, is not an integer, no default value is provided, and is required + // This should return an error + os.Setenv("TEST_ENV", "not_an_integer") + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "123") + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, 123, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Nil(t, value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(1), + isRequired: true, + }) + assert.Error(t, err) +} + +func TestGetEnvAsBoolFunc(t *testing.T) { + // Test when environment variable exists and is a boolean + os.Setenv("TEST_ENV", "true") + value, err := getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, true, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not a boolean, and is required + os.Setenv("TEST_ENV", "not_a_boolean") + _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.Error(t, err) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "true") + value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, true, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, false, *value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(true), + isRequired: true, + }) + assert.Error(t, err) +} diff --git a/internal/config/logger.go b/internal/config/logger.go new file mode 100644 index 0000000..96f02f7 --- /dev/null +++ b/internal/config/logger.go @@ -0,0 +1,15 @@ +package config + +import ( + "log/slog" + "os" +) + +func logFatalError(msg string, args ...any) { + slog.Error(msg, args...) + os.Exit(1) +} + +func logInfo(msg string, args ...any) { + slog.Info(msg, args...) +}