github.com/nuvolaris/nuv@v0.0.0-20240511174247-a74e3a52bfd8/plugin.go (about) 1 // Licensed to the Apache Software Foundation (ASF) under one 2 // or more contributor license agreements. See the NOTICE file 3 // distributed with this work for additional information 4 // regarding copyright ownership. The ASF licenses this file 5 // to you under the Apache License, Version 2.0 (the 6 // "License"); you may not use this file except in compliance 7 // with the License. You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package main 19 20 import ( 21 "errors" 22 "flag" 23 "fmt" 24 "os" 25 "path/filepath" 26 "regexp" 27 "strings" 28 29 git "github.com/go-git/go-git/v5" 30 "github.com/go-git/go-git/v5/plumbing" 31 "github.com/mitchellh/go-homedir" 32 ) 33 34 func pluginTool() error { 35 flag := flag.NewFlagSet("plugin", flag.ExitOnError) 36 flag.Usage = printPluginUsage 37 38 err := flag.Parse(os.Args[1:]) 39 if err != nil { 40 return err 41 } 42 43 if flag.NArg() != 1 { 44 flag.Usage() 45 return errors.New("invalid number of arguments. Expected 1") 46 } 47 48 return downloadPluginTasksFromRepo(flag.Arg(0)) 49 } 50 51 func printPluginUsage() { 52 fmt.Println(`Usage: nuv -plugin <repo-url> 53 54 Install/update plugins from a remote repository. 55 The name of the repository must start with 'olaris-'.`) 56 } 57 58 func downloadPluginTasksFromRepo(repo string) error { 59 isNameValid, repoName := checkGitRepo(repo) 60 if !isNameValid { 61 return fmt.Errorf("plugin repository must be a https url and plugin must start with 'olaris-'") 62 } 63 64 pluginDir, err := homedir.Expand("~/.nuv/" + repoName) 65 if err != nil { 66 return err 67 } 68 69 if isDir(pluginDir) { 70 fmt.Println("Updating plugin", repoName) 71 72 r, err := git.PlainOpen(pluginDir) 73 if err != nil { 74 return err 75 } 76 // Get the working directory for the repository 77 w, err := r.Worktree() 78 if err != nil { 79 return err 80 } 81 82 // Pull the latest changes from the origin remote and merge into the current branch 83 err = w.Pull(&git.PullOptions{RemoteName: "origin"}) 84 if err != nil { 85 if err.Error() == "already up-to-date" { 86 fmt.Println("The plugin repo is already up to date!") 87 return nil 88 } 89 return err 90 } 91 92 return nil 93 } 94 95 if err := os.MkdirAll(pluginDir, 0755); err != nil { 96 return err 97 } 98 99 // if not, clone 100 cloneOpts := &git.CloneOptions{ 101 URL: repo, 102 Progress: os.Stderr, 103 ReferenceName: plumbing.NewBranchReferenceName("main"), 104 } 105 106 fmt.Println("Downloading plugins:", repoName) 107 _, err = git.PlainClone(pluginDir, false, cloneOpts) 108 if err != nil { 109 return err 110 } 111 112 return nil 113 } 114 115 func checkGitRepo(url string) (bool, string) { 116 // Remove the ".git" extension if present 117 url = strings.TrimSuffix(url, ".git") 118 119 // Extract the repository name from the URL 120 parts := strings.Split(url, "/") 121 repoName := parts[len(parts)-1] 122 123 // Check if the repository name matches the pattern "https://...olaris-*" 124 matchProtocol, _ := regexp.MatchString(`^https://.*$`, url) 125 matchName, _ := regexp.MatchString(`^olaris-.*$`, repoName) 126 127 if matchName && matchProtocol { 128 return true, repoName 129 } 130 return false, "" 131 } 132 133 func printPluginsHelp() error { 134 plgs, err := newPlugins() 135 if err != nil { 136 return err 137 } 138 plgs.print() 139 return nil 140 } 141 142 // GetNuvRootPlugins returns the map with all the olaris-*/nuvroot.json files 143 // in the local and ~/.nuv folders, pointed by their plugin names. 144 // If the same plugin is found in both folders, the one in the local folder 145 // is used. 146 // Useful to build the config map including the plugin configs 147 func GetNuvRootPlugins() (map[string]string, error) { 148 plgs, err := newPlugins() 149 if err != nil { 150 return nil, err 151 } 152 153 nuvRoots := make(map[string]string) 154 for _, path := range plgs.local { 155 name := getPluginName(path) 156 nuvRootPath := joinpath(path, NUVROOT) 157 nuvRoots[name] = nuvRootPath 158 } 159 160 for _, path := range plgs.nuv { 161 name := getPluginName(path) 162 // if the plugin is already in the map, skip it 163 if _, ok := nuvRoots[name]; ok { 164 continue 165 } 166 nuvRootPath := joinpath(path, NUVROOT) 167 nuvRoots[name] = nuvRootPath 168 } 169 170 return nuvRoots, nil 171 } 172 173 // findTaskInPlugins returns the path to the plugin containing the task 174 // or an error if the task is not found 175 func findTaskInPlugins(plg string) (string, error) { 176 plgs, err := newPlugins() 177 if err != nil { 178 return "", err 179 } 180 181 // check that plg is the suffix of a folder name in plgs.local 182 for _, path := range plgs.local { 183 folder := filepath.Base(path) 184 if strings.TrimPrefix(folder, "olaris-") == plg { 185 return path, nil 186 } 187 } 188 189 // check that plg is the suffix of a folder name in plgs.nuv 190 for _, path := range plgs.nuv { 191 folder := filepath.Base(path) 192 if strings.TrimPrefix(folder, "olaris-") == plg { 193 return path, nil 194 } 195 } 196 197 return "", &TaskNotFoundErr{input: plg} 198 } 199 200 // plugins struct holds the list of local and ~/.nuv olaris-* folders 201 type plugins struct { 202 local []string 203 nuv []string 204 } 205 206 func newPlugins() (*plugins, error) { 207 localDir := os.Getenv("NUV_ROOT_PLUGIN") 208 localOlarisFolders := make([]string, 0) 209 nuvOlarisFolders := make([]string, 0) 210 211 // Search in directory (localDir/olaris-*) 212 dir := filepath.Join(localDir, "olaris-*") 213 olarisFolders, err := filepath.Glob(dir) 214 if err != nil { 215 return nil, err 216 } 217 218 // filter all folders that are do not contain nuvfile.yaml 219 for _, folder := range olarisFolders { 220 if !isDir(folder) || !exists(folder, NUVFILE) { 221 continue 222 } 223 localOlarisFolders = append(localOlarisFolders, folder) 224 } 225 226 // Search in ~/.nuv/olaris-* 227 nuvHome, err := homedir.Expand("~/.nuv") 228 if err != nil { 229 return nil, err 230 } 231 232 olarisNuvFolders, err := filepath.Glob(filepath.Join(nuvHome, "olaris-*")) 233 if err != nil { 234 return nil, err 235 } 236 for _, folder := range olarisNuvFolders { 237 if !isDir(folder) || !exists(folder, NUVFILE) { 238 continue 239 } 240 nuvOlarisFolders = append(nuvOlarisFolders, folder) 241 } 242 243 return &plugins{ 244 local: localOlarisFolders, 245 nuv: nuvOlarisFolders, 246 }, nil 247 } 248 249 func (p *plugins) print() { 250 if len(p.local) == 0 && len(p.nuv) == 0 { 251 debug("No plugins installed") 252 // fmt.Println("No plugins installed. Use 'nuv -plugin' to add new ones.") 253 return 254 } 255 256 fmt.Println("Plugins:") 257 if len(p.local) > 0 { 258 for _, plg := range p.local { 259 plgName := getPluginName(plg) 260 fmt.Printf(" %s (local)\n", plgName) 261 } 262 } 263 264 if len(p.nuv) > 0 { 265 for _, plg := range p.nuv { 266 plgName := getPluginName(plg) 267 fmt.Printf(" %s (nuv)\n", plgName) 268 } 269 } 270 } 271 272 // getPluginName returns the plugin name from the plugin path, removing the 273 // olaris- prefix 274 func getPluginName(plg string) string { 275 // remove olaris- prefix 276 plgName := strings.TrimPrefix(filepath.Base(plg), "olaris-") 277 return plgName 278 279 }