diff --git a/README.md b/README.md index 0e0f0679..61d17ea4 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ These environment variables are embedded in [deploy/operator.yaml](deploy/operat ## Installation This operator requires a Kubernetes Secret to be created in the same namespace as operator itself. -Secret should contain these keys: POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASS, POSTGRES_URI_ARGS, POSTGRES_CLOUD_PROVIDER, POSTGRES_DEFAULT_DATABASE. +Secret should contain these keys: POSTGRES_HOST, POSTGRES_PORT (optional), POSTGRES_USER, POSTGRES_PASS, POSTGRES_URI_ARGS, POSTGRES_CLOUD_PROVIDER, POSTGRES_DEFAULT_DATABASE. Example: ```yaml @@ -193,12 +193,14 @@ meeting the specific needs of different applications. Available context: -| Variable | Meaning | -|-------------|--------------------------| -| `.Host` | Database host | -| `.Role` | Generated user/role name | -| `.Database` | Referenced database name | -| `.Password` | Generated role password | +| Variable | Meaning | +|---------------|----------------------------| +| `.Host` | Database host and port | +| `.HostNoPort` | Database host without port | +| `.Port` | Database port | +| `.Role` | Generated user/role name | +| `.Database` | Referenced database name | +| `.Password` | Generated role password | ### Contribution diff --git a/go.mod b/go.mod index c0692883..d8e350bc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/movetokube/postgres-operator go 1.18 require ( + github.com/caarlos0/env/v11 v11.0.1 github.com/go-logr/logr v0.1.0 github.com/go-openapi/spec v0.19.4 github.com/golang/mock v1.3.1 diff --git a/go.sum b/go.sum index 4aa89cfc..20453390 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,8 @@ github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqR github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/caarlos0/env/v11 v11.0.1 h1:A8dDt9Ub9ybqRSUF3fQc/TA/gTam2bKT4Pit+cwrsPs= +github.com/caarlos0/env/v11 v11.0.1/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/pkg/config/config.go b/pkg/config/config.go index e3cb922c..6d2da3ea 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,22 +1,25 @@ package config import ( + "log" "net/url" "strconv" + "strings" "sync" - "github.com/movetokube/postgres-operator/pkg/utils" + "github.com/caarlos0/env/v11" ) type cfg struct { - PostgresHost string - PostgresUser string - PostgresPass string - PostgresUriArgs string - PostgresDefaultDb string - CloudProvider string - AnnotationFilter string - KeepSecretName bool + PostgresHost string `env:"POSTGRES_HOST,required"` + PostgresUser string `env:"POSTGRES_USER,required"` + PostgresPass string `env:"POSTGRES_PASS,required"` + PostgresPort uint32 `env:"POSTGRES_PORT" envDefault:"5432"` + PostgresUriArgs string `env:"POSTGRES_URI_ARGS,required"` + PostgresDefaultDb string `env:"POSTGRES_DEFAULT_DATABASE"` + CloudProvider string `env:"POSTGRES_CLOUD_PROVIDER"` + AnnotationFilter string `env:"POSTGRES_INSTANCE"` + KeepSecretName bool `env:"KEEP_SECRET_NAME"` } var doOnce sync.Once @@ -24,16 +27,24 @@ var config *cfg func Get() *cfg { doOnce.Do(func() { - config = &cfg{} - config.PostgresHost = utils.MustGetEnv("POSTGRES_HOST") - config.PostgresUser = url.PathEscape(utils.MustGetEnv("POSTGRES_USER")) - config.PostgresPass = url.PathEscape(utils.MustGetEnv("POSTGRES_PASS")) - config.PostgresUriArgs = utils.MustGetEnv("POSTGRES_URI_ARGS") - config.PostgresDefaultDb = utils.GetEnv("POSTGRES_DEFAULT_DATABASE") - config.CloudProvider = utils.GetEnv("POSTGRES_CLOUD_PROVIDER") - config.AnnotationFilter = utils.GetEnv("POSTGRES_INSTANCE") - if value, err := strconv.ParseBool(utils.GetEnv("KEEP_SECRET_NAME")); err == nil { - config.KeepSecretName = value + config = &cfg{ + PostgresPort: 5432, + } + if err := env.Parse(config); err != nil { + log.Fatal(err) + } + config.PostgresUser = url.PathEscape(config.PostgresUser) + config.PostgresPass = url.PathEscape(config.PostgresPass) + if strings.Contains(config.PostgresHost, ":") { + parts := strings.Split(config.PostgresHost, ":") + if len(parts) > 1 { + port, err := strconv.ParseInt(parts[1], 10, 32) + if err != nil { + log.Fatal(err) + } + config.PostgresPort = uint32(port) + config.PostgresHost = parts[0] + } } }) return config diff --git a/pkg/controller/postgres/postgres_controller.go b/pkg/controller/postgres/postgres_controller.go index 37f0d8a6..5e03c56c 100644 --- a/pkg/controller/postgres/postgres_controller.go +++ b/pkg/controller/postgres/postgres_controller.go @@ -17,9 +17,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" - logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/source" ) @@ -34,7 +34,7 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { c := config.Get() - pg, err := postgres.NewPG(c.PostgresHost, c.PostgresUser, c.PostgresPass, c.PostgresUriArgs, c.PostgresDefaultDb, c.CloudProvider, log.WithName("postgres")) + pg, err := postgres.NewPG(c.PostgresHost, c.PostgresUser, c.PostgresPass, c.PostgresPort, c.PostgresUriArgs, c.PostgresDefaultDb, c.CloudProvider, log.WithName("postgres")) if err != nil { return nil } @@ -220,19 +220,19 @@ func (r *ReconcilePostgres) Reconcile(request reconcile.Request) (_ reconcile.Re } // Set privileges on schema - schemaPrivilegesReader := postgres.PostgresSchemaPrivileges{database, owner, reader, schema, readerPrivs, false} + schemaPrivilegesReader := postgres.PostgresSchemaPrivileges{DB: database, Creator: owner, Role: reader, Schema: schema, Privs: readerPrivs, CreateSchema: false} err = r.pg.SetSchemaPrivileges(schemaPrivilegesReader, reqLogger) if err != nil { reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\"", reader, readerPrivs)) continue } - schemaPrivilegesWriter := postgres.PostgresSchemaPrivileges{database, owner, writer, schema, readerPrivs, true} + schemaPrivilegesWriter := postgres.PostgresSchemaPrivileges{DB: database, Creator: owner, Role: writer, Schema: schema, Privs: readerPrivs, CreateSchema: true} err = r.pg.SetSchemaPrivileges(schemaPrivilegesWriter, reqLogger) if err != nil { reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\"", writer, writerPrivs)) continue } - schemaPrivilegesOwner := postgres.PostgresSchemaPrivileges{database, owner, owner, schema, readerPrivs, true} + schemaPrivilegesOwner := postgres.PostgresSchemaPrivileges{DB: database, Creator: owner, Role: owner, Schema: schema, Privs: readerPrivs, CreateSchema: true} err = r.pg.SetSchemaPrivileges(schemaPrivilegesOwner, reqLogger) if err != nil { reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\"", writer, writerPrivs)) diff --git a/pkg/controller/postgresuser/postgresuser_controller.go b/pkg/controller/postgresuser/postgresuser_controller.go index 85a9437f..dafd4eb7 100644 --- a/pkg/controller/postgresuser/postgresuser_controller.go +++ b/pkg/controller/postgresuser/postgresuser_controller.go @@ -23,9 +23,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" - logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/source" ) @@ -45,7 +45,7 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { c := config.Get() - pg, err := postgres.NewPG(c.PostgresHost, c.PostgresUser, c.PostgresPass, c.PostgresUriArgs, c.PostgresDefaultDb, c.CloudProvider, log.WithName("postgresuser")) + pg, err := postgres.NewPG(c.PostgresHost, c.PostgresUser, c.PostgresPass, c.PostgresPort, c.PostgresUriArgs, c.PostgresDefaultDb, c.CloudProvider, log.WithName("postgresuser")) if err != nil { return nil } @@ -55,6 +55,7 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { scheme: mgr.GetScheme(), pg: pg, pgHost: c.PostgresHost, + pgPort: c.PostgresPort, instanceFilter: c.AnnotationFilter, keepSecretName: c.KeepSecretName, } @@ -100,6 +101,7 @@ type ReconcilePostgresUser struct { scheme *runtime.Scheme pg postgres.PG pgHost string + pgPort uint32 instanceFilter string keepSecretName bool // use secret name as defined in PostgresUserSpec } @@ -280,9 +282,9 @@ func (r *ReconcilePostgresUser) addFinalizer(reqLogger logr.Logger, m *dbv1alpha } func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role, password, login string) (*corev1.Secret, error) { - pgUserUrl := fmt.Sprintf("postgresql://%s:%s@%s/%s", role, password, r.pgHost, cr.Status.DatabaseName) - pgJDBCUrl := fmt.Sprintf("jdbc:postgresql://%s/%s", r.pgHost, cr.Status.DatabaseName) - pgDotnetUrl := fmt.Sprintf("User ID=%s;Password=%s;Host=%s;Port=5432;Database=%s;", role, password, r.pgHost, cr.Status.DatabaseName) + pgUserUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", role, password, r.pgHost, r.pgPort, cr.Status.DatabaseName) + pgJDBCUrl := fmt.Sprintf("jdbc:postgresql://%s:%d/%s", r.pgHost, r.pgPort, cr.Status.DatabaseName) + pgDotnetUrl := fmt.Sprintf("User ID=%s;Password=%s;Host=%s;Port=%d;Database=%s;", role, password, r.pgHost, r.pgPort, cr.Status.DatabaseName) labels := map[string]string{ "app": cr.Name, } @@ -293,10 +295,13 @@ func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role } templateData, err := renderTemplate(cr.Spec.SecretTemplate, templateContext{ - Role: role, - Host: r.pgHost, - Database: cr.Status.DatabaseName, - Password: password, + Role: role, + Host: fmt.Sprintf("%s:%d", r.pgHost, r.pgPort), + HostNoPort: r.pgHost, + Port: r.pgPort, + Login: login, + Database: cr.Status.DatabaseName, + Password: password, }) if err != nil { return nil, fmt.Errorf("render templated keys: %w", err) @@ -306,7 +311,7 @@ func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role "POSTGRES_URL": []byte(pgUserUrl), "POSTGRES_JDBC_URL": []byte(pgJDBCUrl), "POSTGRES_DOTNET_URL": []byte(pgDotnetUrl), - "HOST": []byte(r.pgHost), + "HOST": []byte(fmt.Sprintf("%s:%d", r.pgHost, r.pgPort)), "DATABASE_NAME": []byte(cr.Status.DatabaseName), "ROLE": []byte(role), "PASSWORD": []byte(password), @@ -364,7 +369,7 @@ func (r *ReconcilePostgresUser) getPostgresCR(instance *dbv1alpha1.PostgresUser) return &database, nil } -func (r *ReconcilePostgresUser) addOwnerRef(reqLogger logr.Logger, instance *dbv1alpha1.PostgresUser) error { +func (r *ReconcilePostgresUser) addOwnerRef(_ logr.Logger, instance *dbv1alpha1.PostgresUser) error { // Search postgres database CR pg, err := r.getPostgresCR(instance) if err != nil { @@ -381,10 +386,13 @@ func (r *ReconcilePostgresUser) addOwnerRef(reqLogger logr.Logger, instance *dbv } type templateContext struct { - Host string - Role string - Database string - Password string + Host string + HostNoPort string + Port uint32 + Role string + Login string + Database string + Password string } func renderTemplate(data map[string]string, tc templateContext) (map[string][]byte, error) { diff --git a/pkg/controller/postgresuser/postgresuser_controller_test.go b/pkg/controller/postgresuser/postgresuser_controller_test.go new file mode 100644 index 00000000..9da43a2b --- /dev/null +++ b/pkg/controller/postgresuser/postgresuser_controller_test.go @@ -0,0 +1,50 @@ +package postgresuser + +import ( + "reflect" + "testing" + + dbv1alpha1 "github.com/movetokube/postgres-operator/pkg/apis/db/v1alpha1" +) + +func assertEqual(t *testing.T, a interface{}, b interface{}) { + t.Helper() + + if a == b { + return + } + + // debug.PrintStack() + t.Errorf("Received %v (type %v), expected %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b)) +} + +func TestReconcilePostgresUser_newSecretForCR(t *testing.T) { + rpu := &ReconcilePostgresUser{ + pgHost: "localhost", + pgPort: 5432, + } + + cr := &dbv1alpha1.PostgresUser{ + Status: dbv1alpha1.PostgresUserStatus{ + DatabaseName: "dbname", + }, + Spec: dbv1alpha1.PostgresUserSpec{ + SecretTemplate: map[string]string{ + "all": "host={{.Host}} host_no_port={{.HostNoPort}} port={{.Port}} user={{.Role}} login={{.Login}} password={{.Password}} dbname={{.Database}}", + }, + }, + } + + secret, err := rpu.newSecretForCR(cr, "role", "password", "login") + if err != nil { + t.Fatalf("could not patch object: (%v)", err) + } + + if secret == nil { + t.Fatalf("no secret returned") + } + + // keep old behavior of merging host and port + assertEqual(t, string(secret.Data["HOST"]), "localhost:5432") + assertEqual(t, string(secret.Data["all"]), "host=localhost:5432 host_no_port=localhost port=5432 user=role login=login password=password dbname=dbname") +} diff --git a/pkg/postgres/database.go b/pkg/postgres/database.go index db856e7f..dc57a1a5 100644 --- a/pkg/postgres/database.go +++ b/pkg/postgres/database.go @@ -17,9 +17,9 @@ const ( GRANT_CREATE_TABLE = `GRANT CREATE ON SCHEMA "%s" TO "%s"` GRANT_ALL_TABLES = `GRANT %s ON ALL TABLES IN SCHEMA "%s" TO "%s"` DEFAULT_PRIVS_SCHEMA = `ALTER DEFAULT PRIVILEGES FOR ROLE "%s" IN SCHEMA "%s" GRANT %s ON TABLES TO "%s"` - REVOKE_CONNECT = `REVOKE CONNECT ON DATABASE "%s" FROM public` - TERMINATE_BACKEND = `SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s' AND pid <> pg_backend_pid()` - GET_DB_OWNER = `SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '%s'` + REVOKE_CONNECT = `REVOKE CONNECT ON DATABASE "%s" FROM public` + TERMINATE_BACKEND = `SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s' AND pid <> pg_backend_pid()` + GET_DB_OWNER = `SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '%s'` GRANT_CREATE_SCHEMA = `GRANT CREATE ON DATABASE "%s" TO "%s"` ) @@ -45,7 +45,7 @@ func (c *pg) CreateDB(dbname, role string) error { } func (c *pg) CreateSchema(db, role, schema string, logger logr.Logger) error { - tmpDb, err := GetConnection(c.user, c.pass, c.host, db, c.args, logger) + tmpDb, err := GetConnection(c.user, c.pass, c.host, c.port, db, c.args, logger) if err != nil { return err } @@ -82,7 +82,7 @@ func (c *pg) DropDatabase(database string, logger logr.Logger) error { } func (c *pg) CreateExtension(db, extension string, logger logr.Logger) error { - tmpDb, err := GetConnection(c.user, c.pass, c.host, db, c.args, logger) + tmpDb, err := GetConnection(c.user, c.pass, c.host, c.port, db, c.args, logger) if err != nil { return err } @@ -96,7 +96,7 @@ func (c *pg) CreateExtension(db, extension string, logger logr.Logger) error { } func (c *pg) SetSchemaPrivileges(schemaPrivileges PostgresSchemaPrivileges, logger logr.Logger) error { - tmpDb, err := GetConnection(c.user, c.pass, c.host, schemaPrivileges.DB, c.args, logger) + tmpDb, err := GetConnection(c.user, c.pass, c.host, c.port, schemaPrivileges.DB, c.args, logger) if err != nil { return err } @@ -125,7 +125,7 @@ func (c *pg) SetSchemaPrivileges(schemaPrivileges PostgresSchemaPrivileges, logg _, err = tmpDb.Exec(fmt.Sprintf(GRANT_CREATE_TABLE, schemaPrivileges.Schema, schemaPrivileges.Role)) if err != nil { return err - } + } } return nil diff --git a/pkg/postgres/gcp.go b/pkg/postgres/gcp.go index 9ebb1ec6..41a01f21 100644 --- a/pkg/postgres/gcp.go +++ b/pkg/postgres/gcp.go @@ -55,10 +55,10 @@ func (c *gcppg) CreateDB(dbname, role string) error { } func (c *gcppg) DropRole(role, newOwner, database string, logger logr.Logger) error { - - tmpDb, err := GetConnection(c.user, c.pass, c.host, database, c.args, logger) + + tmpDb, err := GetConnection(c.user, c.pass, c.host, c.port, database, c.args, logger) q := fmt.Sprintf(GET_DB_OWNER, database) - logger.Info("Checking master role: "+ q) + logger.Info("Checking master role: " + q) rows, err := tmpDb.Query(q) if err != nil { return err @@ -68,9 +68,9 @@ func (c *gcppg) DropRole(role, newOwner, database string, logger logr.Logger) er rows.Scan(&masterRole) } - if( role != masterRole){ + if role != masterRole { q = fmt.Sprintf(DROP_ROLE, role) - logger.Info("GCP Drop Role: "+ q) + logger.Info("GCP Drop Role: " + q) _, err = tmpDb.Exec(q) // Check if error exists and if different from "ROLE NOT FOUND" => 42704 if err != nil && err.(*pq.Error).Code != "42704" { diff --git a/pkg/postgres/postgres.go b/pkg/postgres/postgres.go index a5c66b0a..75704d98 100644 --- a/pkg/postgres/postgres.go +++ b/pkg/postgres/postgres.go @@ -28,6 +28,7 @@ type PG interface { type pg struct { db *sql.DB log logr.Logger + port uint32 host string user string pass string @@ -44,8 +45,8 @@ type PostgresSchemaPrivileges struct { CreateSchema bool } -func NewPG(host, user, password, uri_args, default_database, cloud_type string, logger logr.Logger) (PG, error) { - db, err := GetConnection(user, password, host, default_database, uri_args, logger) +func NewPG(host, user, password string, port uint32, uri_args, default_database, cloud_type string, logger logr.Logger) (PG, error) { + db, err := GetConnection(user, password, host, port, default_database, uri_args, logger) if err != nil { log.Fatalf("failed to connect to PostgreSQL server: %s", err.Error()) } @@ -54,6 +55,7 @@ func NewPG(host, user, password, uri_args, default_database, cloud_type string, db: db, log: logger, host: host, + port: port, user: user, pass: password, args: uri_args, @@ -84,8 +86,8 @@ func (c *pg) GetDefaultDatabase() string { return c.default_database } -func GetConnection(user, password, host, database, uri_args string, logger logr.Logger) (*sql.DB, error) { - db, err := sql.Open("postgres", fmt.Sprintf("postgresql://%s:%s@%s/%s?%s", user, password, host, database, uri_args)) +func GetConnection(user, password, host string, port uint32, database, uri_args string, logger logr.Logger) (*sql.DB, error) { + db, err := sql.Open("postgres", fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?%s", user, password, host, port, database, uri_args)) if err != nil { log.Fatal(err) } diff --git a/pkg/postgres/role.go b/pkg/postgres/role.go index 8bf4f4b7..4a3e67c9 100644 --- a/pkg/postgres/role.go +++ b/pkg/postgres/role.go @@ -62,7 +62,7 @@ func (c *pg) RevokeRole(role, revoked string) error { func (c *pg) DropRole(role, newOwner, database string, logger logr.Logger) error { // REASSIGN OWNED BY only works if the correct database is selected - tmpDb, err := GetConnection(c.user, c.pass, c.host, database, c.args, logger) + tmpDb, err := GetConnection(c.user, c.pass, c.host, c.port, database, c.args, logger) if err != nil { if err.(*pq.Error).Code == "3D000" { return nil // Database is does not exist (anymore) diff --git a/vendor/github.com/caarlos0/env/v11/.gitignore b/vendor/github.com/caarlos0/env/v11/.gitignore new file mode 100644 index 00000000..ca6a0ff8 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.gitignore @@ -0,0 +1,4 @@ +coverage.txt +bin +card.png +dist diff --git a/vendor/github.com/caarlos0/env/v11/.golangci.yml b/vendor/github.com/caarlos0/env/v11/.golangci.yml new file mode 100644 index 00000000..ff791f86 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.golangci.yml @@ -0,0 +1,8 @@ +linters: + enable: + - thelper + - gofumpt + - tparallel + - unconvert + - unparam + - wastedassign diff --git a/vendor/github.com/caarlos0/env/v11/.goreleaser.yml b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml new file mode 100644 index 00000000..4688983c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml @@ -0,0 +1,3 @@ +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff --git a/vendor/github.com/caarlos0/env/v11/.mailmap b/vendor/github.com/caarlos0/env/v11/.mailmap new file mode 100644 index 00000000..eeeee601 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.mailmap @@ -0,0 +1,7 @@ +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Becker +dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> +actions-user github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> diff --git a/vendor/github.com/caarlos0/env/v11/LICENSE.md b/vendor/github.com/caarlos0/env/v11/LICENSE.md new file mode 100644 index 00000000..3a59b6b3 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2022 Carlos Alexandro Becker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/caarlos0/env/v11/Makefile b/vendor/github.com/caarlos0/env/v11/Makefile new file mode 100644 index 00000000..da8595fb --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/Makefile @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + gofumpt -w -l . +.PHONY: fmt + +lint: + golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff --git a/vendor/github.com/caarlos0/env/v11/README.md b/vendor/github.com/caarlos0/env/v11/README.md new file mode 100644 index 00000000..9a693a9d --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/README.md @@ -0,0 +1,632 @@ +# env + +[![Build Status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?branch=main&style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) +[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) +[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v11) + +A simple and zero-dependencies library to parse environment variables into +`struct`s. + +## Used and supported by + +

+ + encore icon + +
+
+ Encore – the platform for building Go-based cloud backends. +
+

+ +## Example + +Get the module with: + +```sh +go get github.com/caarlos0/env/v11 +``` + +The usage looks like this: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/caarlos0/env/v11" +) + +type config struct { + Home string `env:"HOME"` + Port int `env:"PORT" envDefault:"3000"` + Password string `env:"PASSWORD,unset"` + IsProduction bool `env:"PRODUCTION"` + Duration time.Duration `env:"DURATION"` + Hosts []string `env:"HOSTS" envSeparator:":"` + TempFolder string `env:"TEMP_FOLDER,expand" envDefault:"${HOME}/tmp"` + StringInts map[string]int `env:"MAP_STRING_INT"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + // or you can use generics + cfg, err := env.ParseAs[config]() + if err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +You can run it like this: + +```sh +$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s MAP_STRING_INT=k1:1,k2:2 go run main.go +{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s StringInts:map[k1:1 k2:2]} +``` + +## Caveats + +> [!CAUTION] +> +> _Unexported fields_ will be **ignored** by `env`. +> This is by design and will not change. + +## Supported types and defaults + +Out of the box all built-in types are supported, plus a few others that +are commonly used. + +Complete list: + +- `string` +- `bool` +- `int` +- `int8` +- `int16` +- `int32` +- `int64` +- `uint` +- `uint8` +- `uint16` +- `uint32` +- `uint64` +- `float32` +- `float64` +- `time.Duration` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers, and maps of those types are also +supported. + +You can also use/define a [custom parser func](#custom-parser-funcs) for any +other type you want. + +You can also use custom keys and values in your maps, as long as you provide a +parser function for them. + +If you set the `envDefault` tag for something, this value will be used in the +case of absence of it in the environment. + +By default, slice types will split the environment value on `,`; you can change +this behavior by setting the `envSeparator` tag. For map types, the default +separator between key and value is `:` and `,` for key-value pairs. +The behavior can be changed by setting the `envKeyValSeparator` and +`envSeparator` tags accordingly. + +## Custom Parser Funcs + +If you have a type that is not supported out of the box by the lib, you are able +to use (or define) and pass custom parsers (and their associated `reflect.Type`) +to the `env.ParseWithOptions()` function. + +In addition to accepting a struct pointer (same as `Parse()`), this function +also accepts a `Options{}`, and you can set your custom parsers in the `FuncMap` +field. + +If you add a custom parser for, say `Foo`, it will also be used to parse +`*Foo` and `[]Foo` types. + +Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v11) +for more info. + +### A note about `TextUnmarshaler` and `time.Time` + +Env supports by default anything that implements the `TextUnmarshaler` interface. +That includes things like `time.Time` for example. +The upside is that depending on the format you need, you don't need to change +anything. +The downside is that if you do need time in another format, you'll need to +create your own type. + +Its fairly straightforward: + +```go +type MyTime time.Time + +func (t *MyTime) UnmarshalText(text []byte) error { + tt, err := time.Parse("2006-01-02", string(text)) + *t = MyTime(tt) + return err +} + +type Config struct { + SomeTime MyTime `env:"SOME_TIME"` +} +``` + +And then you can parse `Config` with `env.Parse`. + +## Required fields + +The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to +ensure that some environment variable is set. In the example above, an error is +returned if the `config` struct is changed to: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,required"` +} +``` + +> [!NOTE] +> +> Note that being set is not the same as being empty. +> If the variable is set, but empty, the field will have its type's default +> value. +> This also means that custom parser funcs will not be invoked. + +## Expand vars + +If you set the `expand` option, environment variables (either in `${var}` or +`$var` format) in the string will be replaced according with the actual value +of the variable. For example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,expand"` +} +``` + +This also works with `envDefault`: + +```go +import ( + "fmt" + "github.com/caarlos0/env/v11" +) + +type config struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT" envDefault:"3000"` + Address string `env:"ADDRESS,expand" envDefault:"$HOST:${PORT}"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + fmt.Printf("%+v\n", cfg) +} +``` + +results in this: + +```sh +$ PORT=8080 go run main.go +{Host:localhost Port:8080 Address:localhost:8080} +``` + +## Not Empty fields + +While `required` demands the environment variable to be set, it doesn't check +its value. If you want to make sure the environment is set and not empty, you +need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,notEmpty"` +} +``` + +## Unset environment variable after reading it + +The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added +to ensure that some environment variable is unset after reading it. + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,unset"` +} +``` + +## From file + +The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added +in order to indicate that the value of the variable shall be loaded from a +file. +The path of that file is given by the environment variable associated with it: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/caarlos0/env/v11" +) + +type config struct { + Secret string `env:"SECRET,file"` + Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` + Certificate string `env:"CERTIFICATE,file,expand" envDefault:"${CERTIFICATE_FILE}"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +```sh +$ echo qwerty > /tmp/secret +$ echo dvorak > /tmp/password +$ echo coleman > /tmp/certificate + +$ SECRET=/tmp/secret \ + CERTIFICATE_FILE=/tmp/certificate \ + go run main.go +{Secret:qwerty Password:dvorak Certificate:coleman} +``` + +## Options + +### Use field names as environment variables by default + +If you don't want to set the `env` tag on every field, you can use the +`UseFieldNameByDefault` option. + +It will use the field name to define the environment variable name. +So, `Foo` becomes `FOO`, `FooBar` becomes `FOO_BAR`, and so on. + +Here's an example: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Username string // will use $USERNAME + Password string // will use $PASSWORD + UserFullName string // will use $USER_FULL_NAME +} + +func main() { + cfg := &Config{} + opts := env.Options{UseFieldNameByDefault: true} + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +### Environment + +By setting the `Options.Environment` map you can tell `Parse` to add those +`keys` and `values` as `env` vars before parsing is done. +These `envs` are stored in the map and never actually set by `os.Setenv`. +This option effectively makes `env` ignore the OS environment variables: only +the ones provided in the option are used. + +This can make your testing scenarios a bit more clean and easy to handle. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := env.Options{Environment: map[string]string{ + "PASSWORD": "MY_PASSWORD", + }} + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +### Changing default tag name + +You can change what tag name to use for setting the env vars by setting the +`Options.TagName` variable. + +For example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Password string `json:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := env.Options{TagName: "json"} + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +### Prefixes + +You can prefix sub-structs env tags, as well as a whole `env.Parse` call. + +Here's an example flexing it a bit: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Home string `env:"HOME"` +} + +type ComplexConfig struct { + Foo Config `envPrefix:"FOO_"` + Clean Config + Bar Config `envPrefix:"BAR_"` + Blah string `env:"BLAH"` +} + +func main() { + cfg := &ComplexConfig{} + opts := env.Options{ + Prefix: "T_", + Environment: map[string]string{ + "T_FOO_HOME": "/foo", + "T_BAR_HOME": "/bar", + "T_BLAH": "blahhh", + "T_HOME": "/clean", + }, + } + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +### On set hooks + +You might want to listen to value sets and, for example, log something or do +some other kind of logic. +You can do this by passing a `OnSet` option: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := env.Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) + }, + } + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +## Making all fields to required + +You can make all fields that don't have a default value be required by setting +the `RequiredIfNoDef: true` in the `Options`. + +For example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := env.Options{RequiredIfNoDef: true} + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + +## Defaults from code + +You may define default value also in code, by initialising the config data +before it's filled by `env.Parse`. +Default values defined as struct tags will overwrite existing values during +Parse. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := Config{ + Username: "test", + Password: "123456", + } + + if err := env.Parse(&cfg); err != nil { + fmt.Println("failed:", err) + } + + fmt.Printf("%+v", cfg) // {Username:admin Password:123456} +} +``` + +## Error handling + +You can handle the errors the library throws like so: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + var cfg Config + err := env.Parse(&cfg) + if e, ok := err.(*env.AggregateError); ok { + for _, er := range e.Errors { + switch v := er.(type) { + case env.ParseError: + // handle it + case env.NotStructPtrError: + // handle it + case env.NoParserError: + // handle it + case env.NoSupportedTagOptionError: + // handle it + default: + fmt.Printf("Unknown error type %v", v) + } + } + } + + fmt.Printf("%+v", cfg) // {Username:admin Password:123456} +} +``` + +> **Info** +> +> If you want to check if an specific error is in the chain, you can also use +> `errors.Is()`. + +## Related projects + +- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/vendor/github.com/caarlos0/env/v11/env.go b/vendor/github.com/caarlos0/env/v11/env.go new file mode 100644 index 00000000..d4bdf556 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env.go @@ -0,0 +1,659 @@ +package env + +import ( + "encoding" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" + "unicode" +) + +// nolint: gochecknoglobals +var ( + defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, + } +) + +func defaultTypeParsers() map[reflect.Type]ParserFunc { + return map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, newParseValueError("unable to parse URL", err) + } + return *u, nil + }, + reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { + s, err := time.ParseDuration(v) + if err != nil { + return nil, newParseValueError("unable to parse duration", err) + } + return s, err + }, + } +} + +// ParserFunc defines the signature of a function that can be used within `CustomParsers`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// processFieldFn is a function which takes all information about a field and processes it. +type processFieldFn func(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tagname to use rather than the default env. + TagName string + + // RequiredIfNoDef automatically sets all env as required if they do not + // declare 'envDefault'. + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set. + OnSet OnSetFn + + // Prefix define a prefix for each key. + Prefix string + + // UseFieldNameByDefault defines whether or not env should use the field + // name by default if the `env` key is missing. + // Note that the field name will be "converted" to conform with environment + // variable names conventions. + UseFieldNameByDefault bool + + // Custom parse functions for different types. + FuncMap map[reflect.Type]ParserFunc + + // Used internally. maps the env variable key to its resolved string value. (for env var expansion) + rawEnvVars map[string]string +} + +func (opts *Options) getRawEnv(s string) string { + val := opts.rawEnvVars[s] + if val == "" { + return opts.Environment[s] + } + return val +} + +func defaultOptions() Options { + return Options{ + TagName: "env", + Environment: toMap(os.Environ()), + FuncMap: defaultTypeParsers(), + rawEnvVars: make(map[string]string), + } +} + +func customOptions(opt Options) Options { + defOpts := defaultOptions() + if opt.TagName == "" { + opt.TagName = defOpts.TagName + } + if opt.Environment == nil { + opt.Environment = defOpts.Environment + } + if opt.FuncMap == nil { + opt.FuncMap = map[reflect.Type]ParserFunc{} + } + if opt.rawEnvVars == nil { + opt.rawEnvVars = defOpts.rawEnvVars + } + for k, v := range defOpts.FuncMap { + if _, exists := opt.FuncMap[k]; !exists { + opt.FuncMap[k] = v + } + } + return opt +} + +func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: opts.Prefix + field.Tag.Get("envPrefix"), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}) error { + return parseInternal(v, setField, defaultOptions()) +} + +// ParseWithOptions parses a struct containing `env` tags and loads its values from +// environment variables. +func ParseWithOptions(v interface{}, opts Options) error { + return parseInternal(v, setField, customOptions(opts)) +} + +// ParseAs parses the given struct type containing `env` tags and loads its +// values from environment variables. +func ParseAs[T any]() (T, error) { + var t T + return t, Parse(&t) +} + +// ParseWithOptions parses the given struct type containing `env` tags and +// loads its values from environment variables. +func ParseAsWithOptions[T any](opts Options) (T, error) { + var t T + return t, ParseWithOptions(&t, opts) +} + +// Must panic is if err is not nil, and returns t otherwise. +func Must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +// GetFieldParams parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParams(v interface{}) ([]FieldParams, error) { + return GetFieldParamsWithOptions(v, defaultOptions()) +} + +// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) { + var result []FieldParams + err := parseInternal( + v, + func(_ reflect.Value, _ reflect.StructField, _ Options, fieldParams FieldParams) error { + if fieldParams.OwnKey != "" { + result = append(result, fieldParams) + } + return nil + }, + customOptions(opts), + ) + if err != nil { + return nil, err + } + + return result, nil +} + +func parseInternal(v interface{}, processField processFieldFn, opts Options) error { + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return newAggregateError(NotStructPtrError{}) + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return newAggregateError(NotStructPtrError{}) + } + + return doParse(ref, processField, opts) +} + +func doParse(ref reflect.Value, processField processFieldFn, opts Options) error { + refType := ref.Type() + + var agrErr AggregateError + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + refTypeField := refType.Field(i) + + if err := doParseField(refField, refTypeField, processField, opts); err != nil { + if val, ok := err.(AggregateError); ok { + agrErr.Errors = append(agrErr.Errors, val.Errors...) + } else { + agrErr.Errors = append(agrErr.Errors, err) + } + } + } + + if len(agrErr.Errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField(refField reflect.Value, refTypeField reflect.StructField, processField processFieldFn, opts Options) error { + if !refField.CanSet() { + return nil + } + if reflect.Ptr == refField.Kind() && !refField.IsNil() { + return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { + return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + params, err := parseFieldParams(refTypeField, opts) + if err != nil { + return err + } + + if err := processField(refField, refTypeField, opts, params); err != nil { + return err + } + + if isStructPtr(refField) && refField.IsNil() { + refField.Set(reflect.New(refField.Type().Elem())) + refField = refField.Elem() + } + + if _, ok := opts.FuncMap[refField.Type()]; ok { + return nil + } + + if reflect.Struct == refField.Kind() { + return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + value, err := get(fieldParams, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, opts.FuncMap) + } + + return nil +} + +const underscore rune = '_' + +func toEnvName(input string) string { + var output []rune + for i, c := range input { + if c == underscore { + continue + } + if len(output) > 0 && unicode.IsUpper(c) { + if len(input) > i+1 { + peek := rune(input[i+1]) + if unicode.IsLower(peek) || unicode.IsLower(rune(input[i-1])) { + output = append(output, underscore) + } + } + } + output = append(output, unicode.ToUpper(c)) + } + return string(output) +} + +// FieldParams contains information about parsed field tags. +type FieldParams struct { + OwnKey string + Key string + DefaultValue string + HasDefaultValue bool + Required bool + LoadFile bool + Unset bool + NotEmpty bool + Expand bool +} + +func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { + ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName)) + if ownKey == "" && opts.UseFieldNameByDefault { + ownKey = toEnvName(field.Name) + } + + defaultValue, hasDefaultValue := field.Tag.Lookup("envDefault") + + result := FieldParams{ + OwnKey: ownKey, + Key: opts.Prefix + ownKey, + Required: opts.RequiredIfNoDef, + DefaultValue: defaultValue, + HasDefaultValue: hasDefaultValue, + } + + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + result.LoadFile = true + case "required": + result.Required = true + case "unset": + result.Unset = true + case "notEmpty": + result.NotEmpty = true + case "expand": + result.Expand = true + default: + return FieldParams{}, newNoSupportedTagOptionError(tag) + } + } + + return result, nil +} + +func get(fieldParams FieldParams, opts Options) (val string, err error) { + var exists, isDefault bool + + val, exists, isDefault = getOr(fieldParams.Key, fieldParams.DefaultValue, fieldParams.HasDefaultValue, opts.Environment) + + if fieldParams.Expand { + val = os.Expand(val, opts.getRawEnv) + } + + opts.rawEnvVars[fieldParams.OwnKey] = val + + if fieldParams.Unset { + defer os.Unsetenv(fieldParams.Key) + } + + if fieldParams.Required && !exists && len(fieldParams.OwnKey) > 0 { + return "", newEnvVarIsNotSet(fieldParams.Key) + } + + if fieldParams.NotEmpty && val == "" { + return "", newEmptyEnvVarError(fieldParams.Key) + } + + if fieldParams.LoadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", newLoadFileContentError(filename, fieldParams.Key, err) + } + } + + if opts.OnSet != nil { + if fieldParams.OwnKey != "" { + opts.OnSet(fieldParams.Key, val, isDefault) + } + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists bool, isDefault bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case exists && value == "" && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + switch field.Kind() { + case reflect.Slice: + return handleSlice(field, value, sf, funcMap) + case reflect.Map: + return handleMap(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + keyType := sf.Type.Key() + keyParserFunc, ok := funcMap[keyType] + if !ok { + keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + elemType := sf.Type.Elem() + elemParserFunc, ok := funcMap[elemType] + if !ok { + elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + + keyValSeparator := sf.Tag.Get("envKeyValSeparator") + if keyValSeparator == "" { + keyValSeparator = ":" + } + + result := reflect.MakeMap(sf.Type) + for _, part := range strings.Split(value, separator) { + pairs := strings.Split(part, keyValSeparator) + if len(pairs) != 2 { + return newParseError(sf, fmt.Errorf(`%q should be in "key%svalue" format`, part, keyValSeparator)) + } + + key, err := keyParserFunc(pairs[0]) + if err != nil { + return newParseError(sf, err) + } + + elem, err := elemParserFunc(pairs[1]) + if err != nil { + return newParseError(sf, err) + } + + result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType)) + } + + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if reflect.Ptr == field.Kind() { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +// ToMap Converts list of env vars as provided by os.Environ() to map you +// can use as Options.Environment field +func ToMap(env []string) map[string]string { + return toMap(env) +} + +func isStructPtr(v reflect.Value) bool { + return reflect.Ptr == v.Kind() && v.Type().Elem().Kind() == reflect.Struct +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap.go b/vendor/github.com/caarlos0/env/v11/env_tomap.go new file mode 100644 index 00000000..aece2ae9 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap.go @@ -0,0 +1,16 @@ +//go:build !windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go new file mode 100644 index 00000000..04ce66f5 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/error.go b/vendor/github.com/caarlos0/env/v11/error.go new file mode 100644 index 00000000..156ca3ec --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/error.go @@ -0,0 +1,164 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// An aggregated error wrapper to combine gathered errors. This allows either to display all errors or convert them individually +// List of the available errors +// ParseError +// NotStructPtrError +// NoParserError +// NoSupportedTagOptionError +// EnvVarIsNotSetError +// EmptyEnvVarError +// LoadFileContentError +// ParseValueError +type AggregateError struct { + Errors []error +} + +func newAggregateError(initErr error) error { + return AggregateError{ + []error{ + initErr, + }, + } +} + +func (e AggregateError) Error() string { + var sb strings.Builder + + sb.WriteString("env:") + + for _, err := range e.Errors { + sb.WriteString(fmt.Sprintf(" %v;", err.Error())) + } + + return strings.TrimRight(sb.String(), ";") +} + +// Is conforms with errors.Is. +func (e AggregateError) Is(err error) bool { + for _, ie := range e.Errors { + if reflect.TypeOf(ie) == reflect.TypeOf(err) { + return true + } + } + return false +} + +// The error occurs when it's impossible to convert the value for given type. +type ParseError struct { + Name string + Type reflect.Type + Err error +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{sf.Name, sf.Type, err} +} + +func (e ParseError) Error() string { + return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.Name, e.Type, e.Err) +} + +// The error occurs when pass something that is not a pointer to a Struct to Parse +type NotStructPtrError struct{} + +func (e NotStructPtrError) Error() string { + return "expected a pointer to a Struct" +} + +// This error occurs when there is no parser provided for given type +// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults +// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs +type NoParserError struct { + Name string + Type reflect.Type +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{sf.Name, sf.Type} +} + +func (e NoParserError) Error() string { + return fmt.Sprintf(`no parser found for field "%s" of type "%s"`, e.Name, e.Type) +} + +// This error occurs when the given tag is not supported +// In-built supported tags: "", "file", "required", "unset", "notEmpty", "expand", "envDefault", "envSeparator" +// How to create a custom tag: https://github.com/caarlos0/env#changing-default-tag-name +type NoSupportedTagOptionError struct { + Tag string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{tag} +} + +func (e NoSupportedTagOptionError) Error() string { + return fmt.Sprintf("tag option %q not supported", e.Tag) +} + +// This error occurs when the required variable is not set +// Read about required fields: https://github.com/caarlos0/env#required-fields +type EnvVarIsNotSetError struct { + Key string +} + +func newEnvVarIsNotSet(key string) error { + return EnvVarIsNotSetError{key} +} + +func (e EnvVarIsNotSetError) Error() string { + return fmt.Sprintf(`required environment variable %q is not set`, e.Key) +} + +// This error occurs when the variable which must be not empty is existing but has an empty value +// Read about not empty fields: https://github.com/caarlos0/env#not-empty-fields +type EmptyEnvVarError struct { + Key string +} + +func newEmptyEnvVarError(key string) error { + return EmptyEnvVarError{key} +} + +func (e EmptyEnvVarError) Error() string { + return fmt.Sprintf("environment variable %q should not be empty", e.Key) +} + +// This error occurs when it's impossible to load the value from the file +// Read about From file feature: https://github.com/caarlos0/env#from-file +type LoadFileContentError struct { + Filename string + Key string + Err error +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{filename, key, err} +} + +func (e LoadFileContentError) Error() string { + return fmt.Sprintf(`could not load content of file "%s" from variable %s: %v`, e.Filename, e.Key, e.Err) +} + +// This error occurs when it's impossible to convert value using given parser +// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults +// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs +type ParseValueError struct { + Msg string + Err error +} + +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} +} + +func (e ParseValueError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Err) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1cdc39b6..d2e50108 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -26,6 +26,9 @@ github.com/PuerkitoBio/urlesc # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile +# github.com/caarlos0/env/v11 v11.0.1 +## explicit; go 1.18 +github.com/caarlos0/env/v11 # github.com/cespare/xxhash/v2 v2.1.1 ## explicit; go 1.11 github.com/cespare/xxhash/v2