From 971eea9f865c2adee5652ef23dc81c1403099585 Mon Sep 17 00:00:00 2001 From: Rajat Chopra Date: Thu, 11 Oct 2018 16:19:05 -0400 Subject: [PATCH] pkg/asset: Save/Load functinality for assets into a state file 1. Calls out on the Save function to persist the state file of all assets in a constant state file 2. Calls out on the Load function to partially load the contents of the state file into a new field in StoreImpl 3. In Save, marshaling as done as a map of type.String() to asset bytes: i.e. stateMap[reflect.TypeOf(assetObject).String()] = marshalledBytes(assetObject) reflect.TypeOf().String() is used as against reflect.Type.Name() function because Name returns the type name only, without scoping the package path. See the implementation of type.Name() function where type.String() is used within: https://golang.org/src/reflect/type.go?#L874 4. Support for 'deferred unmarshal' for assets from state file: Before a target is worked upon, the state file is loaded into the memory as partial asset state map. The key of the map is the string representation of the asset type and the value is raw bytes that are left as is. The idea is that 'fetch' will finally get the asset from the state file, only when needed. A utility function GetStateAsset has been provided to allow for deferred unmarshaling. See example code to use the util function (as in the store's fetch function): ``` func (s *StoreImpl) fetch(asset Asset, indent string) error { ... ... ok, err := s.GetStateAsset(asset) if err != nil { return errors.Wrapf("failed to unmarshal asset from state file: %v. Remove the state file and continue..", err) } if ok { logrus.Debugf("%sAsset found in state file %v", indent, asset) if s.assets == nil { s.assets = make(map[reflect.Type]Asset) } s.assets[reflect.TypeOf(asset)] = asset return nil } ... ... ``` Alternatively, instead of passing the empty asset object, one can make a copy of the asset object and render it with contents from the state file: ``` newAsset := reflect.ValueOf(reflect.New(reflect.TypeOf(asset))).Elem().Interface() ok, err := s.GetStateAsset(newAsset) // now compare newAsset with asset itself ... // and set the contents of asset from newAsset if needed: reflect.ValueOf(asset).Elem().Set(reflect.ValueOf(newAsset).Elem()) ``` Other notes: The utility function GetStateAsset used in this commit such that if an asset is found in the state file, then its used directly. Further work will need to modify this behaviour so that a three way merge can happen between an asset found in the state file, found on disk, rendered by the Generate function. --- cmd/openshift-install/main.go | 1 - cmd/openshift-install/targets.go | 9 ++++ pkg/asset/store.go | 80 ++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/cmd/openshift-install/main.go b/cmd/openshift-install/main.go index deb87798867..eb30a265032 100644 --- a/cmd/openshift-install/main.go +++ b/cmd/openshift-install/main.go @@ -57,7 +57,6 @@ func runRootCmd(cmd *cobra.Command, args []string) error { level, err := logrus.ParseLevel(rootOpts.logLevel) if err != nil { return errors.Wrap(err, "invalid log-level") - } logrus.SetLevel(level) return nil diff --git a/cmd/openshift-install/targets.go b/cmd/openshift-install/targets.go index 4ed0c9db380..f8c70e81a9d 100644 --- a/cmd/openshift-install/targets.go +++ b/cmd/openshift-install/targets.go @@ -69,6 +69,10 @@ func newTargetsCmd() []*cobra.Command { func runTargetCmd(targets ...asset.WritableAsset) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { assetStore := &asset.StoreImpl{} + err := assetStore.Load(rootOpts.dir) + if err != nil { + logrus.Errorf("Could not load assets from state file: %v", err) + } for _, a := range targets { err := assetStore.Fetch(a) if err != nil { @@ -91,6 +95,11 @@ func runTargetCmd(targets ...asset.WritableAsset) func(cmd *cobra.Command, args return err } } + err = assetStore.Save(rootOpts.dir) + if err != nil { + errors.Wrapf(err, "failed to write to state file") + return err + } return nil } } diff --git a/pkg/asset/store.go b/pkg/asset/store.go index 7de7930fa04..c9b63be5ad6 100644 --- a/pkg/asset/store.go +++ b/pkg/asset/store.go @@ -1,12 +1,20 @@ package asset import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" "reflect" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +const ( + stateFileName = ".openshift_install_state.json" +) + // Store is a store for the states of assets. type Store interface { // Fetch retrieves the state of the given asset, generating it and its @@ -16,7 +24,8 @@ type Store interface { // StoreImpl is the implementation of Store. type StoreImpl struct { - assets map[reflect.Type]Asset + assets map[reflect.Type]Asset + stateFileAssets map[string]json.RawMessage } // Fetch retrieves the state of the given asset, generating it and its @@ -25,6 +34,58 @@ func (s *StoreImpl) Fetch(asset Asset) error { return s.fetch(asset, "") } +// Load retrieves the state from the state file present in the given directory +// and returns the assets map +func (s *StoreImpl) Load(dir string) error { + path := filepath.Join(dir, stateFileName) + assets := make(map[string]json.RawMessage) + data, err := ioutil.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + err = json.Unmarshal(data, &assets) + if err != nil { + return errors.Wrapf(err, "failed to unmarshal state file %s", path) + } + s.stateFileAssets = assets + return nil +} + +// GetStateAsset renders the asset object arguments from the state file contents +// also returns a boolean indicating whether the object was found in the state file or not +func (s *StoreImpl) GetStateAsset(asset Asset) (bool, error) { + bytes, ok := s.stateFileAssets[reflect.TypeOf(asset).String()] + if !ok { + return false, nil + } + err := json.Unmarshal(bytes, asset) + return true, err +} + +// Save dumps the entire state map into a file +func (s *StoreImpl) Save(dir string) error { + assetMap := make(map[string]Asset) + for k, v := range s.assets { + assetMap[k.String()] = v + } + data, err := json.MarshalIndent(&assetMap, "", " ") + if err != nil { + return err + } + + path := filepath.Join(dir, stateFileName) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + if err := ioutil.WriteFile(path, data, 0644); err != nil { + return err + } + return nil +} + func (s *StoreImpl) fetch(asset Asset, indent string) error { logrus.Debugf("%sFetching %s...", indent, asset.Name()) storedAsset, ok := s.assets[reflect.TypeOf(asset)] @@ -47,10 +108,21 @@ func (s *StoreImpl) fetch(asset Asset, indent string) error { parents.Add(d) } - logrus.Debugf("%sGenerating %s...", indent, asset.Name()) - err := asset.Generate(parents) + // Before generating the asset, look if we have it all ready in the state file + // if yes, then use it instead + logrus.Debugf("%sLooking up asset from state file: %s", indent, reflect.TypeOf(asset).String()) + ok, err := s.GetStateAsset(asset) if err != nil { - return errors.Wrapf(err, "failed to generate asset %s", asset.Name()) + return errors.Wrapf(err, "failed to unmarshal asset '%s' from state file '%s'", asset.Name(), stateFileName) + } + if ok { + logrus.Debugf("%sAsset found in state file", indent) + } else { + logrus.Debugf("%sAsset not found in state file. Generating %s...", indent, asset.Name()) + err := asset.Generate(parents) + if err != nil { + return errors.Wrapf(err, "failed to generate asset %s", asset.Name()) + } } if s.assets == nil { s.assets = make(map[reflect.Type]Asset)