github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/remote/default-remote.go (about)

     1  package remote
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/criteo/command-launcher/internal/command"
    12  	"github.com/criteo/command-launcher/internal/helper"
    13  	"github.com/criteo/command-launcher/internal/pkg"
    14  	log "github.com/sirupsen/logrus"
    15  )
    16  
    17  var (
    18  	ErrMsg_PackageNotFound = "package not found"
    19  )
    20  
    21  func IsPackageNotFound(err error) bool {
    22  	return strings.HasPrefix(err.Error(), ErrMsg_PackageNotFound)
    23  }
    24  
    25  type defaultRemoteRepository struct {
    26  	repoBaseUrl    string
    27  	PackagesByName map[string]PackagesByVersion
    28  }
    29  
    30  func newRemoteRepository(baseUrl string) *defaultRemoteRepository {
    31  	return &defaultRemoteRepository{
    32  		repoBaseUrl:    baseUrl,
    33  		PackagesByName: make(map[string]PackagesByVersion),
    34  	}
    35  }
    36  
    37  func (remote *defaultRemoteRepository) Fetch() error {
    38  	return remote.load()
    39  }
    40  
    41  func (remote *defaultRemoteRepository) All() ([]PackageInfo, error) {
    42  	packages := make([]PackageInfo, 0)
    43  	if err := remote.load(); err != nil {
    44  		return packages, err
    45  	}
    46  	for _, list := range remote.PackagesByName {
    47  		packages = append(packages, list...)
    48  	}
    49  	return packages, nil
    50  }
    51  
    52  func (remote *defaultRemoteRepository) PackageNames() ([]string, error) {
    53  	packages := make([]string, 0)
    54  	if err := remote.load(); err != nil {
    55  		return packages, err
    56  	}
    57  	for key := range remote.PackagesByName {
    58  		packages = append(packages, key)
    59  	}
    60  	return packages, nil
    61  }
    62  
    63  func (remote *defaultRemoteRepository) Versions(pkgName string) ([]string, error) {
    64  	results := make([]string, 0)
    65  	if err := remote.load(); err != nil {
    66  		return results, err
    67  	}
    68  	pkgInfos, exists := remote.PackagesByName[pkgName]
    69  	if exists {
    70  		for _, info := range pkgInfos {
    71  			var version defaultVersion
    72  			err := ParseVersion(info.Version, &version)
    73  			if err == nil && info.Name == pkgName {
    74  				results = append(results, version.String())
    75  			}
    76  		}
    77  	}
    78  
    79  	return results, nil
    80  }
    81  
    82  func (remote *defaultRemoteRepository) PackageInfosByCmdName(pkgName string) ([]PackageInfo, error) {
    83  	if err := remote.load(); err != nil {
    84  		return []PackageInfo{}, err
    85  	}
    86  	pkgInfos, exists := remote.PackagesByName[pkgName]
    87  	if exists {
    88  		return pkgInfos, nil
    89  	}
    90  	return make(PackagesByVersion, 0), nil
    91  }
    92  
    93  func (remote *defaultRemoteRepository) LatestVersion(pkgName string) (string, error) {
    94  	return remote.QueryLatestVersion(pkgName, func(pkgInfo *PackageInfo) bool {
    95  		return true
    96  	})
    97  }
    98  
    99  func (remote *defaultRemoteRepository) QueryLatestVersion(pkgName string, filter PackageInfoFilterFunc) (string, error) {
   100  	pkgInfo, err := remote.QueryLatestPackageInfo(pkgName, filter)
   101  	if err != nil {
   102  		return "", err
   103  	}
   104  	if pkgInfo == nil {
   105  		return "", fmt.Errorf("%s: %s", ErrMsg_PackageNotFound, pkgName)
   106  	}
   107  	return pkgInfo.Version, nil
   108  }
   109  
   110  func (remote *defaultRemoteRepository) LatestPackageInfo(pkgName string) (*PackageInfo, error) {
   111  	return remote.QueryLatestPackageInfo(pkgName, func(pkgInfo *PackageInfo) bool {
   112  		return true
   113  	})
   114  }
   115  
   116  func (remote *defaultRemoteRepository) QueryLatestPackageInfo(pkgName string, filter PackageInfoFilterFunc) (*PackageInfo, error) {
   117  	pkgInfos, err := remote.PackageInfosByCmdName(pkgName)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	if len(pkgInfos) == 0 {
   122  		return nil, fmt.Errorf("%s in remote repository: %s", ErrMsg_PackageNotFound, pkgName)
   123  	}
   124  	for i := len(pkgInfos) - 1; i >= 0; i-- {
   125  		if filter(&pkgInfos[i]) {
   126  			return &pkgInfos[i], nil
   127  		}
   128  	}
   129  	return nil, fmt.Errorf("%s in remote repository: %s. The package exists, but no version match the query filter", ErrMsg_PackageNotFound, pkgName)
   130  }
   131  
   132  // get package from remote registry, this will download the package
   133  func (remote *defaultRemoteRepository) Package(pkgName string, pkgVersion string) (command.Package, error) {
   134  	tmpDir, err := os.MkdirTemp("", "package-download-*")
   135  	if err != nil {
   136  		return nil, fmt.Errorf("cannot create temporary dir (%v)", err)
   137  	}
   138  
   139  	pkgPathname := filepath.Join(tmpDir, fmt.Sprintf("%s-%s.pkg", pkgName, pkgVersion))
   140  
   141  	url := remote.url(pkgName, pkgVersion)
   142  
   143  	if err := helper.DownloadFile(url, pkgPathname, true); err != nil {
   144  		return nil, fmt.Errorf("error downloading %s: %v", url, err)
   145  	}
   146  
   147  	pkg, err := pkg.CreateZipPackage(pkgPathname)
   148  	if err != nil {
   149  		return nil, fmt.Errorf("invalid package %s: %v", url, err)
   150  	}
   151  
   152  	return pkg, nil
   153  }
   154  
   155  func (remote *defaultRemoteRepository) PackageInfo(pkgName string, pkgVersion string) (*PackageInfo, error) {
   156  	return remote.findPackage(pkgName, pkgVersion)
   157  }
   158  
   159  func (remote *defaultRemoteRepository) Verify(pkg command.Package, verifyChecksum, verifySignature bool) (bool, error) {
   160  	pkgName := pkg.Name()
   161  	pkgVersion := pkg.Version()
   162  	pkgInfo, err := remote.PackageInfo(pkgName, pkgVersion)
   163  	if err != nil {
   164  		return false, fmt.Errorf("Failed to get package information %s: %v\n", pkgName, err)
   165  	}
   166  	if verifyChecksum {
   167  		if ok, err := pkg.VerifyChecksum(pkgInfo.Checksum); !ok || err != nil {
   168  			return false, err
   169  		}
   170  	}
   171  	if verifySignature {
   172  		// TODO: add signature verification here
   173  		return true, nil
   174  	}
   175  	return true, nil
   176  }
   177  
   178  func (remote *defaultRemoteRepository) findPackage(name string, version string) (*PackageInfo, error) {
   179  	if err := remote.load(); err != nil {
   180  		return nil, err
   181  	}
   182  	pkgInfos, err := remote.PackageInfosByCmdName(name)
   183  	if len(pkgInfos) == 0 {
   184  		return nil, fmt.Errorf("no package named %s found from remote registry", name)
   185  	}
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	for _, info := range pkgInfos {
   190  		if info.Version == version {
   191  			return &info, nil
   192  		}
   193  	}
   194  	return nil, fmt.Errorf("no package named %s with version %s found from remote registry", name, version)
   195  }
   196  
   197  func (remote *defaultRemoteRepository) url(name string, version string) string {
   198  	// first get the default url using the remote base following the convention
   199  	pkgUrl := fmt.Sprintf("%s/%s", remote.repoBaseUrl, remote.pkgFilename(name, version))
   200  
   201  	// now try to find the package info
   202  	pkgInfo, err := remote.findPackage(name, version)
   203  	if err != nil || pkgInfo == nil {
   204  		return pkgUrl
   205  	}
   206  	if pkgInfo.Url != "" {
   207  		pkgUrl = pkgInfo.Url
   208  	}
   209  
   210  	return pkgUrl
   211  }
   212  
   213  func (remote *defaultRemoteRepository) pkgFilename(name string, version string) string {
   214  	return fmt.Sprintf("%s-%s.pkg", name, version)
   215  }
   216  
   217  func (remote *defaultRemoteRepository) load() error {
   218  	if !remote.isLoaded() {
   219  		body, err := helper.LoadFile(fmt.Sprintf("%s/index.json", remote.repoBaseUrl))
   220  		if err != nil {
   221  			log.Error("Cannot read remote packages index")
   222  			return err
   223  		}
   224  
   225  		var entries []PackageInfo
   226  		err = json.Unmarshal(body, &entries)
   227  		if err != nil {
   228  			log.Error("json parsing error")
   229  			return err
   230  		}
   231  
   232  		for _, pkg := range entries {
   233  			lst, exists := remote.PackagesByName[pkg.Name]
   234  			if exists {
   235  				lst = append(lst, pkg)
   236  				remote.PackagesByName[pkg.Name] = lst
   237  			} else {
   238  				newLst := make(PackagesByVersion, 0)
   239  				newLst = append(newLst, pkg)
   240  				remote.PackagesByName[pkg.Name] = newLst
   241  			}
   242  		}
   243  
   244  		// sort packages
   245  		for _, v := range remote.PackagesByName {
   246  			sort.Sort(v)
   247  		}
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  func (remote *defaultRemoteRepository) isLoaded() bool {
   254  	return len(remote.PackagesByName) > 0
   255  }