This commit is contained in:
335
internal/config/config.go
Normal file
335
internal/config/config.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/dsa0x/sicher"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/knadh/koanf/providers/structs"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const defaultEnvironment = "development"
|
||||
|
||||
type SicherObject interface {
|
||||
SetEnvStyle(string)
|
||||
LoadEnv(string, interface{}) error
|
||||
}
|
||||
|
||||
type SicherConfig struct {
|
||||
AppSecret string `env:"APP_SECRET" koanf:"app_secret"`
|
||||
AppSecretBlock string `env:"APP_SECRET_BLOCK" koanf:"app_secret_block"`
|
||||
NocoDBToken string `env:"NOCODB_TOKEN" koanf:"nocodb.api_token"`
|
||||
OidcClientId string `env:"OIDC_CLIENT_ID" koanf:"oidc.client_id"`
|
||||
OidcClientSecret string `env:"OIDC_CLIENT_SECRET" koanf:"oidc.client_secret"`
|
||||
ServiceAccountId string `env:"IDMS_SERVICE_ACCOUNT_ID" koanf:"idms.service_account_id"`
|
||||
ServiceAccountPassword string `env:"IDMS_SERVICE_ACCOUNT_PASSWORD" koanf:"idms.service_account_password"`
|
||||
}
|
||||
|
||||
var NewSicherObject = func(environment string, path string) SicherObject {
|
||||
return sicher.New(environment, path)
|
||||
}
|
||||
|
||||
type OidcConfig struct {
|
||||
ClientId string `koanf:"oidc.client_id" validate:"required"`
|
||||
ClientSecret Secret[string] `koanf:"oidc.client_secret" validate:"required"`
|
||||
Issuer string `koanf:"oidc.issuer" validate:"required"`
|
||||
Scopes []string `koanf:"oidc.scopes" validate:"required"`
|
||||
Provider *oidc.Provider
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
OAuth2 *oauth2.Config
|
||||
}
|
||||
|
||||
type IdmsConfig struct {
|
||||
BaseUrl string `koanf:"idms.base_url" validate:"required"`
|
||||
CarsGroupId string `koanf:"idms.cars_group_id" validate:"required"`
|
||||
Id Secret[string] `koanf:"idms.service_account_id" validate:"required"`
|
||||
Password Secret[string] `koanf:"idms.service_account_password" validate:"required"`
|
||||
}
|
||||
|
||||
type NocoDBConfig struct {
|
||||
ApiToken Secret[string] `koanf:"nocodb.api_token" validate:"required"`
|
||||
BaseUrl string `koanf:"nocodb.base_url" validate:"required"`
|
||||
EmployeesTableId string `koanf:"nocodb.employees_table_id" validate:"required"`
|
||||
InfractionsTableId string `koanf:"nocodb.infractions_table_id" validate:"required"`
|
||||
NegativeInfractionId int `koanf:"nocodb.negative_infraction_id" validate:"required"`
|
||||
NoPointsViewId string `koanf:"nocodb.no_points_view_id" validate:"required"`
|
||||
}
|
||||
|
||||
type ApplicationConfig struct {
|
||||
AppSecret Secret[string] `koanf:"app_secret"`
|
||||
AppSecretBlock Secret[string] `koanf:"app_secret_block"`
|
||||
CookieStore *sessions.CookieStore
|
||||
Environment string `koanf:"environment" validate:"required"`
|
||||
Listen string `koanf:"listen" validate:"required,hostname_port"`
|
||||
MatchThreshold int `koanf:"match_threshold" validate:"required"`
|
||||
NocoDBConfig NocoDBConfig `koanf:"nocodb" validate:"required"`
|
||||
OidcConfig OidcConfig `koanf:"oidc" validate:"required"`
|
||||
PublicPath string `koanf:"paths.public"`
|
||||
Idms IdmsConfig `koanf:"idms" validate:"required"`
|
||||
SessionName string
|
||||
ViewPath string `koanf:"paths.views"`
|
||||
}
|
||||
|
||||
type ParsedCliArgs struct {
|
||||
ConfigFiles *[]string
|
||||
DatabasePasswordPath *string
|
||||
Environment *string
|
||||
Listen *string
|
||||
Version *bool
|
||||
}
|
||||
|
||||
func ParseArgs(version Version, exit func(int), args []string) (*ParsedCliArgs, *pflag.FlagSet) {
|
||||
f := pflag.NewFlagSet("config", pflag.ContinueOnError)
|
||||
f.Usage = func() {
|
||||
fmt.Println(version.AppNameAndVersion(false))
|
||||
fmt.Println(f.FlagUsages())
|
||||
exit(0)
|
||||
}
|
||||
|
||||
parsedCliArgs := &ParsedCliArgs{
|
||||
ConfigFiles: f.StringSliceP("conf", "c", []string{}, "path to one or more .yaml config files"),
|
||||
DatabasePasswordPath: f.StringP("db-secret-path", "d", "", "path to database secret"),
|
||||
Environment: f.StringP("environment", "e", defaultEnvironment, "Environment for the running binary"),
|
||||
Listen: f.StringP("listen", "l", "0.0.0.0:3000", "address and port to listen on"),
|
||||
Version: f.Bool("version", false, "Show version"),
|
||||
}
|
||||
|
||||
err := f.Parse(args[1:])
|
||||
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"%s\n\nERROR: %s\n\n%s",
|
||||
version.AppNameAndVersion(false), err, f.FlagUsages(),
|
||||
)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return parsedCliArgs, f
|
||||
}
|
||||
|
||||
// Load application configuration from config files, command line flags, and environment variables.
|
||||
// An error will be returned only if _unexpected_ error happened, such as a file not being found.
|
||||
// If the user specified a --help or a --version flag, the application will exit with a 0 status code.
|
||||
// If the user specified an unsupported flag, the application will exit with a 1 status code and print the help message.
|
||||
func Load(version Version, exit func(int), args []string, logger *zap.Logger) (*ApplicationConfig, error) {
|
||||
k := koanf.New(".")
|
||||
config := defaultConfig()
|
||||
|
||||
err := k.Load(confmap.Provider(config, "."), nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("error initializing config", zap.Error(err))
|
||||
}
|
||||
|
||||
parsedCliArgs, f := ParseArgs(version, exit, args)
|
||||
|
||||
// Load the config files provided in the command line.
|
||||
for _, c := range *parsedCliArgs.ConfigFiles {
|
||||
err := k.Load(file.Provider(c), yaml.Parser())
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading configuration file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = godotenv.Load()
|
||||
if err != nil {
|
||||
logger.Info("No .env file found")
|
||||
}
|
||||
|
||||
configPath := func(p string) string {
|
||||
combinedPath := fmt.Sprintf("%s/%s", k.String("path_prefix"), p)
|
||||
return combinedPath
|
||||
}
|
||||
|
||||
// Override with environment variables
|
||||
lo.ForEach([]string{envPrefix, pgPrefix, databasePrefix, idmsPrefix, oidcPrefix}, func(prefix string, _ int) {
|
||||
if err := k.Load(env.Provider(prefix, "_", mapEnvVarNames(prefix)), nil); err != nil {
|
||||
logger.Fatal("error parsing environment variables", zap.Error(err))
|
||||
}
|
||||
})
|
||||
|
||||
// Override with command-line values
|
||||
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
|
||||
return nil, fmt.Errorf("error parsing command-line parameters: %v", err)
|
||||
}
|
||||
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *parsedCliArgs.Version {
|
||||
fmt.Println(version.AppNameAndVersion(true))
|
||||
exit(0)
|
||||
}
|
||||
|
||||
var sicherConfig SicherConfig
|
||||
|
||||
s := NewSicherObject(k.String("environment"), configPath(k.String("paths.credentials")))
|
||||
s.SetEnvStyle("yaml") // default is dotenv
|
||||
err = s.LoadEnv("", &sicherConfig)
|
||||
|
||||
cookieStore := sessions.NewCookieStore([]byte(k.String("app_secret")))
|
||||
cookieStore.Options.HttpOnly = true
|
||||
cookieStore.Options.Secure = *parsedCliArgs.Environment != "dev" && *parsedCliArgs.Environment != "testEnvironment" // Use HTTPS
|
||||
cookieStore.Options.SameSite = http.SameSiteLaxMode
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if err = k.Load(structs.Provider(sicherConfig, "koanf"), nil); err != nil {
|
||||
return nil, fmt.Errorf("error parsing sicher parameters: %v", err)
|
||||
}
|
||||
|
||||
var appConfig ApplicationConfig
|
||||
|
||||
unmarshalConf := koanf.UnmarshalConf{
|
||||
DecoderConfig: &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
SecretFilePathUnmarshalHookFunc(),
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
),
|
||||
Metadata: nil,
|
||||
Result: &appConfig,
|
||||
WeaklyTypedInput: true,
|
||||
},
|
||||
}
|
||||
|
||||
if err := k.UnmarshalWithConf("", &config, unmarshalConf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appConfig.AppSecret = SecretFromValue(k.String("app_secret"))
|
||||
appConfig.AppSecretBlock = SecretFromValue(k.String("app_secret_block"))
|
||||
|
||||
nocodbConfig := NocoDBConfig{
|
||||
ApiToken: SecretFromValue(k.String("nocodb.api_token")),
|
||||
BaseUrl: k.String("nocodb.base_url"),
|
||||
EmployeesTableId: k.String("nocodb.employees_table_id"),
|
||||
InfractionsTableId: k.String("nocodb.infractions_table_id"),
|
||||
NegativeInfractionId: k.Int("nocodb.negative_infraction_id"),
|
||||
NoPointsViewId: k.String("nocodb.no_points_view_id"),
|
||||
}
|
||||
|
||||
oidcConfig := OidcConfig{
|
||||
ClientId: k.String("oidc.client_id"),
|
||||
ClientSecret: SecretFromValue(k.String("oidc.client_secret")),
|
||||
Issuer: k.String("oidc.issuer"),
|
||||
Scopes: k.Strings("oidc.scopes"),
|
||||
}
|
||||
|
||||
if *parsedCliArgs.Environment != "testEnvironment" {
|
||||
authProvider, err := oidc.NewProvider(context.Background(), k.String("oidc.issuer"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oidcConfig.Provider = authProvider
|
||||
oidcConfig.Verifier = authProvider.Verifier(&oidc.Config{ClientID: k.String("oidc.client_id")})
|
||||
redirectUrl := fmt.Sprintf("%s/auth/callback", k.String("oidc.redirect_url"))
|
||||
oidcConfig.OAuth2 = &oauth2.Config{
|
||||
ClientID: k.String("oidc.client_id"),
|
||||
ClientSecret: k.String("oidc.client_secret"),
|
||||
Endpoint: authProvider.Endpoint(),
|
||||
RedirectURL: redirectUrl,
|
||||
Scopes: k.Strings("oidc.scopes"),
|
||||
}
|
||||
}
|
||||
|
||||
appConfig.CookieStore = cookieStore
|
||||
appConfig.Environment = *parsedCliArgs.Environment
|
||||
appConfig.Idms = IdmsConfig{
|
||||
BaseUrl: k.String("idms.base_url"),
|
||||
CarsGroupId: k.String("idms.cars_group_id"),
|
||||
Id: SecretFromValue(k.String("idms.service_account_id")),
|
||||
Password: SecretFromValue(k.String("idms.service_account_password")),
|
||||
}
|
||||
appConfig.Listen = *parsedCliArgs.Listen
|
||||
appConfig.MatchThreshold = k.Int("match_threshold")
|
||||
appConfig.NocoDBConfig = nocodbConfig
|
||||
appConfig.OidcConfig = oidcConfig
|
||||
appConfig.PublicPath = configPath(k.String("paths.public"))
|
||||
appConfig.SessionName = "calculate-negative-points"
|
||||
appConfig.ViewPath = configPath(k.String("paths.views"))
|
||||
|
||||
validate := validator.New()
|
||||
validate.RegisterCustomTypeFunc(secretTypeTranslator[string], Secret[string]{})
|
||||
err = validate.Struct(appConfig)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
fmt.Print("Could not create config")
|
||||
exit(1)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &appConfig, nil
|
||||
}
|
||||
|
||||
const envPrefix = "APP_"
|
||||
const pgPrefix = "PG_"
|
||||
const databasePrefix = "DATABASE_"
|
||||
const idmsPrefix = "IDMS_"
|
||||
const oidcPrefix = "OIDC_"
|
||||
|
||||
/*
|
||||
* Rio passes in PG_HOST and PG_PORT so we need to replace the PG string with DATABASE and then not trim the prefix
|
||||
*/
|
||||
func mapEnvVarNames(prefix string) func(s string) string {
|
||||
return func(s string) string {
|
||||
if prefix == databasePrefix {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
replacedPg := strings.Replace(s, "PG", "DATABASE", -1)
|
||||
|
||||
lower := strings.ToLower(strings.TrimPrefix(replacedPg, prefix))
|
||||
|
||||
return lower
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig() map[string]interface{} {
|
||||
dbConfig := map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"user": "postgres",
|
||||
"name": "calculate-negative-points",
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"database": dbConfig,
|
||||
"environment": defaultEnvironment,
|
||||
"listen": "127.0.0.1:3000",
|
||||
}
|
||||
}
|
||||
|
||||
func secretTypeTranslator[T SecretValue](field reflect.Value) interface{} {
|
||||
if secret, ok := field.Interface().(Secret[T]); ok {
|
||||
return secret.Value()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
13
internal/config/config_suite_test.go
Normal file
13
internal/config/config_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
41
internal/config/config_test.go
Normal file
41
internal/config/config_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"clintonambulance.com/calculate_negative_points/internal/config"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type FakeSicherObject struct{}
|
||||
|
||||
func (_ FakeSicherObject) SetEnvStyle(_ string) {}
|
||||
func (_ FakeSicherObject) LoadEnv(_ string, obj interface{}) error {
|
||||
var err error
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ = Describe("ConfigTest", func() {
|
||||
var version config.Version
|
||||
var logger, _ = config.NewLogger(version, os.Stdout, []string{"cmd", "-e", "testEnvironment"})
|
||||
shouldNotExit := func(code int) {
|
||||
// "Fatal" to mimic os.Exit
|
||||
log.Default().Print("exit called with code", code)
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
config.NewSicherObject = func(_ string, _ string) config.SicherObject {
|
||||
return FakeSicherObject{}
|
||||
}
|
||||
version = config.Version{Release: "1.0.0", Commit: "abcdef", Date: "2023-01-01"}
|
||||
})
|
||||
|
||||
It("Errors if the config file is not found", func() {
|
||||
argv := []string{"cmd", "-c", "config/no-such-file.yml"}
|
||||
_, err := config.Load(version, shouldNotExit, argv, logger)
|
||||
Expect(err.Error()).To(Equal("error loading configuration file: open config/no-such-file.yml: no such file or directory"))
|
||||
})
|
||||
})
|
||||
34
internal/config/logging.go
Normal file
34
internal/config/logging.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func NewLogger(version Version, output io.Writer, args []string) (*zap.Logger, func()) {
|
||||
parsedCliArgs, _ := ParseArgs(version, os.Exit, args)
|
||||
syncs := []zapcore.WriteSyncer{zapcore.AddSync(output)}
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder
|
||||
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zap.CombineWriteSyncers(syncs...),
|
||||
zap.DebugLevel,
|
||||
)
|
||||
logger := zap.New(core).WithOptions(
|
||||
zap.AddStacktrace(zap.ErrorLevel),
|
||||
zap.WithCaller(false),
|
||||
).With(
|
||||
zap.String("version", version.Release),
|
||||
zap.String("environment", *parsedCliArgs.Environment),
|
||||
)
|
||||
finalizer := func() {
|
||||
// This might fail if logging to console, but we don't care (https://github.com/uber-go/zap/issues/880)
|
||||
_ = logger.Sync()
|
||||
}
|
||||
return logger, finalizer
|
||||
}
|
||||
85
internal/config/secret.go
Normal file
85
internal/config/secret.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// SecretValue defines what types of secrets this secret container supports
|
||||
type SecretValue interface {
|
||||
~string | ~[]string | ~map[string]string
|
||||
}
|
||||
|
||||
// SecretFilePath is the source to the file containing the secret.
|
||||
type SecretFilePath string
|
||||
|
||||
const SecretPlaceholder = "[SECRET]"
|
||||
|
||||
// Secret encapsulates a secret value, which is loaded from a file, or directly from configuration / environment variable
|
||||
type Secret[T SecretValue] struct {
|
||||
value T
|
||||
source SecretFilePath
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface, used to hide the actual secret value in logs etc.
|
||||
func (s Secret[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{"value": SecretPlaceholder, "source": string(s.source)})
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface, used to hide the actual secret value in logs etc.
|
||||
func (s *Secret[T]) MarshalText() ([]byte, error) {
|
||||
return []byte(SecretPlaceholder), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface, used to load the secret from config files and env. values
|
||||
func (s *Secret[T]) UnmarshalText(text []byte) error {
|
||||
var val T
|
||||
switch any(val).(type) {
|
||||
case string:
|
||||
s.value = any(string(text)).(T)
|
||||
return nil
|
||||
default:
|
||||
return json.Unmarshal(text, &s.value)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Secret[T]) Value() T {
|
||||
return s.value
|
||||
}
|
||||
|
||||
func SecretFromSecretPath[T SecretValue](path SecretFilePath) (Secret[T], error) {
|
||||
contentBytes, err := os.ReadFile(string(path))
|
||||
if err != nil {
|
||||
return Secret[T]{}, err
|
||||
}
|
||||
secret := Secret[T]{source: path}
|
||||
err = secret.UnmarshalText(bytes.TrimSpace(contentBytes))
|
||||
return secret, err
|
||||
}
|
||||
|
||||
func SecretFromValue[T SecretValue](value T) Secret[T] {
|
||||
return Secret[T]{value: value}
|
||||
}
|
||||
|
||||
// SecretFilePathUnmarshalHookFunc is a mapstructure.DecodeHookFunc that will convert a SecretFilePath to a Secret
|
||||
func SecretFilePathUnmarshalHookFunc() mapstructure.DecodeHookFuncType {
|
||||
return func(from, to reflect.Type, data interface{}) (interface{}, error) {
|
||||
if from != reflect.TypeOf(SecretFilePath("")) {
|
||||
return data, nil
|
||||
}
|
||||
// Reflection does not work with generics as of 1.20, so we have to do this manually
|
||||
if to == reflect.TypeOf(Secret[map[string]string]{}) {
|
||||
return SecretFromSecretPath[map[string]string](data.(SecretFilePath))
|
||||
} else if to == reflect.TypeOf(Secret[[]string]{}) {
|
||||
return SecretFromSecretPath[[]string](data.(SecretFilePath))
|
||||
} else if to == reflect.TypeOf(Secret[string]{}) {
|
||||
return SecretFromSecretPath[string](data.(SecretFilePath))
|
||||
} else {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
1
internal/config/testdata/DATABASE_SECRET.sample
vendored
Normal file
1
internal/config/testdata/DATABASE_SECRET.sample
vendored
Normal file
@@ -0,0 +1 @@
|
||||
fakesecret
|
||||
22
internal/config/testdata/settings.test.yml
vendored
Normal file
22
internal/config/testdata/settings.test.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
app_secret: "app secret"
|
||||
app_secret_block: "app secret block hash"
|
||||
path_prefix: "."
|
||||
paths:
|
||||
migrations: "db/migrations"
|
||||
credentials: "config/credentials"
|
||||
match_threshold: 3
|
||||
nocodb:
|
||||
api_token: test
|
||||
base_url: https://example.com
|
||||
employees_table_id: "1234567890"
|
||||
infractions_table_id: "2468013579"
|
||||
negative_infraction_id: 1
|
||||
no_points_view_id: "1357924680"
|
||||
oidc:
|
||||
issuer: http://example.com
|
||||
redirect_url: http://example.com
|
||||
idms:
|
||||
base_url: https://example.com
|
||||
cars_group_id: "1234"
|
||||
id: "1234"
|
||||
password: "5678"
|
||||
34
internal/config/version.go
Normal file
34
internal/config/version.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Version struct {
|
||||
Release string
|
||||
Commit string
|
||||
Date string
|
||||
}
|
||||
|
||||
var (
|
||||
release = "dev"
|
||||
commit = ""
|
||||
date = ""
|
||||
)
|
||||
|
||||
func (v *Version) AppNameAndVersion(details bool) string {
|
||||
result := fmt.Sprintf("Basin Feature Flag Service %s", v.Release)
|
||||
if details && v.Commit != "" {
|
||||
result = fmt.Sprintf("%s\nGit commit: %s", result, v.Commit)
|
||||
}
|
||||
if details && v.Date != "" {
|
||||
result = fmt.Sprintf("%s\nBuilt at: %s", result, v.Date)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NewVersion() Version {
|
||||
return Version{
|
||||
Release: release,
|
||||
Commit: commit,
|
||||
Date: date,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user