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 }