From 11064d9544eda11726da7d3c25b8f4e2be89a1e8 Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 11:11:58 -0500 Subject: [PATCH 1/6] lint: Improve time comparsion Signed-off-by: Andy Doan --- internal/app_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app_test.go b/internal/app_test.go index 5a1a6e7..0d25352 100644 --- a/internal/app_test.go +++ b/internal/app_test.go @@ -401,7 +401,7 @@ func TestCheckGood(t *testing.T) { assertFile(t, random, nil) if barChangedStat, err := os.Stat(barChanged); err != nil { t.Fatal(err) - } else if barChangedTime == barChangedStat.ModTime() { + } else if barChangedTime.Equal(barChangedStat.ModTime()) { t.Fatalf("A barChanged modstamp is ought to change") } }) From c5ea98916f45981e14d5eea9d416fce8b1daa5dd Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 11:12:26 -0500 Subject: [PATCH 2/6] lint: Simplify construction of object Signed-off-by: Andy Doan --- internal/rotate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/rotate.go b/internal/rotate.go index d1ee18c..5e96234 100644 --- a/internal/rotate.go +++ b/internal/rotate.go @@ -38,7 +38,7 @@ func NewCertRotationHandler(app *App, stateFile, estServer string) *CertRotation state := &CertRotationState{EstServer: estServer} return &CertRotationHandler{ stateHandler[*CertRotationState]{ - stateContext: newStateContext[*CertRotationState](app, stateFile, state), + stateContext: newStateContext(app, stateFile, state), steps: []certRotationStep{ estStep{}, lockStep{}, From 7fe60e3cce707032634cbd2d4aa21589fb243ff4 Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 11:44:21 -0500 Subject: [PATCH 3/6] app: change secrets_dir to secretsDir Signed-off-by: Andy Doan --- internal/app.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app.go b/internal/app.go index ad9bba1..0dd7a57 100644 --- a/internal/app.go +++ b/internal/app.go @@ -61,7 +61,7 @@ func createClient(cfg *sotatoml.AppConfig) (*http.Client, CryptoHandler) { return client, NewEciesPkcs11Handler(extra, tlsCfg.Certificates[0].PrivateKey) } -func NewApp(configPaths []string, secrets_dir string, unsafeHandlers, testing bool) (*App, error) { +func NewApp(configPaths []string, secretsDir string, unsafeHandlers, testing bool) (*App, error) { if len(configPaths) == 0 { configPaths = sotatoml.DEF_CONFIG_ORDER } @@ -84,7 +84,7 @@ func NewApp(configPaths []string, secrets_dir string, unsafeHandlers, testing bo app := App{ StorageDir: storagePath, EncryptedConfig: filepath.Join(storagePath, "config.encrypted"), - SecretsDir: secrets_dir, + SecretsDir: secretsDir, configUrl: url, configPaths: configPaths, sota: sota, From 9d569a11680e1479b27be1f292384bd86ba2bfb9 Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 11:47:52 -0500 Subject: [PATCH 4/6] sotatoml: Provide method for find config path search order This allows a caller of the API to understand what config paths are being used. Signed-off-by: Andy Doan --- internal/app.go | 4 +--- sotatoml/app_config.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/app.go b/internal/app.go index 0dd7a57..f7a61a1 100644 --- a/internal/app.go +++ b/internal/app.go @@ -37,7 +37,6 @@ type App struct { SecretsDir string configUrl string - configPaths []string unsafeHandlers bool sota *sotatoml.AppConfig @@ -86,7 +85,6 @@ func NewApp(configPaths []string, secretsDir string, unsafeHandlers, testing boo EncryptedConfig: filepath.Join(storagePath, "config.encrypted"), SecretsDir: secretsDir, configUrl: url, - configPaths: configPaths, sota: sota, unsafeHandlers: unsafeHandlers, exitFunc: os.Exit, @@ -172,7 +170,7 @@ func (a *App) runOnChanged(fname string, fullpath string, onChanged []string) { cmd := exec.Command(onChanged[0], onChanged[1:]...) cmd.Env = append(os.Environ(), "CONFIG_FILE="+fullpath) cmd.Env = append(cmd.Env, "STORAGE_DIR="+a.StorageDir) - cmd.Env = append(cmd.Env, "SOTA_DIR="+strings.Join(a.configPaths, ",")) + cmd.Env = append(cmd.Env, "SOTA_DIR="+strings.Join(a.sota.SearchPaths(), ",")) cmd.Env = append(cmd.Env, "FIOCONFIG_BIN="+path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/sotatoml/app_config.go b/sotatoml/app_config.go index 65ebb2f..f9f9a7a 100644 --- a/sotatoml/app_config.go +++ b/sotatoml/app_config.go @@ -28,7 +28,8 @@ type cfgFile struct { } type AppConfig struct { - cfgs []*cfgFile + searchPaths []string + cfgs []*cfgFile } // NewAppConfig parses config files as per: @@ -70,10 +71,15 @@ func NewAppConfig(configPaths []string) (*AppConfig, error) { } } - cfg := AppConfig{cfgs: keys} + cfg := AppConfig{cfgs: keys, searchPaths: configPaths} return &cfg, nil } +// SearchPaths returns the list of config paths used to build this AppConfig +func (c AppConfig) SearchPaths() []string { + return c.searchPaths +} + func (c AppConfig) CombinedConfig() (*toml.Tree, error) { var combined map[string]any for i := len(c.cfgs) - 1; i >= 0; i-- { From 2e66569db0b3d7312854dc34e31a43e5b31a1c4e Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 11:52:47 -0500 Subject: [PATCH 5/6] internal: Add new api for creating App with sotatoml already parsed Signed-off-by: Andy Doan --- internal/app.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/app.go b/internal/app.go index f7a61a1..fbc1718 100644 --- a/internal/app.go +++ b/internal/app.go @@ -68,6 +68,10 @@ func NewApp(configPaths []string, secretsDir string, unsafeHandlers, testing boo if err != nil { return nil, fmt.Errorf("unable to parse sota.toml: %w", err) } + return NewAppWithConfig(sota, secretsDir, unsafeHandlers, testing) +} + +func NewAppWithConfig(sota *sotatoml.AppConfig, secretsDir string, unsafeHandlers, testing bool) (*App, error) { // Assert we have a sane configuration _, crypto := createClient(sota) crypto.Close() From ae762fe0bc2c7b047e30462137af217e5d843bd5 Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Mon, 20 Oct 2025 13:13:25 -0500 Subject: [PATCH 6/6] Create an API for external golang apps This exposes the minimum surface area of Fioconfig while avoiding some of the more complex things like certificate rotation which aren't supported in Fioup or the community edition of FoundriesFactory. Signed-off-by: Andy Doan --- app/api.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/api.go diff --git a/app/api.go b/app/api.go new file mode 100644 index 0000000..8f5ddc3 --- /dev/null +++ b/app/api.go @@ -0,0 +1,42 @@ +package app + +import ( + "github.com/foundriesio/fioconfig/internal" + "github.com/foundriesio/fioconfig/sotatoml" +) + +type App internal.App + +var NotModifiedError = internal.NotModifiedError + +// NewAppWithConfig creates a new App instance with the provided SOTA configuration +// that can perform all the basic Fioconfig operations. +func NewAppWithConfig(sota *sotatoml.AppConfig, secretsDir string, unsafeHandlers bool) (*App, error) { + app, err := internal.NewAppWithConfig(sota, secretsDir, unsafeHandlers, false) + if err != nil { + return nil, err + } + return (*App)(app), nil +} + +// Extract extracts secrets from the encrypted configuration into the secrets directory. +// must be called once. A common way to do this is a systemd "oneshot" service after +// NetworkManager is up. +func (a *App) Extract() error { + return (*internal.App)(a).Extract() +} + +// CheckIn checks with the device gateway for the lastest configuration. If there +// are changes, it applies them locally. If the config on the server is unchanged, +// it returns NotModifiedError. This function also will try and run any pending +// "init" functions required by Fioconfig that have not yet been run. +func (a *App) CheckIn() error { + return (*internal.App)(a).CheckIn() +} + +// RunAndReport runs a command specified by name with args, and collects +// artifacts found under artifactsDir. It reports the results back to the +// device-gateway's fiotest API under the test identified by testId. +func (a *App) RunAndReport(name, testId, artifactsDir string, args []string) error { + return (*internal.App)(a).RunAndReport(name, testId, artifactsDir, args) +}