Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ SITE_ADDR=0.0.0.0:3000
# Logging
LOG_LEVEL=INFO
LOG_PATH=

# Runtime configuration
#
# Use `answer config export-env -C /data` after installation to produce the
# complete canonical variable set. The two database variables below must be
# non-empty to start without config.yaml. Keep the exported file secret because
# the connection value can contain database credentials.
#
# ANSWER_DATA_DATABASE_DRIVER=
# ANSWER_DATA_DATABASE_CONNECTION=
#
# See docs/runtime-configuration.md for every ANSWER_* runtime variable.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:2.0.2

For more information, see [Installation](https://answer.apache.org/docs/installation).

Answer can also start from a complete set of runtime environment variables
without a local `config.yaml`. See [runtime configuration from environment
variables](docs/runtime-configuration.md) for the supported variables,
precedence rules, and safe export workflow.

### Plugins

Answer provides a plugin system for developers to create custom plugins and expand Answer’s features. You can find the [plugin documentation here](https://answer.apache.org/community/plugins).
Expand Down
8 changes: 7 additions & 1 deletion charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ An open-source knowledge-based community software. You can use it quickly to bui
- Kubernetes 1.20+
## Configuration

Answer can run without a persisted `config.yaml` when the complete runtime
configuration is supplied through environment variables. See [runtime
configuration from environment variables](../docs/runtime-configuration.md).
Uploads still require persistent or external storage if they must survive pod
replacement.

The following table lists the configurable parameters of the answer chart and their default values.

| Parameter | Description | Default |
Expand Down Expand Up @@ -72,4 +78,4 @@ Publish the chart to Artifacthub and add proper installation instructions. E.G.
$ helm repo add apache https://charts.answer.apache.org/
$ helm repo update
$ helm install apache/answer -n mynamespace
```
```
5 changes: 3 additions & 2 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ extraContainers: []
# - containerPort: 5432

# Persistence for the /data volume
# Without persistence, your uploads and config.yaml will not be remembered between restarts.
# Without persistence, uploads will not survive restarts. config.yaml can be
# replaced by the documented ANSWER_* runtime environment variables.
persistence:
enabled: true
# If set to "-", storageClassName: "", which disables dynamic provisioning
Expand Down Expand Up @@ -167,4 +168,4 @@ nodeSelector: {}

tolerations: []

affinity: {}
affinity: {}
37 changes: 37 additions & 0 deletions cmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func init() {
upgradeCmd.Flags().StringVarP(&upgradeVersion, "from", "f", "", "upgrade from specific version, eg: -f v1.1.0")

configCmd.Flags().StringSliceVarP(&configFields, "with", "w", []string{}, "the fields that need to be set to the default value, eg: -w allow_password_login")
configCmd.AddCommand(configExportEnvCmd)

i18nCmd.Flags().StringVarP(&i18nSourcePath, "source", "s", "", "i18n source path, eg: -s ./i18n/source")

Expand Down Expand Up @@ -136,6 +137,19 @@ To run answer, use:
}
}

if conf.RuntimeEnvironmentConfigured() {
fmt.Println("runtime configuration found in environment, try to connect database...")
c, err := conf.ReadConfig(path.GetConfigFilePath())
if err != nil {
fmt.Println("read environment config failed: ", err.Error())
return
}
if cli.CheckDBTableExist(c.Data.Database) {
fmt.Println("connect to database successfully and table already exists, do nothing.")
return
}
}

// start installation server to install
install.Run(path.GetConfigFilePath())
},
Expand Down Expand Up @@ -286,6 +300,29 @@ To run answer, use:
},
}

configExportEnvCmd = &cobra.Command{
Use: "export-env",
Short: "Export runtime configuration in dotenv format",
SilenceUsage: true,
Long: `Export the effective runtime configuration in dotenv format.
The output can contain database credentials and is only written to standard output
when this command is explicitly invoked.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
path.FormatAllPath(dataDirPath)
c, err := conf.ReadConfig(path.GetConfigFilePath())
if err != nil {
return fmt.Errorf("read config failed: %w", err)
}
output, err := conf.ExportEnvironment(c)
if err != nil {
return fmt.Errorf("export config failed: %w", err)
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), output)
return err
},
}

i18nCmd = &cobra.Command{
Use: "i18n",
Short: "Overwrite i18n files",
Expand Down
88 changes: 88 additions & 0 deletions docs/runtime-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Runtime configuration from environment variables

Answer normally reads its runtime configuration from `/data/conf/config.yaml`.
It can also run without that file when both
`ANSWER_DATA_DATABASE_DRIVER` and `ANSWER_DATA_DATABASE_CONNECTION` are set to
non-empty values. This explicit requirement prevents an accidentally missing
configuration file from silently starting Answer with the embedded SQLite
defaults.

When `config.yaml` exists, canonical `ANSWER_*` environment variables override
the corresponding file values. `SITE_ADDR`, `SWAGGER_HOST`, and
`SWAGGER_ADDRESS_PORT` remain supported for backward compatibility, but their
canonical replacements take precedence when both names are set.

When `config.yaml` does not exist and the two required database variables are
present, Answer starts with the embedded configuration template and applies all
set environment variables over it. Values omitted from the environment retain
the embedded defaults shown below.

| Environment variable | `config.yaml` field | Embedded default |
| --- | --- | --- |
| `ANSWER_DEBUG` | `debug` | `false` |
| `ANSWER_SERVER_HTTP_ADDR` | `server.http.addr` | `0.0.0.0:80` |
| `ANSWER_DATA_DATABASE_DRIVER` | `data.database.driver` | `sqlite3` |
| `ANSWER_DATA_DATABASE_CONNECTION` | `data.database.connection` | `/data/sqlite3/answer.db` |
| `ANSWER_DATA_DATABASE_CONN_MAX_LIFE_TIME` | `data.database.conn_max_life_time` | `0` |
| `ANSWER_DATA_DATABASE_MAX_OPEN_CONN` | `data.database.max_open_conn` | `0` |
| `ANSWER_DATA_DATABASE_MAX_IDLE_CONN` | `data.database.max_idle_conn` | `0` |
| `ANSWER_DATA_CACHE_FILE_PATH` | `data.cache.file_path` | `/data/cache/cache.db` |
| `ANSWER_I18N_BUNDLE_DIR` | `i18n.bundle_dir` | `/data/i18n` |
| `ANSWER_SERVICE_CONFIG_UPLOAD_PATH` | `service_config.upload_path` | `/data/uploads` |
| `ANSWER_SERVICE_CONFIG_CLEAN_UP_UPLOADS` | `service_config.clean_up_uploads` | `true` |
| `ANSWER_SERVICE_CONFIG_CLEAN_ORPHAN_UPLOADS_PERIOD_HOURS` | `service_config.clean_orphan_uploads_period_hours` | `48` |
| `ANSWER_SERVICE_CONFIG_PURGE_DELETED_FILES_PERIOD_DAYS` | `service_config.purge_deleted_files_period_days` | `30` |
| `ANSWER_SWAGGERUI_SHOW` | `swaggerui.show` | `true` |
| `ANSWER_SWAGGERUI_PROTOCOL` | `swaggerui.protocol` | `http` |
| `ANSWER_SWAGGERUI_HOST` | `swaggerui.host` | `127.0.0.1` |
| `ANSWER_SWAGGERUI_ADDRESS` | `swaggerui.address` | `:80` |
| `ANSWER_UI_BASE_URL` | `ui.base_url` | empty |
| `ANSWER_UI_API_BASE_URL` | `ui.api_base_url` | empty |

Boolean values use Go boolean syntax such as `true` or `false`. Integer values
must be base-10 integers. An invalid value stops configuration loading and names
the invalid variable.

## Exporting an installed configuration

After installation, export the effective runtime configuration in dotenv format:

```bash
umask 077
answer config export-env -C /data > answer-runtime.env
```

The command writes only the dotenv document to standard output, so it can also
be piped to a secret-management command. The database connection can contain a
password. Answer never emits the exported document during ordinary `init`,
`upgrade`, or `run` operations; it is produced only by this explicit command.

The exported file contains the complete canonical variable set. Store it as a
secret, not as a ConfigMap or a source-controlled file.

To start another instance against the already initialized database, inject the
exported values and run Answer normally. `AUTO_INSTALL` is not needed and should
not be set for this runtime-only path.

Configuration statelessness does not make uploaded files persistent. Configure
the appropriate upload storage plugin or retain storage for the upload path if
uploads must survive pod replacement.
75 changes: 43 additions & 32 deletions internal/base/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ package conf

import (
"bytes"
"errors"
"os"
"path/filepath"

"github.com/apache/answer/configs"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/path"
"github.com/apache/answer/internal/base/server"
Expand All @@ -46,20 +48,6 @@ type AllConfig struct {
UI *server.UI `json:"ui" mapstructure:"ui" yaml:"ui"`
}

type envConfigOverrides struct {
SwaggerHost string
SwaggerAddressPort string
SiteAddr string
}

func loadEnvs() (envOverrides *envConfigOverrides) {
return &envConfigOverrides{
SwaggerHost: os.Getenv("SWAGGER_HOST"),
SwaggerAddressPort: os.Getenv("SWAGGER_ADDRESS_PORT"),
SiteAddr: os.Getenv("SITE_ADDR"),
}
}

type PathIgnore struct {
Users []string `yaml:"users"`
}
Expand All @@ -77,21 +65,32 @@ type Data struct {

// SetDefault set default config
func (c *AllConfig) SetDefault() {
if c.UI == nil {
c.UI = &server.UI{}
if c.Server == nil {
c.Server = &Server{}
}
}

func (c *AllConfig) SetEnvironmentOverrides() {
envs := loadEnvs()
if envs.SiteAddr != "" {
c.Server.HTTP.Addr = envs.SiteAddr
if c.Server.HTTP == nil {
c.Server.HTTP = &server.HTTP{}
}
if c.Data == nil {
c.Data = &Data{}
}
if envs.SwaggerHost != "" {
c.Swaggerui.Host = envs.SwaggerHost
if c.Data.Database == nil {
c.Data.Database = &data.Database{}
}
if envs.SwaggerAddressPort != "" {
c.Swaggerui.Address = envs.SwaggerAddressPort
if c.Data.Cache == nil {
c.Data.Cache = &data.CacheConf{}
}
if c.I18n == nil {
c.I18n = &translator.I18n{}
}
if c.ServiceConfig == nil {
c.ServiceConfig = &service_config.ServiceConfig{}
}
if c.Swaggerui == nil {
c.Swaggerui = &router.SwaggerConfig{}
}
if c.UI == nil {
c.UI = &server.UI{}
}
}

Expand All @@ -101,15 +100,27 @@ func ReadConfig(configFilePath string) (c *AllConfig, err error) {
configFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName)
}
c = &AllConfig{}
config, err := viper.NewWithPath(configFilePath)
if err != nil {
return nil, err
_, statErr := os.Stat(configFilePath)
switch {
case statErr == nil:
config, err := viper.NewWithPath(configFilePath)
if err != nil {
return nil, err
}
if err = config.Parse(&c); err != nil {
return nil, err
}
case errors.Is(statErr, os.ErrNotExist) && RuntimeEnvironmentConfigured():
if err = yaml.Unmarshal(configs.Config, c); err != nil {
return nil, err
}
default:
return nil, statErr
}
if err = config.Parse(&c); err != nil {
c.SetDefault()
if err = c.SetEnvironmentOverrides(); err != nil {
return nil, err
}
c.SetDefault()
c.SetEnvironmentOverrides()
return c, nil
}

Expand Down
Loading