package config // code for managing groundseg and container configurations import ( "encoding/base64" "encoding/json" "fmt" "goseg/defaults" "goseg/structs" "io/ioutil" "log/slog" "math/rand" "net" "os" "path/filepath" "runtime" "sync" "time" ) var ( Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) // global settings config (accessed via funcs) globalConfig structs.SysConfig // base path for installation (override default with env var) BasePath = os.Getenv("GS_BASE_PATH") // only amd64 or arm64 Architecture = getArchitecture() // struct of /retrieve blob StartramConfig structs.StartramRetrieve // unused for now, set with `./groundseg dev` DebugMode = false Ready = false // representation of desired/actual container states GSContainers = make(map[string]structs.ContainerState) DockerDir = "/var/lib/docker/volumes/" // version server check checkInterval = 5 * time.Minute confPath = filepath.Join(BasePath, "settings", "system.json") confMutex sync.Mutex contMutex sync.Mutex versMutex sync.Mutex ) // try initializing from system.json on disk func init() { Logger.Info("Starting GroundSeg") Logger.Info("Urbit is love <3") for _, arg := range os.Args[1:] { // trigger this with `./groundseg dev` if arg == "dev" { Logger.Info("Starting GroundSeg in debug mode") DebugMode = true } } if BasePath == "" { // default base path BasePath = "/opt/nativeplanet/groundseg" } pathMsg := fmt.Sprintf("Loading configs from %s", BasePath) Logger.Info(pathMsg) confPath := filepath.Join(BasePath, "settings", "system.json") file, err := os.Open(confPath) if err != nil { // create a default if it doesn't exist err = createDefaultConf() if err != nil { // panic if we can't create it errmsg := fmt.Sprintf("Unable to create config! Please elevate permissions. %v", err) Logger.Error(errmsg) panic(errmsg) } // generate and insert wireguard keys wgPriv, wgPub, err := WgKeyGen() salt := RandString(32) if err != nil { Logger.Error(fmt.Sprintf("%v", err)) } else { err = UpdateConf(map[string]interface{}{ "Pubkey": wgPub, "Privkey": wgPriv, "Salt": salt, }) if err != nil { Logger.Error(fmt.Sprintf("%v", err)) } } } defer file.Close() // read the sysconfig to memory decoder := json.NewDecoder(file) err = decoder.Decode(&globalConfig) if err != nil { errmsg := fmt.Sprintf("Error decoding JSON: %v", err) Logger.Error(errmsg) } // wipe the sessions on each startup globalConfig.Sessions.Authorized = make(map[string]structs.SessionInfo) globalConfig.Sessions.Unauthorized = make(map[string]structs.SessionInfo) configMap := make(map[string]interface{}) configBytes, err := json.Marshal(globalConfig) if err != nil { errmsg := fmt.Sprintf("Error marshaling JSON: %v", err) Logger.Error(errmsg) } err = json.Unmarshal(configBytes, &configMap) if err != nil { errmsg := fmt.Sprintf("Error unmarshaling JSON: %v", err) Logger.Error(errmsg) } err = persistConf(configMap) if err != nil { errmsg := fmt.Sprintf("Error persisting JSON: %v", err) Logger.Error(errmsg) } file, err = os.Open(confPath) if err != nil { errmsg := fmt.Sprintf("Error opening JSON: %v", err) Logger.Error(errmsg) } decoder = json.NewDecoder(file) err = decoder.Decode(&globalConfig) if err != nil { errmsg := fmt.Sprintf("Error decoding JSON: %v", err) Logger.Error(errmsg) } } // return the global conf var func Conf() structs.SysConfig { confMutex.Lock() defer confMutex.Unlock() return globalConfig } // tell if we're amd64 or arm64 func getArchitecture() string { switch runtime.GOARCH { case "arm64", "aarch64": return "arm64" default: return "amd64" } } // update by passing in a map of key:values you want to modify func UpdateConf(values map[string]interface{}) error { // mutex lock to avoid race conditions confMutex.Lock() defer confMutex.Unlock() file, err := ioutil.ReadFile(confPath) if err != nil { return fmt.Errorf("Unable to load config: %v", err) } // unmarshal the config to struct var configMap map[string]interface{} if err := json.Unmarshal(file, &configMap); err != nil { return fmt.Errorf("Error decoding JSON: %v", err) } // update our unmarshaled struct for key, value := range values { configMap[key] = value } if err = persistConf(configMap); err != nil { return fmt.Errorf("Unable to persist config update: %v", err) } return nil } func persistConf(configMap map[string]interface{}) error { // marshal and persist it updatedJSON, err := json.MarshalIndent(configMap, "", " ") if err != nil { return fmt.Errorf("Error encoding JSON: %v", err) } // update the globalConfig var if err := json.Unmarshal(updatedJSON, &globalConfig); err != nil { return fmt.Errorf("Error updating global config: %v", err) } // write to disk Logger.Info("Persisting configuration to disk") if err := ioutil.WriteFile(confPath, updatedJSON, 0644); err != nil { return fmt.Errorf("Error writing to file: %v", err) } return nil } // we keep map[string]structs.ContainerState in memory to keep track of the containers // eg if they're running and whether they should be // modify the desired/actual state of containers func UpdateContainerState(name string, containerState structs.ContainerState) { contMutex.Lock() defer contMutex.Unlock() GSContainers[name] = containerState logMsg := "" if DebugMode { res, _ := json.Marshal(containerState) logMsg = string(res) } Logger.Info(fmt.Sprintf("%s state:%s", name, logMsg)) } // get the current container state func GetContainerState() map[string]structs.ContainerState { contMutex.Lock() defer contMutex.Unlock() return GSContainers } // write a default conf to disk func createDefaultConf() error { defaultConfig := defaults.SysConfig(BasePath) path := filepath.Join(BasePath, "settings", "system.json") if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { return err } file, err := os.Create(path) if err != nil { return err } defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") if err := encoder.Encode(&defaultConfig); err != nil { return err } return nil } // check outbound tcp connectivity // takes ip:port func NetCheck(netCheck string) bool { internet := false timeout := 3 * time.Second conn, err := net.DialTimeout("tcp", netCheck, timeout) if err != nil { errmsg := fmt.Sprintf("Check internet access error: %v", err) Logger.Error(errmsg) } else { internet = true _ = conn.Close() } return internet } // generates a random secret string of the input length func RandString(length int) string { randBytes := make([]byte, length) _, err := rand.Read(randBytes) if err != nil { Logger.Warn("Random error :s") return "" } return base64.URLEncoding.EncodeToString(randBytes) }