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  }