Adding new system settings table. (#409)

* Adding new system settings table.
Adding associated database migration.
Added tests
Fixing tests for user-settings.

* updated migration, make sure default system settings are created.
Added database commands to create and update system settings.
Added generic response wrapper.

* make sure we can get related versions (fasten sources, onprem and desktop) when starting the app.
This commit is contained in:
Jason Kulatunga 2024-02-09 09:10:06 -08:00 committed by GitHub
parent dc9704831e
commit c85e829c46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 560 additions and 20 deletions

2
.gitignore vendored
View File

@ -68,3 +68,5 @@ fasten-*.db-wal
fasten.db
fasten.db-shm
fasten.db-wal
backend/resources/related_versions.json

View File

@ -39,6 +39,7 @@ clean-backend:
.PHONY: dep-backend
dep-backend:
go mod tidy && go mod vendor
cd scripts && go generate ./...
.PHONY: test-backend

View File

@ -10,6 +10,7 @@ import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
"github.com/fastenhealth/fasten-onprem/backend/pkg/web"
"github.com/fastenhealth/fasten-onprem/backend/resources"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"io"
@ -114,10 +115,13 @@ func main() {
settingsData, err := json.Marshal(appconfig.AllSettings())
appLogger.Debug(string(settingsData), err)
relatedVersions, _ := resources.GetRelatedVersions()
webServer := web.AppEngine{
Config: appconfig,
Logger: appLogger,
EventBus: event_bus.NewEventBusServer(appLogger),
Config: appconfig,
Logger: appLogger,
EventBus: event_bus.NewEventBusServer(appLogger),
RelatedVersions: relatedVersions,
}
return webServer.Start()
},

View File

@ -7,6 +7,9 @@ type BackgroundJobSchedule string
type DatabaseRepositoryType string
type InstallationVerificationStatus string
type InstallationQuotaStatus string
const (
ResourceListPageSize int = 20
@ -41,4 +44,10 @@ const (
DatabaseRepositoryTypeSqlite DatabaseRepositoryType = "sqlite"
DatabaseRepositoryTypePostgres DatabaseRepositoryType = "postgres"
InstallationVerificationStatusMissing InstallationVerificationStatus = "MISSING" //email is missing for this installation
InstallationVerificationStatusPending InstallationVerificationStatus = "PENDING" //email has not been verified
InstallationVerificationStatusVerified InstallationVerificationStatus = "VERIFIED" //email has been verified
InstallationQuotaStatusActive InstallationQuotaStatus = "ACTIVE"
InstallationQuotaStatusConsumed InstallationQuotaStatus = "CONSUMED"
)

View File

@ -7,6 +7,7 @@ import (
_20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541"
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
@ -141,6 +142,45 @@ func (gr *GormRepository) Migrate() error {
)
},
},
{
ID: "20240208112210", // add system settings
Migrate: func(tx *gorm.DB) error {
err := tx.AutoMigrate(
&_20240208112210.SystemSettingEntry{},
)
if err != nil {
return err
}
//add the default system settings
defaultSystemSettings := []_20240208112210.SystemSettingEntry{
{
SettingKeyName: "installation_id",
SettingKeyDescription: "installation id is used to identify this installation when making external calls to Fasten Health, Inc. infrastructure. It does not contain any personally identifiable information",
SettingDataType: "string",
SettingValueString: "",
},
{
SettingKeyName: "installation_secret",
SettingKeyDescription: "installation secret is used to sign requests/updates to Fasten Health, Inc. infrastructure",
SettingDataType: "string",
SettingValueString: "",
},
}
for _, setting := range defaultSystemSettings {
tx.Logger.Info(context.Background(), fmt.Sprintf("Creating System Setting: %s", setting.SettingKeyName))
settingCreateResp := tx.Create(&setting)
if settingCreateResp.Error != nil {
tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred creating System Setting: %s", setting.SettingKeyName))
return settingCreateResp.Error
}
}
return nil
},
},
})
// run when database is empty

View File

@ -3,11 +3,77 @@ package database
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/google/uuid"
)
// LoadSystemSettings will retrieve settings from the database, and return a SystemSettings struct
func (gr *GormRepository) LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error) {
settingsEntries := []models.SystemSettingEntry{}
if err := gr.GormClient.
WithContext(ctx).
Find(&settingsEntries).Error; err != nil {
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
}
settings := models.SystemSettings{}
for _, settingsEntry := range settingsEntries {
err := settings.FromSystemSettingsEntry(&settingsEntry)
if err != nil {
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
}
}
return &settings, nil
}
// testing
// SaveSystemSettings will update save the settings to the database.
func (gr *GormRepository) SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error {
//retrieve current settings from the database
currentSettingsEntries := []models.SystemSettingEntry{}
if err := gr.GormClient.
WithContext(ctx).
Find(&currentSettingsEntries).Error; err != nil {
return fmt.Errorf("Could not get settings from DB: %v", err)
}
//update settingsEntries
newSettingsEntries, err := newSettings.ToSystemSettingsEntry(currentSettingsEntries)
if err != nil {
return fmt.Errorf("merge new settings with DB: %v", err)
}
for ndx, settingsEntry := range newSettingsEntries {
var upsertErr error
if settingsEntry.ID == uuid.Nil {
//create new entry
upsertErr = gr.GormClient.
WithContext(ctx).
Model(&models.SystemSettingEntry{}).
Create(&settingsEntry).Error
} else {
// store in database.
upsertErr = gr.GormClient.
WithContext(ctx).
Model(&models.SystemSettingEntry{}).
Where([]uuid.UUID{settingsEntry.ID}).
Select("setting_value_numeric", "setting_value_string", "setting_value_bool", "setting_value_array").
Updates(newSettingsEntries[ndx]).Error
}
if upsertErr != nil {
return err
}
}
return nil
}
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
func (gr *GormRepository) LoadUserSettings(ctx context.Context) (*models.UserSettings, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)

View File

@ -53,6 +53,8 @@ type DatabaseRepository interface {
ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error)
//settings
LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error)
SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error
LoadUserSettings(ctx context.Context) (*models.UserSettings, error)
SaveUserSettings(context.Context, *models.UserSettings) error
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error

View File

@ -0,0 +1,22 @@
package _20240208112210
import "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
// SystemSettingEntry matches a setting row in the database
type SystemSettingEntry struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
models.ModelBase
SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`
SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
}
func (s SystemSettingEntry) TableName() string {
return "system_settings"
}

View File

@ -0,0 +1,13 @@
package models
type InstallationRegistrationRequest struct {
// AdministratorEmail specifies email address for the administrator of the installation
AdministratorEmail string `json:"administrator_email,omitempty"` //opt-in
SoftwareArchitecture string `json:"software_architecture,omitempty"`
SoftwareOS string `json:"software_os,omitempty"`
FastenDesktopVersion string `json:"fasten_desktop_version,omitempty"`
FastenOnpremVersion string `json:"fasten_onprem_version,omitempty"`
FastenSourcesVersion string `json:"fasten_sources_version,omitempty"`
}

View File

@ -0,0 +1,22 @@
package models
import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"time"
)
type InstallationRegistrationResponse struct {
// InstallationID specifies client identifier string. REQUIRED
InstallationID string `json:"installation_id"`
// InstallationSecret specifies client secret string. OPTIONAL
InstallationSecret string `json:"installation_secret"`
// InstallationIDIssuedAt specifies time at which the client identifier was issued. OPTIONAL
InstallationIDIssuedAt time.Time `json:"installation_id_issued_at"`
VerificationStatus pkg.InstallationVerificationStatus `json:"verification_status"`
QuotaStatus pkg.InstallationQuotaStatus `json:"quota_status"`
*InstallationRegistrationRequest `json:",inline"`
}

View File

@ -5,3 +5,9 @@ type ResponseWrapper struct {
Error string `json:"error"`
Data interface{} `json:"data"`
}
type ResponseWrapperTyped[T any] struct {
Success bool `json:"success"`
Error string `json:"error"`
Data T `json:"data"`
}

View File

@ -0,0 +1,20 @@
package models
// SystemSettingEntry matches a setting row in the database
type SystemSettingEntry struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
ModelBase
SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`
SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
}
func (s SystemSettingEntry) TableName() string {
return "system_settings"
}

View File

@ -0,0 +1,70 @@
package models
import (
"reflect"
)
type SystemSettings struct {
InstallationID string `json:"installation_id"`
InstallationSecret string `json:"installation_secret"`
}
// see https://gist.github.com/lelandbatey/a5c957b537bed39d1d6fb202c3b8de06
func (s *SystemSettings) FromSystemSettingsEntry(entry *SystemSettingEntry) error {
structType := reflect.ValueOf(s).Elem()
for i := 0; i < structType.NumField(); i++ {
typeField := structType.Type().Field(i)
if jsonTagValue := typeField.Tag.Get("json"); jsonTagValue == entry.SettingKeyName {
//fmt.Println("found field", field.Name)
if entry.SettingDataType == "numeric" {
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
} else if entry.SettingDataType == "string" {
structType.Field(i).SetString(entry.SettingValueString)
} else if entry.SettingDataType == "bool" {
structType.Field(i).SetBool(entry.SettingValueBool)
} else if entry.SettingDataType == "array" {
structType.Field(i).Set(reflect.ValueOf(entry.SettingValueArray))
}
break
}
}
//if entry.SettingKeyName == "dashboard_locations" {
// s.DashboardLocations = entry.SettingValueArray
//}
return nil
}
func (s *SystemSettings) ToSystemSettingsEntry(entries []SystemSettingEntry) ([]SystemSettingEntry, error) {
structType := reflect.ValueOf(s).Elem()
fieldNameNdxLookup := map[string]int{}
for i := 0; i < structType.NumField(); i++ {
typeField := structType.Type().Field(i)
jsonTagValue := typeField.Tag.Get("json")
fieldNameNdxLookup[jsonTagValue] = i
}
for ndx, entry := range entries {
fieldId := fieldNameNdxLookup[entry.SettingKeyName]
if entry.SettingDataType == "numeric" {
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
} else if entry.SettingDataType == "string" {
entries[ndx].SettingValueString = structType.Field(fieldId).String()
} else if entry.SettingDataType == "bool" {
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
} else if entry.SettingDataType == "array" {
sliceVal := structType.Field(fieldId).Slice(0, structType.Field(fieldId).Len())
entries[ndx].SettingValueArray = sliceVal.Interface().([]string)
}
}
return entries, nil
}

View File

@ -0,0 +1,57 @@
package models
import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"testing"
)
func TestFromSystemSettingsEntry(t *testing.T) {
t.Parallel()
//setup
systemSettings := new(SystemSettings)
systemSettingsEntry := SystemSettingEntry{
SettingKeyName: "installation_id",
SettingDataType: "string",
SettingValueString: "12345",
}
//test
err := systemSettings.FromSystemSettingsEntry(&systemSettingsEntry)
//assert
require.NoError(t, err)
require.Equal(t, "12345", systemSettings.InstallationID)
require.Equal(t, "", systemSettings.InstallationSecret)
}
func TestToSystemSettingsEntry(t *testing.T) {
t.Parallel()
//setup
systemSettings := new(SystemSettings)
previousSystemSettingsEntries := []SystemSettingEntry{{
ModelBase: ModelBase{
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
},
SettingKeyName: "installation_id",
SettingDataType: "string",
SettingValueString: "4567",
}}
//test
systemSettings.InstallationID = "9876"
updatedSystemSettingsEntries, err := systemSettings.ToSystemSettingsEntry(previousSystemSettingsEntries)
//assert
require.NoError(t, err)
require.Equal(t, []SystemSettingEntry{{
ModelBase: ModelBase{
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
},
SettingKeyName: "installation_id",
SettingDataType: "string",
SettingValueString: "9876",
}}, updatedSystemSettingsEntries)
}

View File

@ -21,7 +21,7 @@ func (s *UserSettings) FromUserSettingsEntry(entry *UserSettingEntry) error {
if entry.SettingDataType == "numeric" {
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
} else if entry.SettingDataType == "string" {
structType.Elem().Field(i).SetString(entry.SettingValueString)
structType.Field(i).SetString(entry.SettingValueString)
} else if entry.SettingDataType == "bool" {
structType.Field(i).SetBool(entry.SettingValueBool)
} else if entry.SettingDataType == "array" {
@ -55,7 +55,7 @@ func (s *UserSettings) ToUserSettingsEntry(entries []UserSettingEntry) ([]UserSe
if entry.SettingDataType == "numeric" {
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
} else if entry.SettingDataType == "string" {
entries[ndx].SettingValueString = structType.Elem().Field(fieldId).String()
entries[ndx].SettingValueString = structType.Field(fieldId).String()
} else if entry.SettingDataType == "bool" {
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
} else if entry.SettingDataType == "array" {

View File

@ -2,20 +2,11 @@ package middleware
import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger, eventBus event_bus.Interface) gin.HandlerFunc {
deviceRepo, err := database.NewRepository(appConfig, globalLogger, eventBus)
if err != nil {
panic(err)
}
func RepositoryMiddleware(deviceRepo database.DatabaseRepository) gin.HandlerFunc {
//TODO: determine where we can call defer deviceRepo.Close()
return func(c *gin.Context) {
c.Set(pkg.ContextKeyTypeDatabase, deviceRepo)

View File

@ -1,31 +1,46 @@
package web
import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/handler"
"github.com/fastenhealth/fasten-onprem/backend/pkg/web/middleware"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"io"
"net/http"
"runtime"
"strings"
)
type AppEngine struct {
Config config.Interface
Logger *logrus.Entry
EventBus event_bus.Interface
Config config.Interface
Logger *logrus.Entry
EventBus event_bus.Interface
deviceRepo database.DatabaseRepository
RelatedVersions map[string]string //related versions metadata provided & embedded by the build process
}
func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
r := gin.New()
//setup database
deviceRepo, err := database.NewRepository(ae.Config, ae.Logger, ae.EventBus)
if err != nil {
panic(err)
}
ae.deviceRepo = deviceRepo
r.Use(middleware.LoggerMiddleware(ae.Logger))
r.Use(middleware.RepositoryMiddleware(ae.Config, ae.Logger, ae.EventBus))
r.Use(middleware.RepositoryMiddleware(ae.deviceRepo))
r.Use(middleware.ConfigMiddleware(ae.Config))
r.Use(middleware.EventBusMiddleware(ae.EventBus))
r.Use(gin.Recovery())
@ -172,6 +187,81 @@ func (ae *AppEngine) SetupEmbeddedFrontendRouting(embeddedAssetsFS embed.FS, bas
return router
}
func (ae *AppEngine) SetupInstallationRegistration() error {
//check if installation is already registered
systemSettings, err := ae.deviceRepo.LoadSystemSettings(context.Background())
if err != nil {
return fmt.Errorf("an error occurred while loading system settings: %s", err)
}
if systemSettings.InstallationID != "" && systemSettings.InstallationSecret != "" {
//already setup, exit
//TODO: future, update fasten-onprem, fasten-sources version
return nil
}
//setup the installation registration payload
registrationData := &models.InstallationRegistrationRequest{
SoftwareArchitecture: runtime.GOARCH,
SoftwareOS: runtime.GOOS,
}
if ae.RelatedVersions != nil {
if fastenSourcesVersion, fastenSourcesVersionOk := ae.RelatedVersions["sources"]; fastenSourcesVersionOk {
registrationData.FastenSourcesVersion = fastenSourcesVersion
}
if fastenOnpremVersion, fastenOnpremVersionOk := ae.RelatedVersions["onprem"]; fastenOnpremVersionOk {
registrationData.FastenOnpremVersion = fastenOnpremVersion
}
if fastenDesktopVersion, fastenDesktopVersionOk := ae.RelatedVersions["desktop"]; fastenDesktopVersionOk {
registrationData.FastenDesktopVersion = fastenDesktopVersion
}
}
//setup the http request
registrationDataJson, err := json.Marshal(registrationData)
if err != nil {
return fmt.Errorf("an error occurred while serializing installation registration data: %s", err)
}
//send the registration request
resp, err := http.Post(
"https://api.platform.fastenhealth.com/v1/installation/register",
"application/json",
bytes.NewBuffer(registrationDataJson),
)
if err != nil {
return fmt.Errorf("an error occurred while sending installation registration request: %s", err)
} else if resp.StatusCode != http.StatusOK {
return fmt.Errorf("an error occurred while sending installation registration request: %s", resp.Status)
}
defer resp.Body.Close()
//unmarshal the registration response
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("an error occurred while reading installation registration response: %s", err)
}
var registrationResponse models.ResponseWrapperTyped[models.InstallationRegistrationResponse]
err = json.Unmarshal(bodyBytes, &registrationResponse)
if err != nil {
return fmt.Errorf("an error occurred while unmarshalling installation registration response: %s", err)
}
//now that we have the registration response, store the registration data in the system settings
systemSettings.InstallationID = registrationResponse.Data.InstallationID
systemSettings.InstallationSecret = registrationResponse.Data.InstallationSecret
ae.Logger.Infof("Saving installation id to settings table: %s", systemSettings.InstallationID)
//save the system settings
err = ae.deviceRepo.SaveSystemSettings(context.Background(), systemSettings)
if err != nil {
return fmt.Errorf("an error occurred while saving system settings: %s", err)
}
return nil
}
func (ae *AppEngine) Start() error {
//set the gin mode
gin.SetMode(gin.ReleaseMode)
@ -180,6 +270,10 @@ func (ae *AppEngine) Start() error {
}
baseRouterGroup, ginRouter := ae.Setup()
err := ae.SetupInstallationRegistration()
if err != nil {
return err
}
r := ae.SetupFrontendRouting(baseRouterGroup, ginRouter)
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))

View File

@ -0,0 +1,19 @@
package resources
import (
_ "embed"
"encoding/json"
)
//go:embed related_versions.json
var relatedVersionsJson string
func GetRelatedVersions() (map[string]string, error) {
var relatedVersions map[string]string
err := json.Unmarshal([]byte(relatedVersionsJson), &relatedVersions)
if err != nil {
return nil, nil
}
return relatedVersions, nil
}

View File

@ -29,4 +29,6 @@ log:
level: INFO
jwt:
issuer:
# you should ABSOLUTELY change this value before deploying Fasten.
# TODO: in future versions, this will be generated on first run with a random value, and stored as a System Setting.
key: "thisismysupersecuressessionsecretlength"

1
go.mod
View File

@ -32,6 +32,7 @@ require (
github.com/urfave/cli/v2 v2.11.2
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
golang.org/x/mod v0.15.0
golang.org/x/net v0.17.0
gorm.io/datatypes v1.0.7
gorm.io/driver/sqlite v1.5.4

2
go.sum
View File

@ -497,6 +497,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -0,0 +1,97 @@
//go:generate go run related_versions.go
package main
import (
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
"golang.org/x/mod/modfile"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
const relativeToRepoRoot = "../../"
func main() {
log.Printf("generating related_versions.json file...")
relatedVersions, err := getRelatedVersions()
if err != nil {
log.Fatalf("could not get version info: %s", err)
}
//remove fasten- prefix from keys
newRelatedVersion := map[string]string{}
for k, v := range relatedVersions {
newRelatedVersion[strings.TrimPrefix(k, "fasten-")] = v
}
relatedVersionsJson, err := json.Marshal(newRelatedVersion)
if err != nil {
log.Fatalf("could not write related version info: %s", err)
}
err = os.WriteFile(filepath.Join(relativeToRepoRoot, "backend/resources", "related_versions.json"), relatedVersionsJson, 0644)
if err != nil {
log.Fatalf("could not write version info json: %s", err)
}
}
func getRelatedVersions() (map[string]string, error) {
goModBytes, err := ioutil.ReadFile(filepath.Join(relativeToRepoRoot, "go.mod"))
if err != nil {
return nil, fmt.Errorf("could not read go.mod file: %w", err)
}
modFile, err := modfile.Parse("go.mod", goModBytes, nil)
if err != nil {
return nil, fmt.Errorf("could not parse go.mod file: %w", err)
}
fastenOnpremVersion := version.VERSION
fastenSourcesVersion := findDependencyVersion("github.com/fastenhealth/fasten-sources", modFile)
return map[string]string{
"fasten-onprem": fastenOnpremVersion,
"fasten-sources": fastenSourcesVersion,
}, nil
}
func findDependencyVersion(modulePath string, modFile *modfile.File) string {
//check replacements first by iterating through the replace statements
for _, replace := range modFile.Replace {
if replace.Old.Path == modulePath {
if len(replace.New.Version) > 0 {
return replace.New.Version
} else {
relativeNewPath := filepath.Join(relativeToRepoRoot, replace.New.Path)
log.Printf("Attempting to get git version for %s", relativeNewPath)
//replace.New.Path is a relative path to the dependency directory
//use git describe --tags command
gitCommand := exec.Command("git", "describe", "--tags")
gitCommand.Dir = relativeNewPath
gitCommandOutput := new(strings.Builder)
gitCommand.Stdout = gitCommandOutput
gitCommand.Run()
return strings.TrimSpace(gitCommandOutput.String())
}
}
}
//find modulePath dependency in modFile Require
for _, require := range modFile.Require {
if require.Mod.Path == modulePath {
//strip "v" prefix from version
if strings.HasPrefix(require.Mod.Version, "v") {
return require.Mod.Version[1:]
} else {
return require.Mod.Version
}
}
}
return "unknown"
}