Skip to content

Commit

Permalink
refactor: configuration management
Browse files Browse the repository at this point in the history
  • Loading branch information
jozefcipa committed May 8, 2024
1 parent ebb950a commit ac729fe
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 110 deletions.
15 changes: 8 additions & 7 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package cmd

import (
"errors"
"os"

"github.com/jozefcipa/novus/internal/brew"
"github.com/jozefcipa/novus/internal/config"
"github.com/jozefcipa/novus/internal/config_manager"
"github.com/jozefcipa/novus/internal/logger"
"github.com/jozefcipa/novus/internal/shared"
"github.com/jozefcipa/novus/internal/tui"
Expand All @@ -15,27 +15,28 @@ import (
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize Novus configuration",
Long: "Initialize Novus configuration by creating the " + config.AppName + " file and installs all required binaries if not installed yet.",
Long: "Initialize Novus configuration by creating the " + config.ConfigFileName + " file and installs all required binaries if not installed yet.",
Run: func(cmd *cobra.Command, args []string) {
// Install nginx, dnsmasq and mkcert if not installed
if err := brew.InstallBinaries(); err != nil {
if errors.Is(err, &brew.BrewMissingError{}) {
logger.Errorf(err.Error())
logger.Errorf(err.Error())

if _, ok := err.(*brew.BrewMissingError); ok {
logger.Hintf("You can install it from \033[4mhttps://brew.sh/\033[0m")
}
os.Exit(1)
}

// Check if novus.yml config exists
_, exists := config.Load()
_, exists := config_manager.LoadConfiguration()
if !exists {
// If config doesn't exist, create a new one
input := tui.AskUser("Enter a new app name: ")
appName := shared.ToKebabCase(input)

err := config.CreateDefaultConfigFile(appName)
err := config_manager.CreateNewConfiguration(appName)
if err != nil {
logger.Errorf("%s", err.Error())
logger.Errorf(err.Error())
os.Exit(1)
}
logger.Successf("Novus has been initialized.")
Expand Down
31 changes: 4 additions & 27 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package cmd

import (
"os"
"slices"

"github.com/jozefcipa/novus/internal/brew"
"github.com/jozefcipa/novus/internal/config"
"github.com/jozefcipa/novus/internal/config_manager"
"github.com/jozefcipa/novus/internal/diff_manager"
"github.com/jozefcipa/novus/internal/dns_manager"
"github.com/jozefcipa/novus/internal/dnsmasq"
Expand All @@ -26,21 +26,20 @@ var serveCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
// If the binaries are missing, exit here, user needs to run `novus init` first
if err := brew.CheckIfRequiredBinariesInstalled(); err != nil {
logger.Errorf(err.Error())
logger.Hintf("Run \"novus init\" first to initialize Novus.")
os.Exit(1)
}

// Load configuration file
conf, exists := config.Load()
if exists {
conf, exists := config_manager.LoadConfiguration()
if !exists {
logger.Warnf("Novus is not initialized in this directory (" + config.ConfigFileName + " file does not exist).")
logger.Hintf("Run \"novus init\" to create a configuration file.")
os.Exit(1)
}

// Load application state
appState, isNewState := novus.GetAppState(config.AppName)
appState, isNewState := novus.GetAppState(config.AppName())

// Compare state and current config to detect changes
addedRoutes, deletedRoutes := diff_manager.DetectConfigDiff(conf, *appState)
Expand Down Expand Up @@ -68,28 +67,6 @@ var serveCmd = &cobra.Command{
}
}

// TODO: refactor this into a function and move to some module
// Check if there are duplicate domains across apps
type AppDomain struct {
App string
Domain string
}
allDomains := []AppDomain{}
for appName, appConfig := range novus.GetState().Apps {
for _, route := range appConfig.Routes {
allDomains = append(allDomains, AppDomain{App: appName, Domain: route.Domain})
}
}
for _, route := range addedRoutes {
if idx := slices.IndexFunc(allDomains, func(appDomain AppDomain) bool { return appDomain.Domain == route.Domain }); idx != -1 {
usedDomainAppName := allDomains[idx].App
logger.Errorf("Domain %s is already defined by app \"%s\"", route.Domain, usedDomainAppName)
logger.Hintf("Use a different domain name or temporarily stop \"%[1]s\" by running `novus stop %[1]s`", usedDomainAppName)
os.Exit(1)
}
allDomains = append(allDomains, AppDomain{App: conf.AppName, Domain: route.Domain})
}

// Configure SSL
mkcert.Configure(conf)
domainCerts, hasNewCerts := ssl_manager.EnsureSSLCertificates(conf)
Expand Down
4 changes: 2 additions & 2 deletions cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ and print a list of all URLs that are registered by Novus.`,
isDNSMasqRunning := <-dnsMasqChan

if isNginxRunning {
logger.Successf("Nginx running")
logger.Successf("Nginx running 🚀")
logger.Debugf("Nginx configuration loaded from %s", nginx.NginxServersDir)
} else {
logger.Errorf("Nginx not running")
}

if isDNSMasqRunning {
logger.Successf("DNSMasq running")
logger.Successf("DNSMasq running 🚀")
} else {
logger.Errorf("DNSMasq not running")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/brew/brew.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func CheckIfRequiredBinariesInstalled() error {
func InstallBinaries() error {
// First check that Homebrew is installed
brewExists := binExists("brew")
if brewExists {
if !brewExists {
return &BrewMissingError{}
}

Expand Down Expand Up @@ -174,7 +174,7 @@ func execBrewCommand(commands []string) []byte {

out, err := cmd.Output()
if err != nil {
logger.Errorf("Failed to run %s: %v", commandString, err)
logger.Errorf("Failed to run \"%s\": %v", commandString, err)
os.Exit(1)
}

Expand Down
59 changes: 14 additions & 45 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,54 @@
package config

import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/go-playground/validator/v10"
"github.com/jozefcipa/novus/internal/fs"
"github.com/jozefcipa/novus/internal/logger"
"github.com/jozefcipa/novus/internal/novus"
"github.com/jozefcipa/novus/internal/shared"
"gopkg.in/yaml.v3"
)

const ConfigFileName = "novus.yml"

var AppName = ""
var appName = ""

type NovusConfig struct {
AppName string `yaml:"appName" validate:"required"`
Routes []shared.Route `yaml:"routes" validate:"required,unique_routes,dive"`
}

func (config *NovusConfig) validate() {
logger.Debugf("Validating configuration file")

validate := validator.New(validator.WithRequiredStructEnabled())

// Register custom `unique_routes` rule
shared.RegisterUniqueRoutesValidator(validate)

if err := validate.Struct(config); err != nil {
logger.Errorf("Configuration file contains errors.\n\n%s", err.(validator.ValidationErrors))
os.Exit(1)
}

if err := validateAppName(config.AppName); err != nil {
logger.Errorf("Configuration file contains errors.\n\n%s", err.Error())
os.Exit(1)
}
func SetAppName(name string) {
logger.Debugf("Setting app [app=%s]", name)
appName = name
}

func validateAppName(appName string) error {
isValid, _ := regexp.MatchString("^[A-Za-z0-9-_]+$", appName)
if !isValid {
return fmt.Errorf("Invalid app name. Only alphanumeric characters are allowed.")
}

// Check in state file if appName is already being used
for appNameFromConfig, appConfig := range novus.GetState().Apps {
if appNameFromConfig == appName && appConfig.Directory != fs.CurrentDir {
return fmt.Errorf("App \"%s\" is already defined in a different directory (%s)", appName, appConfig.Directory)
}
func AppName() string {
if appName != "" {
return appName
}

return nil
// This should not happen normally,
// but let's throw an error if the program tries to access config.AppName() when not set
logger.Errorf("[Internal error]: No app set, make sure to call `config.SetAppName()`")
os.Exit(1)
return ""
}

func CreateDefaultConfigFile(appName string) error {
func WriteDefaultFile(appName string) {
// Read the config file template
configTemplate := fs.ReadFileOrExit(filepath.Join(fs.AssetsDir, "novus.template.yml"))

// Set app name in the config
configTemplate = strings.Replace(configTemplate, "--APP_NAME--", appName, 1)

if err := validateAppName(appName); err != nil {
return err
}

// Create a new config file
fs.WriteFileOrExit(filepath.Join(fs.CurrentDir, ConfigFileName), configTemplate)
return nil
}

func Load() (NovusConfig, bool) {
func LoadFile() (NovusConfig, bool) {
configPath := filepath.Join(fs.CurrentDir, ConfigFileName)

logger.Debugf("Loading configuration file [%s]", configPath)
Expand All @@ -91,9 +64,5 @@ func Load() (NovusConfig, bool) {
os.Exit(1)
}

config.validate()

AppName = config.AppName

return config, true
}
97 changes: 97 additions & 0 deletions internal/config_manager/config_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package config_manager

import (
"fmt"
"os"
"regexp"

"github.com/go-playground/validator/v10"
"github.com/jozefcipa/novus/internal/config"
"github.com/jozefcipa/novus/internal/diff_manager"
"github.com/jozefcipa/novus/internal/fs"
"github.com/jozefcipa/novus/internal/logger"
"github.com/jozefcipa/novus/internal/novus"
"github.com/jozefcipa/novus/internal/shared"
)

func LoadConfiguration() (config.NovusConfig, bool) {
conf, exists := config.LoadFile()
if !exists {
return conf, false
}

// Validate configuration
if err := validateConfigSyntax(conf); err != nil {
logger.Errorf("Configuration file contains errors.\n\n%s", err.(validator.ValidationErrors))
os.Exit(1)
}

// Validate app name syntax and whether it is unique across apps
if err := validateConfigAppName(conf.AppName); err != nil {
logger.Errorf(err.Error())
os.Exit(1)
}

// Check if the config contains domains that are already registered in another app
if err := checkForDuplicateDomains(conf); err != nil {
logger.Errorf(err.Error())
if err, ok := err.(*diff_manager.DuplicateDomainError); ok {
logger.Hintf(
"Use a different domain name or temporarily stop %[1]s by running \"novus stop %[1]s\"",
err.OriginalAppWithDomain,
)
}
os.Exit(1)
}

config.SetAppName(conf.AppName)

return conf, true
}

func CreateNewConfiguration(appName string) error {
if err := validateConfigAppName(appName); err != nil {
return err
}

config.WriteDefaultFile(appName)
return nil
}

func validateConfigSyntax(conf config.NovusConfig) error {
logger.Debugf("Validating configuration file syntax")

validate := validator.New(validator.WithRequiredStructEnabled())

// Register custom `unique_routes` rule
shared.RegisterUniqueRoutesValidator(validate)

return validate.Struct(conf)
}

func validateConfigAppName(appName string) error {
logger.Debugf("Validating configuration file app name [%s]", appName)

isValid, _ := regexp.MatchString("^[A-Za-z0-9-_]+$", appName)
if !isValid {
return fmt.Errorf("Invalid app name. Only alphanumeric characters are allowed.")
}

// Check in state file if appName is already being used
for appNameFromConfig, appConfig := range novus.GetState().Apps {
if appNameFromConfig == appName && appConfig.Directory != fs.CurrentDir {
return fmt.Errorf("App \"%s\" is already defined in a different directory (%s)", appName, appConfig.Directory)
}
}

return nil
}

func checkForDuplicateDomains(conf config.NovusConfig) error {
logger.Debugf("Checking for duplicate domains across apps")

appState, _ := novus.GetAppState(conf.AppName)
addedRoutes, _ := diff_manager.DetectConfigDiff(conf, *appState)

return diff_manager.DetectDuplicateDomains(novus.GetState().Apps, addedRoutes)
}
30 changes: 30 additions & 0 deletions internal/diff_manager/diff_manager.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package diff_manager

import (
"slices"

"github.com/jozefcipa/novus/internal/config"
"github.com/jozefcipa/novus/internal/dns_manager"
"github.com/jozefcipa/novus/internal/novus"
Expand Down Expand Up @@ -44,3 +46,31 @@ func DetectUnusedTLDs(conf config.NovusConfig, state novus.AppState) (unusedTLDs

return unusedTLDs
}

type appDomain struct {
App string
Domain string
}

func DetectDuplicateDomains(existingApps map[string]*novus.AppState, addedRoutes []shared.Route) error {
allDomains := []appDomain{}

// Collect all existing domains across apps
for appName, appConfig := range existingApps {
for _, route := range appConfig.Routes {
allDomains = append(allDomains, appDomain{App: appName, Domain: route.Domain})
}
}

// Iterate through the newly added routes to see if some of them already exists in the slice
for _, route := range addedRoutes {
if idx := slices.IndexFunc(allDomains, func(appDomain appDomain) bool { return appDomain.Domain == route.Domain }); idx != -1 {
return &DuplicateDomainError{
DuplicateDomain: route.Domain,
OriginalAppWithDomain: allDomains[idx].App,
}
}
}

return nil
}
Loading

0 comments on commit ac729fe

Please sign in to comment.