github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/ociinstaller/versionfile/plugin_version_file.go (about) 1 package versionfile 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log" 9 "os" 10 "path/filepath" 11 "sync" 12 13 filehelpers "github.com/turbot/go-kit/files" 14 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 15 "github.com/turbot/steampipe/pkg/error_helpers" 16 "github.com/turbot/steampipe/pkg/filepaths" 17 ) 18 19 var ( 20 ErrNoContent = errors.New("no content") 21 ) 22 23 const ( 24 PluginStructVersion = 20220411 25 // the name of the version files that are put in the plugin installation directories 26 pluginVersionFileName = "version.json" 27 ) 28 29 type PluginVersionFile struct { 30 Plugins map[string]*InstalledVersion `json:"plugins"` 31 StructVersion int64 `json:"struct_version"` 32 } 33 34 func newPluginVersionFile() *PluginVersionFile { 35 return &PluginVersionFile{ 36 Plugins: map[string]*InstalledVersion{}, 37 StructVersion: PluginStructVersion, 38 } 39 } 40 41 // IsValid checks whether the struct was correctly deserialized, 42 // by checking if the StructVersion is populated 43 func (p *PluginVersionFile) IsValid() bool { 44 return p.StructVersion > 0 45 } 46 47 // EnsurePluginVersionFile reads the version file in the plugin directory (if exists) and overwrites it if the data in the 48 // argument is different. The comparison is done using the `Name` and `BinaryDigest` properties. 49 // If the file doesn't exist, or cannot be read/parsed, EnsurePluginVersionFile fails over to overwriting the data 50 func (p *PluginVersionFile) EnsurePluginVersionFile(installData *InstalledVersion) error { 51 pluginFolder, err := filepaths.FindPluginFolder(installData.Name) 52 if err != nil { 53 return err 54 } 55 versionFile := filepath.Join(pluginFolder, pluginVersionFileName) 56 57 // If the version file already exists, we only write to it if the incoming data is newer 58 if filehelpers.FileExists(versionFile) { 59 installation, err := readPluginVersionFile(versionFile) 60 if err == nil && installation.Equal(installData) { 61 // the new and old data match - no need to overwrite 62 return nil 63 } 64 // in case of error, just failover to a overwrite 65 } 66 67 theBytes, err := json.MarshalIndent(installData, "", " ") 68 if err != nil { 69 return err 70 } 71 return os.WriteFile(versionFile, theBytes, 0644) 72 } 73 74 // Save writes the config file to disk 75 func (p *PluginVersionFile) Save() error { 76 // set struct version 77 p.StructVersion = PluginStructVersion 78 versionFilePath := filepaths.PluginVersionFilePath() 79 return p.write(versionFilePath) 80 } 81 82 func (p *PluginVersionFile) write(path string) error { 83 versionFileJSON, err := json.MarshalIndent(p, "", " ") 84 if err != nil { 85 log.Println("[ERROR]", "Error while writing version file", err) 86 return err 87 } 88 if len(versionFileJSON) == 0 { 89 log.Println("[ERROR]", "Cannot write 0 bytes to file") 90 return sperr.WrapWithMessage(ErrNoContent, "cannot write versions file") 91 } 92 return os.WriteFile(path, versionFileJSON, 0644) 93 } 94 95 func (p *PluginVersionFile) ensureVersionFilesInPluginDirectories() error { 96 removals := []*InstalledVersion{} 97 for _, installation := range p.Plugins { 98 if err := p.EnsurePluginVersionFile(installation); err != nil { 99 if errors.Is(err, os.ErrNotExist) { 100 removals = append(removals, installation) 101 continue 102 } 103 return err 104 } 105 } 106 107 // if we found any plugins that do not have installations, remove them from the map 108 if len(removals) > 0 { 109 for _, removal := range removals { 110 delete(p.Plugins, removal.Name) 111 } 112 return p.Save() 113 } 114 return nil 115 } 116 117 // any plugins installed under the `local` folder are added to the plugin version file 118 func (p *PluginVersionFile) AddLocalPlugins(ctx context.Context) error_helpers.ErrorAndWarnings { 119 localPlugins, err := loadLocalPlugins(ctx) 120 if err != nil { 121 return error_helpers.NewErrorsAndWarning(err) 122 } 123 for name, install := range localPlugins { 124 if _, ok := p.Plugins[name]; ok { 125 // if the plugin is already in the global version file, skip it 126 continue 127 } 128 p.Plugins[fmt.Sprintf("local/%s", name)] = install 129 } 130 return error_helpers.EmptyErrorsAndWarning() 131 } 132 133 // to lock plugin version file loads 134 var pluginLoadLock = sync.Mutex{} 135 136 // LoadPluginVersionFile migrates from the old version file format if necessary and loads the plugin version data 137 func LoadPluginVersionFile(ctx context.Context) (*PluginVersionFile, error) { 138 139 // we need a lock here so that we don't hit a race condition where 140 // the plugin file needs to be composed 141 // if recomposition is not required, this has (almost) zero penalty 142 pluginLoadLock.Lock() 143 defer pluginLoadLock.Unlock() 144 145 versionFilePath := filepaths.PluginVersionFilePath() 146 if filehelpers.FileExists(versionFilePath) { 147 pluginVersions, err := readGlobalPluginVersionsFile(versionFilePath) 148 149 // we could read and parse out the file - all is well 150 if err == nil { 151 return pluginVersions, nil 152 } 153 } 154 155 // we don't have a global plugin/versions.json or it is not parseable or is empty (always recompose) 156 // generate the version file from the individual version files by walking the plugin directories 157 // this will return an Empty Version file if there are no version files in the plugin directories 158 pluginVersions := recomposePluginVersionFile(ctx) 159 160 // save the recomposed file 161 err := pluginVersions.Save() 162 if err != nil { 163 return nil, err 164 } 165 return pluginVersions, err 166 } 167 168 func loadLocalPlugins(ctx context.Context) (map[string]*InstalledVersion, error) { 169 localFolder := filepaths.LocalPluginPath() 170 localPlugins := map[string]*InstalledVersion{} 171 172 // iterate over all folders underneath the local plugin directory and if the folder contains a plugin, add to the map 173 pluginFolders, err := filehelpers.ListFilesWithContext(ctx, localFolder, &filehelpers.ListOptions{Flags: filehelpers.DirectoriesFlat}) 174 if err != nil { 175 return nil, err 176 } 177 for _, pluginFolder := range pluginFolders { 178 // check if the folder contains a plugin file 179 pluginName := filepath.Base(pluginFolder) 180 181 pluginShortName := filepaths.PluginAliasToShortName(pluginName) 182 pluginLongName := filepaths.PluginAliasToLongName(pluginName) 183 184 pluginFiles := []string{ 185 pluginShortName + ".plugin", 186 pluginLongName + ".plugin", 187 } 188 // check both short and long names 189 190 for _, pluginFile := range pluginFiles { 191 pluginPath := filepath.Join(pluginFolder, pluginFile) 192 if filehelpers.FileExists(pluginPath) { 193 localPlugins[pluginName] = &InstalledVersion{ 194 Name: pluginPath, 195 Version: "local", 196 StructVersion: InstalledVersionStructVersion, 197 } 198 } 199 } 200 } 201 202 return localPlugins, nil 203 } 204 205 // EnsureVersionFilesInPluginDirectories attempts a backfill of the individual version.json for plugins 206 // this is required only once when upgrading from 0.20.x 207 func EnsureVersionFilesInPluginDirectories(ctx context.Context) error { 208 versions, err := LoadPluginVersionFile(ctx) 209 if err != nil { 210 return err 211 } 212 return versions.ensureVersionFilesInPluginDirectories() 213 } 214 215 // recomposePluginVersionFile recursively traverses down the plugin direcory and tries to 216 // recompose the global version file from the plugin version files 217 // if there are no plugin version files, this returns a ready to use empty global version file 218 func recomposePluginVersionFile(ctx context.Context) *PluginVersionFile { 219 pvf := newPluginVersionFile() 220 221 versionFiles, err := filehelpers.ListFilesWithContext(ctx, filepaths.EnsurePluginDir(), &filehelpers.ListOptions{ 222 Include: []string{fmt.Sprintf("**/%s", pluginVersionFileName)}, 223 Flags: filehelpers.FilesRecursive, 224 }) 225 226 if err != nil { 227 log.Println("[TRACE] recomposePluginVersionFile failed - error while walking plugin directory for version files", err) 228 return pvf 229 } 230 231 for _, versionFile := range versionFiles { 232 install, err := readPluginVersionFile(versionFile) 233 if err != nil { 234 log.Println("[TRACE] could not read file", versionFile) 235 continue 236 } 237 pvf.Plugins[install.Name] = install 238 } 239 240 return pvf 241 } 242 243 func readPluginVersionFile(versionFile string) (*InstalledVersion, error) { 244 data, err := os.ReadFile(versionFile) 245 if err != nil { 246 log.Println("[TRACE] could not read file", versionFile) 247 return nil, err 248 } 249 install := EmptyInstalledVersion() 250 if err := json.Unmarshal(data, &install); err != nil { 251 // this wasn't the version file (probably) - keep going 252 log.Println("[TRACE] unmarshal failed for file:", versionFile) 253 return nil, err 254 } 255 return install, nil 256 } 257 258 func readGlobalPluginVersionsFile(path string) (*PluginVersionFile, error) { 259 file, err := os.ReadFile(path) 260 if err != nil { 261 return nil, err 262 } 263 if len(file) == 0 { 264 // the file exists, but is empty - return an error 265 // start from scratch 266 return nil, sperr.New("plugin versions.json file is empty") 267 } 268 269 var data PluginVersionFile 270 271 if err := json.Unmarshal(file, &data); err != nil { 272 return nil, err 273 } 274 275 if data.Plugins == nil { 276 data.Plugins = map[string]*InstalledVersion{} 277 } 278 279 for key, installedPlugin := range data.Plugins { 280 // hard code the name to the key 281 installedPlugin.Name = key 282 if installedPlugin.StructVersion == 0 { 283 // also backfill the StructVersion in map values 284 installedPlugin.StructVersion = InstalledVersionStructVersion 285 } 286 } 287 288 return &data, nil 289 }