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  }