github.com/openshift/installer@v1.4.17/pkg/asset/store/store.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"os"
     7  	"path/filepath"
     8  	"reflect"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/sirupsen/logrus"
    12  
    13  	"github.com/openshift/installer/pkg/asset"
    14  )
    15  
    16  const (
    17  	stateFileName = ".openshift_install_state.json"
    18  )
    19  
    20  // assetSource indicates from where the asset was fetched
    21  type assetSource int
    22  
    23  const (
    24  	// unsourced indicates that the asset has not been fetched
    25  	unfetched assetSource = iota
    26  	// generatedSource indicates that the asset was generated
    27  	generatedSource
    28  	// onDiskSource indicates that the asset was fetched from disk
    29  	onDiskSource
    30  	// stateFileSource indicates that the asset was fetched from the state file
    31  	stateFileSource
    32  )
    33  
    34  type assetState struct {
    35  	// asset is the asset.
    36  	// If the asset has not been fetched, then this will be nil.
    37  	asset asset.Asset
    38  	// source is the source from which the asset was fetched
    39  	source assetSource
    40  	// anyParentsDirty is true if any of the parents of the asset are dirty
    41  	anyParentsDirty bool
    42  	// presentOnDisk is true if the asset in on-disk. This is set whether the
    43  	// asset is sourced from on-disk or not. It is used in purging consumed assets.
    44  	presentOnDisk bool
    45  }
    46  
    47  // storeImpl is the implementation of Store.
    48  type storeImpl struct {
    49  	directory       string
    50  	assets          map[reflect.Type]*assetState
    51  	stateFileAssets map[string]json.RawMessage
    52  	fileFetcher     asset.FileFetcher
    53  }
    54  
    55  // NewStore returns an asset store that implements the asset.Store interface.
    56  func NewStore(dir string) (asset.Store, error) {
    57  	return newStore(dir)
    58  }
    59  
    60  func newStore(dir string) (*storeImpl, error) {
    61  	store := &storeImpl{
    62  		directory:   dir,
    63  		fileFetcher: &fileFetcher{directory: dir},
    64  		assets:      map[reflect.Type]*assetState{},
    65  	}
    66  
    67  	if err := store.loadStateFile(); err != nil {
    68  		return nil, err
    69  	}
    70  	return store, nil
    71  }
    72  
    73  // Fetch retrieves the state of the given asset, generating it and its
    74  // dependencies if necessary. When purging consumed assets, none of the
    75  // assets in preserved will be purged.
    76  func (s *storeImpl) Fetch(ctx context.Context, a asset.Asset, preserved ...asset.WritableAsset) error {
    77  	if err := s.fetch(ctx, a, ""); err != nil {
    78  		return err
    79  	}
    80  	if err := s.saveStateFile(); err != nil {
    81  		return errors.Wrap(err, "failed to save state")
    82  	}
    83  	if wa, ok := a.(asset.WritableAsset); ok {
    84  		return errors.Wrap(s.purge(append(preserved, wa)), "failed to purge asset")
    85  	}
    86  	return nil
    87  }
    88  
    89  // Destroy removes the asset from all its internal state and also from
    90  // disk if possible.
    91  func (s *storeImpl) Destroy(a asset.Asset) error {
    92  	if sa, ok := s.assets[reflect.TypeOf(a)]; ok {
    93  		reflect.ValueOf(a).Elem().Set(reflect.ValueOf(sa.asset).Elem())
    94  	} else if s.isAssetInState(a) {
    95  		if err := s.loadAssetFromState(a); err != nil {
    96  			return err
    97  		}
    98  	} else {
    99  		// nothing to do
   100  		return nil
   101  	}
   102  
   103  	if wa, ok := a.(asset.WritableAsset); ok {
   104  		if err := asset.DeleteAssetFromDisk(wa, s.directory); err != nil {
   105  			return err
   106  		}
   107  	}
   108  
   109  	delete(s.assets, reflect.TypeOf(a))
   110  	delete(s.stateFileAssets, reflect.TypeOf(a).String())
   111  	return s.saveStateFile()
   112  }
   113  
   114  // DestroyState removes the state file from disk
   115  func (s *storeImpl) DestroyState() error {
   116  	s.stateFileAssets = nil
   117  	path := filepath.Join(s.directory, stateFileName)
   118  	err := os.Remove(path)
   119  	if err != nil {
   120  		if os.IsNotExist(err) {
   121  			return nil
   122  		}
   123  		return err
   124  	}
   125  	return nil
   126  }
   127  
   128  // loadStateFile retrieves the state from the state file present in the given directory
   129  // and returns the assets map
   130  func (s *storeImpl) loadStateFile() error {
   131  	path := filepath.Join(s.directory, stateFileName)
   132  	assets := map[string]json.RawMessage{}
   133  	data, err := os.ReadFile(path)
   134  	if err != nil {
   135  		if os.IsNotExist(err) {
   136  			return nil
   137  		}
   138  		return err
   139  	}
   140  	err = json.Unmarshal(data, &assets)
   141  	if err != nil {
   142  		return errors.Wrapf(err, "failed to unmarshal state file %q", path)
   143  	}
   144  	s.stateFileAssets = assets
   145  	return nil
   146  }
   147  
   148  // loadAssetFromState renders the asset object arguments from the state file contents.
   149  func (s *storeImpl) loadAssetFromState(a asset.Asset) error {
   150  	bytes, ok := s.stateFileAssets[reflect.TypeOf(a).String()]
   151  	if !ok {
   152  		return errors.Errorf("asset %q is not found in the state file", a.Name())
   153  	}
   154  	return json.Unmarshal(bytes, a)
   155  }
   156  
   157  // isAssetInState tests whether the asset is in the state file.
   158  func (s *storeImpl) isAssetInState(a asset.Asset) bool {
   159  	_, ok := s.stateFileAssets[reflect.TypeOf(a).String()]
   160  	return ok
   161  }
   162  
   163  // saveStateFile dumps the entire state map into a file
   164  func (s *storeImpl) saveStateFile() error {
   165  	if s.stateFileAssets == nil {
   166  		s.stateFileAssets = map[string]json.RawMessage{}
   167  	}
   168  	for k, v := range s.assets {
   169  		if v.source == unfetched {
   170  			continue
   171  		}
   172  		data, err := json.MarshalIndent(v.asset, "", "    ")
   173  		if err != nil {
   174  			return err
   175  		}
   176  		s.stateFileAssets[k.String()] = json.RawMessage(data)
   177  	}
   178  	data, err := json.MarshalIndent(s.stateFileAssets, "", "    ")
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	path := filepath.Join(s.directory, stateFileName)
   184  	if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
   185  		return err
   186  	}
   187  	if err := os.WriteFile(path, data, 0o640); err != nil { //nolint:gosec // no sensitive info
   188  		return err
   189  	}
   190  	return nil
   191  }
   192  
   193  // fetch populates the given asset, generating it and its dependencies if
   194  // necessary, and returns whether or not the asset had to be regenerated and
   195  // any errors.
   196  func (s *storeImpl) fetch(ctx context.Context, a asset.Asset, indent string) error {
   197  	logrus.Debugf("%sFetching %s...", indent, a.Name())
   198  
   199  	assetState, ok := s.assets[reflect.TypeOf(a)]
   200  	if !ok {
   201  		if _, err := s.load(a, ""); err != nil {
   202  			return err
   203  		}
   204  		assetState = s.assets[reflect.TypeOf(a)]
   205  	}
   206  
   207  	// Return immediately if the asset has been fetched before,
   208  	// this is because we are doing a depth-first-search, it's guaranteed
   209  	// that we always fetch the parent before children, so we don't need
   210  	// to worry about invalidating anything in the cache.
   211  	if assetState.source != unfetched {
   212  		logrus.Debugf("%sReusing previously-fetched %s", indent, a.Name())
   213  		reflect.ValueOf(a).Elem().Set(reflect.ValueOf(assetState.asset).Elem())
   214  		return nil
   215  	}
   216  
   217  	// Re-generate the asset
   218  	dependencies := a.Dependencies()
   219  	parents := make(asset.Parents, len(dependencies))
   220  	for _, d := range dependencies {
   221  		if err := s.fetch(ctx, d, increaseIndent(indent)); err != nil {
   222  			return errors.Wrapf(err, "failed to fetch dependency of %q", a.Name())
   223  		}
   224  		parents.Add(d)
   225  	}
   226  	logrus.Debugf("%sGenerating %s...", indent, a.Name())
   227  	if err := a.Generate(ctx, parents); err != nil {
   228  		return errors.Wrapf(err, "failed to generate asset %q", a.Name())
   229  	}
   230  	assetState.asset = a
   231  	assetState.source = generatedSource
   232  	return nil
   233  }
   234  
   235  // load loads the asset and all of its ancestors from on-disk and the state file.
   236  func (s *storeImpl) load(a asset.Asset, indent string) (*assetState, error) {
   237  	logrus.Debugf("%sLoading %s...", indent, a.Name())
   238  
   239  	// Stop descent if the asset has already been loaded.
   240  	if state, ok := s.assets[reflect.TypeOf(a)]; ok {
   241  		return state, nil
   242  	}
   243  
   244  	// Load dependencies from on-disk.
   245  	anyParentsDirty := false
   246  	for _, d := range a.Dependencies() {
   247  		state, err := s.load(d, increaseIndent(indent))
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  		if state.anyParentsDirty || state.source == onDiskSource {
   252  			anyParentsDirty = true
   253  		}
   254  	}
   255  
   256  	// Try to load from on-disk.
   257  	var (
   258  		onDiskAsset asset.WritableAsset
   259  		foundOnDisk bool
   260  	)
   261  	if _, isWritable := a.(asset.WritableAsset); isWritable {
   262  		onDiskAsset = reflect.New(reflect.TypeOf(a).Elem()).Interface().(asset.WritableAsset)
   263  		var err error
   264  		foundOnDisk, err = onDiskAsset.Load(s.fileFetcher)
   265  		if err != nil {
   266  			return nil, errors.Wrapf(err, "failed to load asset %q", a.Name())
   267  		}
   268  	}
   269  
   270  	// Try to load from state file.
   271  	var (
   272  		stateFileAsset         asset.Asset
   273  		foundInStateFile       bool
   274  		onDiskMatchesStateFile bool
   275  	)
   276  	// Do not need to bother with loading from state file if any of the parents
   277  	// are dirty because the asset must be re-generated in this case.
   278  	if !anyParentsDirty {
   279  		foundInStateFile = s.isAssetInState(a)
   280  		if foundInStateFile {
   281  			stateFileAsset = reflect.New(reflect.TypeOf(a).Elem()).Interface().(asset.Asset)
   282  			if err := s.loadAssetFromState(stateFileAsset); err != nil {
   283  				return nil, errors.Wrapf(err, "failed to load asset %q from state file", a.Name())
   284  			}
   285  		}
   286  
   287  		if foundOnDisk && foundInStateFile {
   288  			logrus.Debugf("%sLoading %s from both state file and target directory", indent, a.Name())
   289  
   290  			// If the on-disk asset is the same as the one in the state file, there
   291  			// is no need to consider the one on disk and to mark the asset dirty.
   292  			onDiskMatchesStateFile = reflect.DeepEqual(onDiskAsset, stateFileAsset)
   293  			if onDiskMatchesStateFile {
   294  				logrus.Debugf("%sOn-disk %s matches asset in state file", indent, a.Name())
   295  			}
   296  		}
   297  	}
   298  
   299  	var (
   300  		assetToStore asset.Asset
   301  		source       assetSource
   302  	)
   303  	switch {
   304  	// A parent is dirty. The asset must be re-generated.
   305  	case anyParentsDirty:
   306  		if foundOnDisk {
   307  			logrus.Warningf("%sDiscarding the %s that was provided in the target directory because its dependencies are dirty and it needs to be regenerated", indent, a.Name())
   308  		}
   309  		source = unfetched
   310  	// The asset is on disk and that differs from what is in the source file.
   311  	// The asset is sourced from on disk.
   312  	case foundOnDisk && !onDiskMatchesStateFile:
   313  		logrus.Debugf("%sUsing %s loaded from target directory", indent, a.Name())
   314  		assetToStore = onDiskAsset
   315  		source = onDiskSource
   316  	// The asset is in the state file. The asset is sourced from state file.
   317  	case foundInStateFile:
   318  		logrus.Debugf("%sUsing %s loaded from state file", indent, a.Name())
   319  		assetToStore = stateFileAsset
   320  		source = stateFileSource
   321  	// There is no existing source for the asset. The asset will be generated.
   322  	default:
   323  		source = unfetched
   324  	}
   325  
   326  	state := &assetState{
   327  		asset:           assetToStore,
   328  		source:          source,
   329  		anyParentsDirty: anyParentsDirty,
   330  		presentOnDisk:   foundOnDisk,
   331  	}
   332  	s.assets[reflect.TypeOf(a)] = state
   333  	return state, nil
   334  }
   335  
   336  // purge deletes the on-disk assets that are consumed already.
   337  // E.g., install-config.yaml will be deleted after fetching 'manifests'.
   338  // The target asset is excluded.
   339  func (s *storeImpl) purge(excluded []asset.WritableAsset) error {
   340  	excl := make(map[reflect.Type]bool, len(excluded))
   341  	for _, a := range excluded {
   342  		excl[reflect.TypeOf(a)] = true
   343  	}
   344  	for _, assetState := range s.assets {
   345  		if !assetState.presentOnDisk || excl[reflect.TypeOf(assetState.asset)] {
   346  			continue
   347  		}
   348  		logrus.Infof("Consuming %s from target directory", assetState.asset.Name())
   349  		if err := asset.DeleteAssetFromDisk(assetState.asset.(asset.WritableAsset), s.directory); err != nil {
   350  			return err
   351  		}
   352  		assetState.presentOnDisk = false
   353  	}
   354  	return nil
   355  }
   356  
   357  func increaseIndent(indent string) string {
   358  	return indent + "  "
   359  }
   360  
   361  // Load retrieves the given asset if it is present in the store and does not generate the asset
   362  // if it does not exist and will return nil.
   363  func (s *storeImpl) Load(a asset.Asset) (asset.Asset, error) {
   364  	foundOnDisk, err := s.load(a, "")
   365  	if err != nil {
   366  		return nil, errors.Wrap(err, "failed to load asset")
   367  	}
   368  
   369  	if foundOnDisk.source == unfetched {
   370  		return nil, nil
   371  	}
   372  
   373  	return s.assets[reflect.TypeOf(a)].asset, nil
   374  }