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 }