github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/ociinstaller/plugin.go (about)

     1  package ociinstaller
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/turbot/steampipe/pkg/filepaths"
    17  	"github.com/turbot/steampipe/pkg/ociinstaller/versionfile"
    18  	"github.com/turbot/steampipe/pkg/utils"
    19  )
    20  
    21  var versionFileUpdateLock = &sync.Mutex{}
    22  
    23  // InstallPlugin installs a plugin from an OCI Image
    24  func InstallPlugin(ctx context.Context, imageRef string, constraint string, sub chan struct{}, opts ...PluginInstallOption) (*SteampipeImage, error) {
    25  	config := &pluginInstallConfig{}
    26  	for _, opt := range opts {
    27  		opt(config)
    28  	}
    29  	tempDir := NewTempDir(filepaths.EnsurePluginDir())
    30  	defer func() {
    31  		// send a last beacon to signal completion
    32  		sub <- struct{}{}
    33  		if err := tempDir.Delete(); err != nil {
    34  			log.Printf("[TRACE] Failed to delete temp dir '%s' after installing plugin: %s", tempDir, err)
    35  		}
    36  	}()
    37  
    38  	ref := NewSteampipeImageRef(imageRef)
    39  	imageDownloader := NewOciDownloader()
    40  
    41  	sub <- struct{}{}
    42  	image, err := imageDownloader.Download(ctx, ref, ImageTypePlugin, tempDir.Path)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	// update the image ref to include the constraint and use to get the plugin install path
    48  	constraintRef := image.ImageRef.DisplayImageRefConstraintOverride(constraint)
    49  	pluginPath := filepaths.EnsurePluginInstallDir(constraintRef)
    50  
    51  	sub <- struct{}{}
    52  	if err = installPluginBinary(image, tempDir.Path, pluginPath); err != nil {
    53  		return nil, fmt.Errorf("plugin installation failed: %s", err)
    54  	}
    55  	sub <- struct{}{}
    56  	if err = installPluginDocs(image, tempDir.Path, pluginPath); err != nil {
    57  		return nil, fmt.Errorf("plugin installation failed: %s", err)
    58  	}
    59  	if !config.skipConfigFile {
    60  		if err = installPluginConfigFiles(image, tempDir.Path, constraint); err != nil {
    61  			return nil, fmt.Errorf("plugin installation failed: %s", err)
    62  		}
    63  	}
    64  	sub <- struct{}{}
    65  	if err := updatePluginVersionFiles(ctx, image, constraint); err != nil {
    66  		return nil, err
    67  	}
    68  	return image, nil
    69  }
    70  
    71  // updatePluginVersionFiles updates the global versions.json to add installation of the plugin
    72  // also adds a version file in the plugin installation directory with the information
    73  func updatePluginVersionFiles(ctx context.Context, image *SteampipeImage, constraint string) error {
    74  	versionFileUpdateLock.Lock()
    75  	defer versionFileUpdateLock.Unlock()
    76  
    77  	timeNow := versionfile.FormatTime(time.Now())
    78  	v, err := versionfile.LoadPluginVersionFile(ctx)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	// For the full name we want the constraint (^0.4) used, not the resolved version (0.4.1)
    84  	// we override the DisplayImageRef with the constraint here.
    85  	pluginFullName := image.ImageRef.DisplayImageRefConstraintOverride(constraint)
    86  
    87  	installedVersion, ok := v.Plugins[pluginFullName]
    88  	if !ok {
    89  		installedVersion = versionfile.EmptyInstalledVersion()
    90  	}
    91  
    92  	installedVersion.Name = pluginFullName
    93  	installedVersion.Version = image.Config.Plugin.Version
    94  	installedVersion.ImageDigest = string(image.OCIDescriptor.Digest)
    95  	installedVersion.BinaryDigest = image.Plugin.BinaryDigest
    96  	installedVersion.BinaryArchitecture = image.Plugin.BinaryArchitecture
    97  	installedVersion.InstalledFrom = image.ImageRef.ActualImageRef()
    98  	installedVersion.LastCheckedDate = timeNow
    99  	installedVersion.InstallDate = timeNow
   100  
   101  	v.Plugins[pluginFullName] = installedVersion
   102  
   103  	// Ensure that the version file is written to the plugin installation folder
   104  	// Having this file is important, since this can be used
   105  	// to compose the global version file if it is unavailable or unparseable
   106  	// This makes sure that in the event of corruption (global/individual) we don't end up
   107  	// losing all the plugin install data
   108  	if err := v.EnsurePluginVersionFile(installedVersion); err != nil {
   109  		return err
   110  	}
   111  
   112  	return v.Save()
   113  }
   114  
   115  func installPluginBinary(image *SteampipeImage, tempDir string, destDir string) error {
   116  	sourcePath := filepath.Join(tempDir, image.Plugin.BinaryFile)
   117  
   118  	// check if system is M1 - if so we need some special handling
   119  	isM1, err := utils.IsMacM1()
   120  	if err != nil {
   121  		return fmt.Errorf("failed to detect system architecture")
   122  	}
   123  	if isM1 {
   124  		// NOTE: for Mac M1 machines, if the binary is updated in place without deleting the existing file,
   125  		// the updated plugin binary may crash on execution - for an undetermined reason
   126  		// to avoid this, remove the existing plugin folder and re-create it
   127  		if err := os.RemoveAll(destDir); err != nil {
   128  			return fmt.Errorf("could not remove plugin folder")
   129  		}
   130  		if err := os.MkdirAll(destDir, 0755); err != nil {
   131  			return fmt.Errorf("could not create plugin folder")
   132  		}
   133  	}
   134  
   135  	// unzip the file into the plugin folder
   136  	if _, err := ungzip(sourcePath, destDir); err != nil {
   137  		return fmt.Errorf("could not unzip %s to %s", sourcePath, destDir)
   138  	}
   139  	return nil
   140  }
   141  
   142  func installPluginDocs(image *SteampipeImage, tempDir string, destDir string) error {
   143  	// if DocsDir is not set, then there are no docs.
   144  	if image.Plugin.DocsDir == "" {
   145  		return nil
   146  	}
   147  
   148  	// install the docs
   149  	sourcePath := filepath.Join(tempDir, image.Plugin.DocsDir)
   150  	destPath := filepath.Join(destDir, "docs")
   151  	if fileExists(destPath) {
   152  		os.RemoveAll(destPath)
   153  	}
   154  	if err := moveFolderWithinPartition(sourcePath, destPath); err != nil {
   155  		return fmt.Errorf("could not copy %s to %s", sourcePath, destPath)
   156  	}
   157  	return nil
   158  }
   159  
   160  func installPluginConfigFiles(image *SteampipeImage, tempdir string, constraint string) error {
   161  	installTo := filepaths.EnsureConfigDir()
   162  
   163  	// if ConfigFileDir is not set, then there are no config files.
   164  	if image.Plugin.ConfigFileDir == "" {
   165  		return nil
   166  	}
   167  	// install config files (if they dont already exist)
   168  	sourcePath := filepath.Join(tempdir, image.Plugin.ConfigFileDir)
   169  
   170  	objects, err := os.ReadDir(sourcePath)
   171  	if err != nil {
   172  		return fmt.Errorf("couldn't read source dir: %s", err)
   173  	}
   174  
   175  	for _, obj := range objects {
   176  		sourceFile := filepath.Join(sourcePath, obj.Name())
   177  		destFile := filepath.Join(installTo, obj.Name())
   178  		if err := copyConfigFileUnlessExists(sourceFile, destFile, constraint); err != nil {
   179  			return fmt.Errorf("could not copy config file from %s to %s", sourceFile, destFile)
   180  		}
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  func copyConfigFileUnlessExists(sourceFile string, destFile string, constraint string) error {
   187  	if fileExists(destFile) {
   188  		return nil
   189  	}
   190  	inputData, err := os.ReadFile(sourceFile)
   191  	if err != nil {
   192  		return fmt.Errorf("couldn't open source file: %s", err)
   193  	}
   194  	inputStat, err := os.Stat(sourceFile)
   195  	if err != nil {
   196  		return fmt.Errorf("couldn't read source file permissions: %s", err)
   197  	}
   198  	// update the connection config with the correct plugin version
   199  	inputData = addPluginConstraintToConfig(inputData, constraint)
   200  	if err = os.WriteFile(destFile, inputData, inputStat.Mode()); err != nil {
   201  		return fmt.Errorf("writing to output file failed: %s", err)
   202  	}
   203  	return nil
   204  }
   205  
   206  // The default config files have the plugin set to the 'latest' stream (as this is what is installed by default)
   207  // When installing non-latest plugins, that property needs to be adjusted to the stream actually getting installed.
   208  // Otherwise, during plugin resolution, it will resolve to an incorrect plugin instance
   209  // (or none at all, if  'latest' versions isn't installed)
   210  func addPluginConstraintToConfig(src []byte, constraint string) []byte {
   211  	if constraint == "latest" {
   212  		return src
   213  	}
   214  
   215  	regex := regexp.MustCompile(`^(\s*)plugin\s*=\s*"(.*)"\s*$`)
   216  	substitution := fmt.Sprintf(`$1 plugin = "$2@%s"`, constraint)
   217  
   218  	srcScanner := bufio.NewScanner(strings.NewReader(string(src)))
   219  	srcScanner.Split(bufio.ScanLines)
   220  	destBuffer := bytes.NewBufferString("")
   221  
   222  	for srcScanner.Scan() {
   223  		line := srcScanner.Text()
   224  		if regex.MatchString(line) {
   225  			line = regex.ReplaceAllString(line, substitution)
   226  			// remove the extra space we had to add to the substitution token
   227  			line = line[1:]
   228  		}
   229  		destBuffer.WriteString(fmt.Sprintf("%s\n", line))
   230  	}
   231  	return destBuffer.Bytes()
   232  }