Skip to content

Commit

Permalink
Merge pull request #327 from KyleMaas/toml-table
Browse files Browse the repository at this point in the history
Allow for configuration of both go-sbot and sbotcli
  • Loading branch information
decentral1se authored Feb 12, 2023
2 parents 7702e23 + 53c4e99 commit 2cdd828
Show file tree
Hide file tree
Showing 9 changed files with 525 additions and 175 deletions.
180 changes: 23 additions & 157 deletions cmd/go-sbot/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,112 +5,16 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"
loglib "log"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/komkom/toml"
"github.com/ssbc/go-ssb/internal/testutils"
"go.mindeco.de/log/level"
"github.com/ssbc/go-ssb/internal/config-reader"
)

type ConfigBool bool
type SbotConfig struct {
ShsCap string `json:"shscap,omitempty"`
Hmac string `json:"hmac,omitempty"`
Hops uint `json:"hops,omitempty"`

Repo string `json:"repo,omitempty"`
DebugDir string `json:"debugdir,omitempty"`

MuxRPCAddress string `json:"lis,omitempty"`
WebsocketAddress string `json:"wslis,omitempty"`
WebsocketTLSCert string `json:"wstlscert,omitempty"`
WebsocketTLSKey string `json:"wstlskey,omitempty"`
MetricsAddress string `json:"debuglis,omitempty"`

NoUnixSocket ConfigBool `json:"nounixsock"`
EnableAdvertiseUDP ConfigBool `json:"localadv"`
EnableDiscoveryUDP ConfigBool `json:"localdiscov"`
EnableEBT ConfigBool `json:"enable-ebt"`
EnableFirewall ConfigBool `json:"promisc"`
RepairFSBeforeStart ConfigBool `json:"repair"`

NumPeer uint `json:"numPeer,omitempty"`
NumRepl uint `json:"numRepl,omitempty"`

presence map[string]interface{}
}

func (config SbotConfig) Has(flagname string) bool {
_, ok := config.presence[flagname]
return ok
}

func readConfig(configPath string) (SbotConfig, bool) {
var conf SbotConfig

conf.presence = make(map[string]interface{})

// setup logger if not yet setup (used for tests)
if log == nil {
log = testutils.NewRelativeTimeLogger(nil)
}

data, err := os.ReadFile(configPath)
if err != nil {
level.Info(log).Log("event", "read config", "msg", "no config detected", "path", configPath)
return conf, false
}

level.Info(log).Log("event", "read config", "msg", "config detected", "path", configPath)

// 1) first we unmarshal into struct for type checks
decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data)))
err = decoder.Decode(&conf)
check(err, "decode into struct")

// 2) then we unmarshal into a map for presence check (to make sure bools are treated correctly)
decoder = json.NewDecoder(toml.New(bytes.NewBuffer(data)))
err = decoder.Decode(&conf.presence)
check(err, "decode into presence map")

// help repo path's default to align with common user expectations
conf.Repo = expandPath(conf.Repo)

return conf, true
}

// ensure the following type of path expansions take place:
// * ~/.ssb => /home/<user>/.ssb
// * .ssb => /home/<user>/.ssb
// * /stuff/.ssb => /stuff/.ssb
func expandPath(p string) string {
home, err := os.UserHomeDir()
if err != nil {
loglib.Fatalln("could not get user home directory (os.UserHomeDir()")
}

if strings.HasPrefix(p, "~") {
p = strings.Replace(p, "~", home, 1)
}

// not relative path, not absolute path =>
// place relative to home dir "~/<here>"
if !filepath.IsAbs(p) {
p = filepath.Join(home, p)
}

return p
}

func ReadEnvironmentVariables(config *SbotConfig) {
func ReadEnvironmentVariables(config *config.SbotConfig) {
if val := os.Getenv("SSB_SECRET_FILE"); val != "" {
loglib.Fatalln("flag SSB_SECRET_FILE not implemented")
}
Expand All @@ -130,88 +34,88 @@ func ReadEnvironmentVariables(config *SbotConfig) {
// go-ssb specific env flag, for peachcloud/pub compat
if val := os.Getenv("GO_SSB_REPAIR_FS"); val != "" {
config.RepairFSBeforeStart = readEnvironmentBoolean(val)
config.presence["repair"] = true
config.SetPresence("repair", true)
}

if val := os.Getenv("SSB_DATA_DIR"); val != "" {
config.Repo = val
config.presence["repo"] = true
config.SetPresence("repo", true)
}

if val := os.Getenv("SSB_LOG_DIR"); val != "" {
config.DebugDir = val
config.presence["debugdir"] = true
config.SetPresence("debugdir", true)
}

if val := os.Getenv("SSB_PROMETHEUS_ADDRESS"); val != "" {
config.MetricsAddress = val
config.presence["debuglis"] = true
config.SetPresence("debuglis", true)
}

if val := os.Getenv("SSB_PROMETHEUS_ENABLED"); val != "" {
config.presence["debuglis"] = readEnvironmentBoolean(val)
config.SetPresence("debuglis", readEnvironmentBoolean(val))
}

if val := os.Getenv("SSB_HOPS"); val != "" {
hops, err := strconv.Atoi(val)
check(err, "parse hops from environment variable")
config.Hops = uint(hops)
config.presence["hops"] = true
config.SetPresence("hops", true)
}

if val := os.Getenv("SSB_MUXRPC_ADDRESS"); val != "" {
config.MuxRPCAddress = val
config.presence["lis"] = true
config.SetPresence("lis", true)
}

if val := os.Getenv("SSB_WS_ADDRESS"); val != "" {
config.WebsocketAddress = val
config.presence["wslis"] = true
config.SetPresence("wslis", true)
}

if val := os.Getenv("SSB_WS_TLS_CERT"); val != "" {
config.WebsocketTLSCert = val
config.presence["wstlscert"] = true
config.SetPresence("wstlscert", true)
}

if val := os.Getenv("SSB_WS_TLS_KEY"); val != "" {
config.WebsocketTLSKey = val
config.presence["wstlskey"] = true
config.SetPresence("wstlskey", true)
}

if val := os.Getenv("SSB_EBT_ENABLED"); val != "" {
config.EnableEBT = readEnvironmentBoolean(val)
config.presence["enable-ebt"] = true
config.SetPresence("enable-ebt", true)
}

if val := os.Getenv("SSB_CONN_FIREWALL_ENABLED"); val != "" {
config.EnableFirewall = readEnvironmentBoolean(val)
config.presence["promisc"] = true
config.SetPresence("promisc", true)
}

if val := os.Getenv("SSB_SOCKET_ENABLED"); val != "" {
config.NoUnixSocket = !readEnvironmentBoolean(val)
config.presence["nounixsock"] = true
config.SetPresence("nounixsock", true)
}

if val := os.Getenv("SSB_CONN_DISCOVERY_UDP_ENABLED"); val != "" {
config.EnableDiscoveryUDP = readEnvironmentBoolean(val)
config.presence["localdiscov"] = true
config.SetPresence("localdiscov", true)
}

if val := os.Getenv("SSB_CONN_BROADCAST_UDP_ENABLED"); val != "" {
config.EnableAdvertiseUDP = readEnvironmentBoolean(val)
config.presence["localadv"] = true
config.SetPresence("localadv", true)
}

if val := os.Getenv("SSB_CAP_SHS_KEY"); val != "" {
config.ShsCap = val
config.presence["shscap"] = true
config.SetPresence("shscap", true)
}

if val := os.Getenv("SSB_CAP_HMAC_KEY"); val != "" {
config.Hmac = val
config.presence["hmac"] = true
config.SetPresence("hmac", true)
}

if val := os.Getenv("SSB_NUM_PEER"); val != "" {
Expand All @@ -227,53 +131,15 @@ func ReadEnvironmentVariables(config *SbotConfig) {
}
}

func (booly ConfigBool) MarshalJSON() ([]byte, error) {
temp := (bool)(booly)
b, err := json.Marshal(temp)
return b, err
}

func (booly *ConfigBool) UnmarshalJSON(b []byte) error {
// unmarshal into interface{} first, as a bool can't be unmarshaled into a string
var v interface{}
err := json.Unmarshal(b, &v)
if err != nil {
return eout(err, "unmarshal config bool")
}

// go through a type assertion dance, capturing the two cases:
// 1. if the config value is a proper boolean, and
// 2. if the config value is a boolish string (e.g. "true" or "1")
var temp bool
if val, ok := v.(bool); ok {
temp = val
} else if s, ok := v.(string); ok {
temp = booleanIsTrue(s)
if !temp {
// catch strings that cause a false value, but which aren't boolish
if s != "false" && s != "0" && s != "no" && s != "off" {
return errors.New("non-boolean string found when unmarshaling boolish values")
}
}
}
*booly = (ConfigBool)(temp)

return nil
}

func booleanIsTrue(s string) bool {
return s == "true" || s == "1" || s == "yes" || s == "on"
}

func readEnvironmentBoolean(s string) ConfigBool {
var booly ConfigBool
func readEnvironmentBoolean(s string) config.ConfigBool {
var booly config.ConfigBool
err := json.Unmarshal([]byte(s), booly)
check(err, "parsing environment variable bool")
return booly
}

func readConfigAndEnv(configPath string) (SbotConfig, bool) {
config, exists := readConfig(configPath)
func readConfigAndEnv(configPath string) (config.SbotConfig, bool) {
config, exists := config.ReadConfigSbot(configPath)
ReadEnvironmentVariables(&config)
return config, exists
}
Expand Down
24 changes: 14 additions & 10 deletions cmd/go-sbot/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
"time"

"github.com/ssbc/go-ssb/client"
"github.com/ssbc/go-ssb/internal/config-reader"
"github.com/stretchr/testify/require"
)

func TestMarshalConfigBooleans(t *testing.T) {
r := require.New(t)
configContents := `# Supply various flags to control go-sbot options.
configContents := `[go-sbot]
# Supply various flags to control go-sbot options.
hops = 2
# Address to listen on
Expand Down Expand Up @@ -50,7 +52,7 @@ numPeer = 5
# how many feeds can be replicated concurrently using legacy gossip replication
numRepl = 10
`
expectedConfig := SbotConfig{
expectedConfig := config.SbotConfig{
Hops: 2,
MuxRPCAddress: ":8008",
WebsocketAddress: ":8989",
Expand All @@ -70,7 +72,7 @@ numRepl = 10
configPath := filepath.Join(testPath, "config.toml")
err := os.WriteFile(configPath, []byte(configContents), 0700)
r.NoError(err, "write config file")
configFromDisk, _ := readConfig(configPath)
configFromDisk, _ := config.ReadConfigSbot(configPath)
// config values should be read correctly
r.EqualValues(expectedConfig.Hops, configFromDisk.Hops)
r.EqualValues(expectedConfig.MuxRPCAddress, configFromDisk.MuxRPCAddress)
Expand All @@ -88,7 +90,7 @@ numRepl = 10

func TestUnmarshalConfig(t *testing.T) {
r := require.New(t)
config := SbotConfig{
config := config.SbotConfig{
NoUnixSocket: true,
EnableAdvertiseUDP: true,
EnableDiscoveryUDP: true,
Expand Down Expand Up @@ -117,7 +119,8 @@ func TestUnmarshalConfig(t *testing.T) {

func TestConfiguredSbot(t *testing.T) {
r := require.New(t)
configContents := `# Supply various flags to control go-sbot options.
configContents := `[go-sbot]
# Supply various flags to control go-sbot options.
hops = 2
shscap = "0KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s="
Expand All @@ -142,7 +145,7 @@ numPeer = 5
# how many feeds can be replicated concurrently using legacy gossip replication
numRepl = 10
`
expectedConfig := SbotConfig{
expectedConfig := config.SbotConfig{
ShsCap: "0KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=",
Hops: 2,
MuxRPCAddress: ":8008",
Expand Down Expand Up @@ -188,7 +191,7 @@ numRepl = 10
runningConfPath := filepath.Join(testPath, "running-config.json")
b, err := os.ReadFile(runningConfPath)
r.NoError(err)
var runningConfig SbotConfig
var runningConfig config.SbotConfig
err = json.Unmarshal(b, &runningConfig)
r.NoError(err)

Expand All @@ -210,15 +213,16 @@ func TestConfigRepoPathExpands(t *testing.T) {
r := require.New(t)

testRepoConfig := func(repodir, expected, failMsg string) {
configContents := fmt.Sprintf(`repo = "%s"`, repodir)
configContents := fmt.Sprintf(`[go-sbot]
repo = "%s"`, repodir)

testPath := filepath.Join(".", "testrun", t.Name())
r.NoError(os.RemoveAll(testPath), "remove testrun folder")
r.NoError(os.MkdirAll(testPath, 0700), "make new testrun folder")
configPath := filepath.Join(testPath, "config.toml")
err := os.WriteFile(configPath, []byte(configContents), 0700)
r.NoError(err, "write config file")
parsedConfig, _ := readConfig(configPath)
parsedConfig, _ := config.ReadConfigSbot(configPath)

r.EqualValues(expected, parsedConfig.Repo, failMsg)
}
Expand Down Expand Up @@ -306,6 +310,6 @@ func TestGenerateDefaultConfig(t *testing.T) {
_, err = os.Stat(confPath)
r.NoError(err)

_, exists := readConfig(confPath)
_, exists := config.ReadConfigSbot(confPath)
r.True(exists)
}
Loading

0 comments on commit 2cdd828

Please sign in to comment.