diff --git a/main.go b/main.go index 7a4201030..c1feb49d1 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,20 @@ package main import ( "context" "flag" + "io/ioutil" "log" "net" "net/http" "os" "os/signal" + "path/filepath" "osbuild-composer/rpmmd" "osbuild-composer/weldr" ) +const StateFile = "/var/lib/osbuild-composer/weldr-state.json" + func main() { var verbose bool flag.BoolVar(&verbose, "v", false, "Print access log") @@ -44,9 +48,28 @@ func main() { logger = log.New(os.Stdout, "", 0) } - api := weldr.New(repo, packages, logger) - server := http.Server{Handler: api} + err = os.MkdirAll("/var/lib/osbuild-composer", 0755) + if err != nil { + panic(err) + } + state, err := ioutil.ReadFile(StateFile) + if err != nil && !os.IsNotExist(err) { + log.Fatalf("cannot read state: %v", err) + } + + stateChannel := make(chan []byte, 10) + api := weldr.New(repo, packages, logger, state, stateChannel) + go func() { + for { + err := writeFileAtomically(StateFile, <-stateChannel, 0755) + if err != nil { + log.Fatalf("cannot write state: %v", err) + } + } + }() + + server := http.Server{Handler: api} shutdownDone := make(chan struct{}, 1) go func() { channel := make(chan os.Signal, 1) @@ -63,3 +86,37 @@ func main() { <-shutdownDone } + +func writeFileAtomically(filename string, data []byte, mode os.FileMode) error { + dir, name := filepath.Dir(filename), filepath.Base(filename) + + tmpfile, err := ioutil.TempFile(dir, name + "-*.tmp") + if err != nil { + return err + } + + _, err = tmpfile.Write(data) + if err != nil { + os.Remove(tmpfile.Name()) + return err + } + + err = tmpfile.Chmod(mode) + if err != nil { + return err + } + + err = tmpfile.Close() + if err != nil { + os.Remove(tmpfile.Name()) + return err + } + + err = os.Rename(tmpfile.Name(), filename) + if err != nil { + os.Remove(tmpfile.Name()) + return err + } + + return nil +} diff --git a/weldr/api.go b/weldr/api.go index 1631a5763..587a0e936 100644 --- a/weldr/api.go +++ b/weldr/api.go @@ -22,22 +22,24 @@ type API struct { router *httprouter.Router } -func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger) *API { +func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger, initialState []byte, stateChannel chan<- []byte) *API { api := &API{ - store: newStore(), + store: newStore(initialState, stateChannel), repo: repo, packages: packages, logger: logger, } - // sample blueprint - api.store.pushBlueprint(blueprint{ - Name: "example", - Description: "An Example", - Version: "1", - Packages: []blueprintPackage{{"httpd", "2.*"}}, - Modules: []blueprintPackage{}, - }) + // sample blueprint on first run + if initialState == nil { + api.store.pushBlueprint(blueprint{ + Name: "example", + Description: "An Example", + Version: "1", + Packages: []blueprintPackage{{"httpd", "2.*"}}, + Modules: []blueprintPackage{}, + }) + } api.router = httprouter.New() api.router.RedirectTrailingSlash = false diff --git a/weldr/store.go b/weldr/store.go index 3b146ab12..453ba9b5d 100644 --- a/weldr/store.go +++ b/weldr/store.go @@ -1,6 +1,8 @@ package weldr import ( + "log" + "encoding/json" "sort" "sync" ) @@ -9,7 +11,8 @@ type store struct { Blueprints map[string]blueprint `json:"blueprints"` Workspace map[string]blueprint `json:"workspace"` - mu sync.RWMutex // protects all fields + mu sync.RWMutex // protects all fields + stateChannel chan<- []byte } type blueprint struct { @@ -25,10 +28,41 @@ type blueprintPackage struct { Version string `json:"version,omitempty"` } -func newStore() *store { - return &store{ - Blueprints: make(map[string]blueprint), - Workspace: make(map[string]blueprint), +func newStore(initialState []byte, stateChannel chan<- []byte) *store { + var s store + + if initialState != nil { + err := json.Unmarshal(initialState, &s) + if err != nil { + log.Fatalf("invalid initial state: %v", err) + } + } + + if s.Blueprints == nil { + s.Blueprints = make(map[string]blueprint) + } + if s.Workspace == nil { + s.Workspace = make(map[string]blueprint) + } + s.stateChannel = stateChannel + + return &s +} + +func (s *store) change(f func()) { + s.mu.Lock() + defer s.mu.Unlock() + + f() + + if s.stateChannel != nil { + serialized, err := json.Marshal(s) + if err != nil { + // we ought to know all types that go into the store + panic(err) + } + + s.stateChannel <- serialized } } @@ -75,31 +109,27 @@ func (s *store) getBlueprint(name string, bp *blueprint, changed *bool) bool { } func (s *store) pushBlueprint(bp blueprint) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.Workspace, bp.Name) - s.Blueprints[bp.Name] = bp + s.change(func() { + delete(s.Workspace, bp.Name) + s.Blueprints[bp.Name] = bp + }) } func (s *store) pushBlueprintToWorkspace(bp blueprint) { - s.mu.Lock() - defer s.mu.Unlock() - - s.Workspace[bp.Name] = bp + s.change(func() { + s.Workspace[bp.Name] = bp + }) } func (s *store) deleteBlueprint(name string) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.Workspace, name) - delete(s.Blueprints, name) + s.change(func() { + delete(s.Workspace, name) + delete(s.Blueprints, name) + }) } func (s *store) deleteBlueprintFromWorkspace(name string) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.Workspace, name) + s.change(func() { + delete(s.Workspace, name) + }) }