Skip to content

Commit

Permalink
Implement auto updating
Browse files Browse the repository at this point in the history
  • Loading branch information
pjcdawkins committed Jan 1, 2025
1 parent c45ffe1 commit c9a537e
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 5 deletions.
7 changes: 7 additions & 0 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
updateMessageChan <- rel
}()
}
if alt.ShouldAutoUpdate(cnf) {
go func() {
if err := alt.AutoUpdate(cmd.Context(), cnf, debugLog); err != nil {
cmd.PrintErrln("Error auto-updating config:", err)
}
}()
}
},
Run: func(cmd *cobra.Command, _ []string) {
c := &legacy.CLIWrapper{
Expand Down
File renamed without changes.
File renamed without changes.
111 changes: 111 additions & 0 deletions internal/config/alt/auto_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package alt

import (
"context"
"fmt"
"os"
"time"

"github.com/gofrs/flock"
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/state"
)

func ShouldAutoUpdate(cnf *config.Config) bool {
if !cnf.Updates.Check {
return false
}
src := config.GetSource()
if src.Type != config.SourceTypeFile {
return false
}
cnfPath := src.Value
if cnfPath == "" {
return false
}
if cnf.Metadata.URL == "" {
return false
}
return true
}

const defaultUpdateInterval = 21600 // 6 hours

func AutoUpdate(ctx context.Context, cnf *config.Config, debugLog func(fmt string, i ...any)) error {
s, err := state.Load(cnf)
interval := cnf.Metadata.UpdateInterval
if interval == 0 {
interval = defaultUpdateInterval
}
if err == nil && int(time.Now().Unix()-s.ConfigUpdates.LastChecked) < interval {
return nil
}

defer func() {
s.ConfigUpdates.LastChecked = time.Now().Unix()
if err := state.Save(s, cnf); err != nil {
debugLog("Error saving state: %s", err)
}
}()

src := config.GetSource()
if src.Type != config.SourceTypeFile || src.Value == "" {
return fmt.Errorf("no config file path available")
}
if cnf.Metadata.URL == "" {
return fmt.Errorf("no config URL available")
}
cnfPath := src.Value

lock := flock.New(cnfPath)
locked, err := lock.TryLock()
if err != nil {
return fmt.Errorf("failed to lock configuration file: %w", err)
}
defer func() {
if err := lock.Unlock(); err != nil {
debugLog("Failed to unlock configuration file: %s", err)
}
}()
if !locked {
debugLog("Not updating configuration: could not lock file: %s", cnfPath)
return nil
}

f, err := os.OpenFile(cnfPath, os.O_WRONLY, 0600)

Check failure on line 77 in internal/config/alt/auto_update.go

View workflow job for this annotation

GitHub Actions / test

octalLiteral: use new octal literal style, 0o600 (gocritic)
if err != nil {
return fmt.Errorf("failed to open config file for writing: %w", err)
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return fmt.Errorf("could not stat config file %s: %w", cnfPath, err)
}
if time.Since(stat.ModTime()) < time.Duration(interval)*time.Second {
debugLog("Config file updated recently: %s", cnfPath)
return nil
}

debugLog("Checking for configuration updates from URL: %s", cnf.Metadata.URL)
newCnfNode, newCnfStruct, err := FetchConfig(ctx, cnf.Metadata.URL)
if err != nil {
return err
}
if !cnf.Metadata.UpdatedAt.IsZero() && !newCnfStruct.Metadata.UpdatedAt.IsZero() &&
cnf.Metadata.UpdatedAt.After(newCnfStruct.Metadata.UpdatedAt) {
debugLog("Skipping auto update: current config updated after remote config")
return nil
}
b, err := yaml.Marshal(newCnfNode)
if err != nil {
return err
}
if _, err := f.Write(b); err != nil {
return err
}
debugLog("Automatically updated config file: %s", cnfPath)

return nil
}
31 changes: 31 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,46 @@ import (
"gopkg.in/yaml.v3"
)

const (
SourceTypeFile = "file"
SourceTypeEmbedded = "embedded"
)

// Source represents the source configuration and information about where it comes from.
type Source struct {
Type string
Value string
Content []byte
}

var source *Source

// GetSource returns configuration source information.
func GetSource() *Source {
if source == nil {
_, _ = LoadYAML()
}
return source
}

// LoadYAML reads the configuration file from the environment if specified, falling back to the embedded file.
func LoadYAML() ([]byte, error) {
if source != nil {
return source.Content, nil
}
source = &Source{}
if path := os.Getenv("CLI_CONFIG_FILE"); path != "" {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
source.Content = b
source.Type = SourceTypeFile
source.Value = path
return b, nil
}
source.Type = SourceTypeEmbedded
source.Content = embedded
return embedded, nil
}

Expand Down
9 changes: 5 additions & 4 deletions internal/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ type Config struct {

// Metadata defines information about the config itself.
type Metadata struct {
Version string `validate:"omitempty" yaml:"version,omitempty"`
UpdatedAt time.Time `validate:"omitempty" yaml:"updated_at,omitempty"`
DownloadedAt time.Time `validate:"omitempty" yaml:"downloaded_at,omitempty"`
URL string `validate:"omitempty,url" yaml:"url,omitempty"`
Version string `validate:"omitempty" yaml:"version,omitempty"`
UpdatedAt time.Time `validate:"omitempty" yaml:"updated_at,omitempty"`
DownloadedAt time.Time `validate:"omitempty" yaml:"downloaded_at,omitempty"`
URL string `validate:"omitempty,url" yaml:"url,omitempty"`
UpdateInterval int `validate:"omitempty" yaml:"update_interval,omitempty"`
}

// applyDefaults applies defaults to config before parsing.
Expand Down
6 changes: 5 additions & 1 deletion internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import (
type State struct {
Updates struct {
LastChecked int64 `json:"last_checked"`
} `json:"updates"`
} `json:"updates,omitempty"`

ConfigUpdates struct {
LastChecked int64 `json:"last_checked"`
} `json:"config_updates,omitempty"`
}

// Load reads state from the filesystem.
Expand Down

0 comments on commit c9a537e

Please sign in to comment.