Skip to content

Commit

Permalink
pkg/asset: Save/Load functinality for assets into a state file
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Rajat Chopra committed Oct 12, 2018
1 parent 6d9337d commit 971eea9
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 5 deletions.
1 change: 0 additions & 1 deletion cmd/openshift-install/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions cmd/openshift-install/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")

This comment has been minimized.

Copy link
@abhinavdahiya

abhinavdahiya Oct 12, 2018

Contributor

Just return errors.Wrap

return err
}
return nil
}
}
80 changes: 76 additions & 4 deletions pkg/asset/store.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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)]
Expand All @@ -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())

This comment has been minimized.

Copy link
@abhinavdahiya

abhinavdahiya Oct 12, 2018

Contributor

No need to say Asset not found in state file.

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)
Expand Down

0 comments on commit 971eea9

Please sign in to comment.