package headscale import ( "errors" "time" "github.com/glebarez/sqlite" "github.com/rs/zerolog/log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) const ( dbVersion = "1" errValueNotFound = Error("not found") ) // KV is a key-value store in a psql table. For future use... type KV struct { Key string Value string } func (h *Headscale) initDB() error { db, err := h.openDB() if err != nil { return err } h.db = db if h.dbType == Postgres { db.Exec(`create extension if not exists "uuid-ossp";`) } _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") // If the Machine table has a column for registered, // find all occourences of "false" and drop them. Then // remove the column. if db.Migrator().HasColumn(&Machine{}, "registered") { log.Info(). Msg(`Database has legacy "registered" column in machine, removing...`) machines := Machines{} if err := h.db.Not("registered").Find(&machines).Error; err != nil { log.Error().Err(err).Msg("Error accessing db") } for _, machine := range machines { log.Info(). Str("machine", machine.Name). Str("machine_key", machine.MachineKey). Msg("Deleting unregistered machine") if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil { log.Error(). Err(err). Str("machine", machine.Name). Str("machine_key", machine.MachineKey). Msg("Error deleting unregistered machine") } } err := db.Migrator().DropColumn(&Machine{}, "registered") if err != nil { log.Error().Err(err).Msg("Error dropping registered column") } } err = db.AutoMigrate(&Machine{}) if err != nil { return err } err = db.AutoMigrate(&KV{}) if err != nil { return err } err = db.AutoMigrate(&Namespace{}) if err != nil { return err } err = db.AutoMigrate(&PreAuthKey{}) if err != nil { return err } _ = db.Migrator().DropTable("shared_machines") err = db.AutoMigrate(&APIKey{}) if err != nil { return err } err = h.setValue("db_version", dbVersion) return err } func (h *Headscale) openDB() (*gorm.DB, error) { var db *gorm.DB var err error var log logger.Interface if h.dbDebug { log = logger.Default } else { log = logger.Default.LogMode(logger.Silent) } switch h.dbType { case Sqlite: db, err = gorm.Open( sqlite.Open(h.dbString+"?_synchronous=1&_journal_mode=WAL"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, Logger: log, }, ) db.Exec("PRAGMA foreign_keys=ON") // The pure Go SQLite library does not handle locking in // the same way as the C based one and we cant use the gorm // connection pool as of 2022/02/23. sqlDB, _ := db.DB() sqlDB.SetMaxIdleConns(1) sqlDB.SetMaxOpenConns(1) sqlDB.SetConnMaxIdleTime(time.Hour) case Postgres: db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, Logger: log, }) } if err != nil { return nil, err } return db, nil } // getValue returns the value for the given key in KV. func (h *Headscale) getValue(key string) (string, error) { var row KV if result := h.db.First(&row, "key = ?", key); errors.Is( result.Error, gorm.ErrRecordNotFound, ) { return "", errValueNotFound } return row.Value, nil } // setValue sets value for the given key in KV. func (h *Headscale) setValue(key string, value string) error { keyValue := KV{ Key: key, Value: value, } if _, err := h.getValue(key); err == nil { h.db.Model(&keyValue).Where("key = ?", key).Update("value", value) return nil } h.db.Create(keyValue) return nil }