diff --git a/Makefile b/Makefile index 3d79a7e..b3f17f6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SHELL := bash -ARCH ?= arm64 +ARCH ?= amd64 ADDR ?= 0.0.0.0:8080 generate: @@ -11,10 +11,12 @@ build: env GOOS=linux GOARCH=${ARCH} go build -o bin/rcond-${ARCH} ./cmd/rcond/main.go run: - source .env && bin/rcond-${ARCH} ${ADDR} + bin/rcond-${ARCH} -config config.yaml dev: - RCOND_API_TOKEN=1234567890 go run cmd/rcond/main.go + RCOND_ADDR=127.0.0.1:8080 \ + RCOND_API_TOKEN=1234567890 \ + go run cmd/rcond/main.go upload: scp bin/rcond-${ARCH} pi@rpi-test:/home/pi/rcond diff --git a/README.md b/README.md index d714e36..abd8408 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A simple daemon and REST API to manage: - authorized SSH keys through the user's authorized_keys file ## Requirements + - Make - Go 1.19 or later - NetworkManager @@ -50,6 +51,20 @@ All endpoints except `/health` require authentication via an API token passed in ### Request/Response Format All endpoints use JSON for request and response payloads. +## Configuration + +### File + +The default config file location is `/etc/rcond/config.yaml`. +It can be overwritten by environment variables and flags. + +Example configuration: +```yaml +rcond: + addr: 0.0.0.0:8080 + api_token: 1234567890 +``` + ### Environment Variables | Environment Variable | Description | Default | diff --git a/cmd/rcond/main.go b/cmd/rcond/main.go index 7b6beaf..5b073f7 100644 --- a/cmd/rcond/main.go +++ b/cmd/rcond/main.go @@ -3,41 +3,105 @@ package main import ( + "flag" "fmt" "log" "os" + "github.com/0x1d/rcond/pkg/config" http "github.com/0x1d/rcond/pkg/http" ) -const ( - NETWORK_CONNECTION_UUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab" -) - func usage() { - fmt.Printf("Usage: %s
\n", os.Args[0]) - os.Exit(0) + fmt.Println("Usage: rcond ") + flag.PrintDefaults() } func main() { - - addr := os.Getenv("RCOND_ADDR") - if addr == "" { - addr = "0.0.0.0:8080" - } - if len(os.Args) > 1 { - addr = os.Args[1] - } - apiToken := os.Getenv("RCOND_API_TOKEN") - if apiToken == "" { - log.Fatal("RCOND_API_TOKEN environment variable not set") + appConfig, err := loadConfig() + if err != nil { + usage() + fmt.Printf("\nFailed to load config: %v\n", err) + os.Exit(1) } - srv := http.NewServer(addr, apiToken) + srv := http.NewServer(appConfig) srv.RegisterRoutes() - log.Printf("Starting server on %s", addr) + log.Printf("Starting server on %s", appConfig.Rcond.Addr) if err := srv.Start(); err != nil { log.Fatal(err) } } + +func loadConfig() (*config.Config, error) { + configPath := "/etc/rcond/config.yaml" + appConfig := &config.Config{} + help := false + + flag.StringVar(&configPath, "config", configPath, "Path to the configuration file") + flag.StringVar(&appConfig.Rcond.Addr, "addr", "", "Address to bind the HTTP server to") + flag.StringVar(&appConfig.Rcond.ApiToken, "token", "", "API token to use for authentication") + flag.BoolVar(&help, "help", false, "Show help") + flag.Parse() + + if help { + usage() + os.Exit(0) + } + + // Load config from file + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + configFile, err := config.LoadConfig(configPath) + if err != nil { + return nil, err + } + appConfig = configFile + } + + // Override config values from environment variables and flags + overrideConfigValuesFromEnv(map[string]*string{ + "RCOND_ADDR": &appConfig.Rcond.Addr, + "RCOND_API_TOKEN": &appConfig.Rcond.ApiToken, + }) + + overrideConfigValuesFromFlag(map[string]*string{ + "addr": &appConfig.Rcond.Addr, + "token": &appConfig.Rcond.ApiToken, + }) + + // Validate required fields + if err := validateRequiredFields(map[string]*string{ + "addr": &appConfig.Rcond.Addr, + "token": &appConfig.Rcond.ApiToken, + }); err != nil { + return nil, err + } + + return appConfig, nil +} + +func overrideConfigValuesFromEnv(envMap map[string]*string) { + for varName, configValue := range envMap { + if envValue, ok := os.LookupEnv(varName); ok { + *configValue = envValue + } + } +} + +func overrideConfigValuesFromFlag(flagMap map[string]*string) { + for flagName, configValue := range flagMap { + if flagValue := flag.Lookup(flagName).Value.String(); flagValue != "" { + *configValue = flagValue + } + } +} + +func validateRequiredFields(fields map[string]*string) error { + for name, value := range fields { + if *value == "" { + return fmt.Errorf("%s is required", name) + } + } + return nil +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..5627dea --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +rcond: + addr: 0.0.0.0:8080 + api_token: 1234567890 diff --git a/go.mod b/go.mod index acbb638..2fe6c29 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 golang.org/x/crypto v0.37.0 + gopkg.in/yaml.v3 v3.0.1 ) require golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index bee5136..83db8c0 100644 --- a/go.sum +++ b/go.sum @@ -10,3 +10,7 @@ golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/install/rcond.service b/install/rcond.service new file mode 100644 index 0000000..f284444 --- /dev/null +++ b/install/rcond.service @@ -0,0 +1,13 @@ +[Unit] +Description=rcond service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/path/to/your/rcond +ExecStart=/path/to/your/rcond/bin/rcond-${ARCH} ${ADDR} +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..f699b89 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,38 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Rcond RcondConfig `yaml:"rcond"` +} + +type RcondConfig struct { + Addr string `yaml:"addr"` + ApiToken string `yaml:"api_token"` +} + +func LoadConfig(path string) (*Config, error) { + yamlFile, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var config Config + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + return nil, err + } + return &config, nil +} + +func SaveConfig(path string, config *Config) error { + yamlFile, err := yaml.Marshal(config) + if err != nil { + return err + } + return os.WriteFile(path, yamlFile, 0644) +} diff --git a/pkg/http/server.go b/pkg/http/server.go index 200cd61..ad14f9d 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/0x1d/rcond/pkg/config" "github.com/gorilla/mux" ) @@ -15,11 +16,15 @@ type Server struct { apiToken string } -func NewServer(addr string, apiToken string) *Server { +func NewServer(cfg *config.Config) *Server { + if cfg.Rcond.Addr == "" || cfg.Rcond.ApiToken == "" { + panic("addr or api_token is not set") + } + router := mux.NewRouter() srv := &http.Server{ - Addr: addr, + Addr: cfg.Rcond.Addr, Handler: router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, @@ -28,7 +33,7 @@ func NewServer(addr string, apiToken string) *Server { return &Server{ router: router, srv: srv, - apiToken: apiToken, + apiToken: cfg.Rcond.ApiToken, } }