With the recent merging of embedded package imports and the entry CLI package, it is now possible for users to build in mcl code into a single binary. This additional permission makes it explicitly clear that this is permitted to make it easier for those users. The condition is phrased so that the terms can be "patched" by the original author if it's necessary for the project. For example, if the name of the language (mcl) changes, has a differently named new version, someone finds a phrasing improvement or a legal loophole, or for some other reasonable circumstance. Now go write some beautiful embedded tools!
167 lines
6.1 KiB
Go
167 lines
6.1 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2024+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
// Package download is used for downloading language modules from git.
|
|
package download
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
|
|
git "gopkg.in/src-d/go-git.v4"
|
|
)
|
|
|
|
// Downloader implements the Downloader interface. It provides a mechanism to
|
|
// pull down new code from the internet. This is usually done with git.
|
|
type Downloader struct {
|
|
info *interfaces.DownloadInfo
|
|
|
|
// Depth is the max recursion depth that we should descent to. A
|
|
// negative value means infinite. This is usually the default.
|
|
Depth int
|
|
|
|
// Retry is the max number of retries we should run if we encounter a
|
|
// network error. A negative value means infinite. The default is
|
|
// usually zero.
|
|
Retry int
|
|
|
|
// TODO: add a retry backoff parameter
|
|
}
|
|
|
|
// Init initializes the downloader with some core structures we'll need.
|
|
func (obj *Downloader) Init(info *interfaces.DownloadInfo) error {
|
|
obj.info = info
|
|
return nil
|
|
}
|
|
|
|
// Get runs a single download of an import and stores it on disk.
|
|
// XXX: this should only touch the filesystem via obj.info.Fs, but that is not
|
|
// implemented at the moment, so we cheat and use the local fs directly. This is
|
|
// not disastrous, since we only run Get on a local fs, since we don't download
|
|
// to etcdfs directly with the downloader during a deploy. This is because we'd
|
|
// need to implement the afero.Fs -> billy.Filesystem mapping layer.
|
|
func (obj *Downloader) Get(info *interfaces.ImportData, modulesPath string) error {
|
|
if info == nil {
|
|
return fmt.Errorf("empty import information")
|
|
}
|
|
if info.URL == "" {
|
|
return fmt.Errorf("can't clone from empty URL")
|
|
}
|
|
if modulesPath == "" || !strings.HasSuffix(modulesPath, "/") || !strings.HasPrefix(modulesPath, "/") {
|
|
return fmt.Errorf("module path (`%s`) (must be an absolute dir)", modulesPath)
|
|
}
|
|
if stat, err := obj.info.Fs.Stat(modulesPath); err != nil || !stat.IsDir() {
|
|
if err == nil {
|
|
return fmt.Errorf("module path (`%s`) must be a dir", modulesPath)
|
|
}
|
|
if err == os.ErrNotExist {
|
|
return fmt.Errorf("module path (`%s`) must exist", modulesPath)
|
|
}
|
|
return errwrap.Wrapf(err, "could not read module path (`%s`)", modulesPath)
|
|
}
|
|
|
|
if info.IsSystem || info.IsLocal {
|
|
// NOTE: this doesn't prevent us from downloading from a remote
|
|
// git repo that is actually a .git file path instead of HTTP...
|
|
return fmt.Errorf("can only download remote repos")
|
|
}
|
|
// TODO: error early if we're provided *ImportData that we can't act on
|
|
|
|
pull := false
|
|
dir := modulesPath + info.Path // TODO: is this dir unique?
|
|
isBare := false
|
|
options := &git.CloneOptions{
|
|
URL: info.URL,
|
|
// TODO: do we want to add an option for infinite recursion here?
|
|
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
|
|
}
|
|
|
|
msg := fmt.Sprintf("downloading `%s` to: `%s`", info.URL, dir)
|
|
if obj.info.Noop {
|
|
msg = "(noop) " + msg // add prefix
|
|
}
|
|
obj.info.Logf(msg)
|
|
if obj.info.Debug {
|
|
obj.info.Logf("info: `%+v`", info)
|
|
obj.info.Logf("options: `%+v`", options)
|
|
}
|
|
if obj.info.Noop {
|
|
return nil // done early
|
|
}
|
|
// FIXME: replace with:
|
|
// `git.Clone(s storage.Storer, worktree billy.Filesystem, o *CloneOptions)`
|
|
// that uses an `fs engine.Fs` wrapped to the git Filesystem interface:
|
|
// `billyFs := desfacer.New(obj.info.Fs)`
|
|
// TODO: repo, err := git.Clone(??? storage.Storer, billyFs, options)
|
|
repo, err := git.PlainClone(path.Clean(dir), isBare, options)
|
|
if err == git.ErrRepositoryAlreadyExists {
|
|
if obj.info.Update {
|
|
pull = true // make sure to pull latest...
|
|
}
|
|
} else if err != nil {
|
|
return errwrap.Wrapf(err, "can't clone repo: `%s` to: `%s`", info.URL, dir)
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "can't get working tree: `%s`", dir)
|
|
}
|
|
if worktree == nil {
|
|
// FIXME: not sure how we're supposed to handle this scenario...
|
|
return errwrap.Wrapf(err, "can't work with nil work tree for: `%s`", dir)
|
|
}
|
|
|
|
// TODO: do we need to checkout master first, before pulling?
|
|
if pull {
|
|
options := &git.PullOptions{
|
|
// TODO: do we want to add an option for infinite recursion here?
|
|
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
|
|
}
|
|
err := worktree.Pull(options)
|
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
|
return errwrap.Wrapf(err, "can't pull latest from: `%s`", info.URL)
|
|
}
|
|
}
|
|
|
|
// TODO: checkout requested sha1/tag if one was specified...
|
|
// if err := worktree.Checkout(opts *CheckoutOptions)
|
|
|
|
// does the repo have a metadata file present? (we'll validate it later)
|
|
if _, err := obj.info.Fs.Stat(dir + interfaces.MetadataFilename); err != nil {
|
|
return errwrap.Wrapf(err, "could not read repo metadata file `%s` in its root", interfaces.MetadataFilename)
|
|
}
|
|
|
|
return nil
|
|
}
|