package core import ( "archive/tar" "context" "encoding/json" "errors" "io" "io/ioutil" "net/http" "os" "path/filepath" "sync" "time" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-version" "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant/internal/server/proto/vagrant_server" "github.com/mitchellh/mapstructure" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) // Number of seconds to wait between checks for box updates const BoxUpdateCheckInterval = 3600 type Box struct { basis *Basis box *vagrant_server.Box logger hclog.Logger m sync.Mutex } func NewBox(opts ...BoxOption) (b *Box, err error) { b = &Box{ logger: hclog.L(), box: &vagrant_server.Box{}, } for _, opt := range opts { if oerr := opt(b); oerr != nil { err = multierror.Append(err, oerr) } } if b.basis == nil { return nil, errors.New("Basis must be specified for the box") } if b.box.Directory == "" { return nil, errors.New("Box directory must be specified for the box") } metadataFile := filepath.Join(b.box.Directory, "metadata.json") if _, err := os.Stat(metadataFile); err != nil { return nil, err } data, err := os.ReadFile(metadataFile) if err != nil { return nil, err } metadata := make(map[string]interface{}) if err := json.Unmarshal(data, &metadata); err != nil { return nil, err } b.box.Metadata, err = structpb.NewStruct(metadata) if err != nil { return nil, err } // The metadata should have provider info under the "provider" key b.box.Provider = metadata["provider"].(string) b.box.Id = b.box.Name + "-" + b.box.Version + "-" + b.box.Provider return } type BoxOption func(*Box) error func BoxWithRef(ref *vagrant_plugin_sdk.Ref_Box, ctx context.Context) BoxOption { return func(b *Box) (err error) { boxResponse, err := b.basis.client.GetBox( ctx, &vagrant_server.GetBoxRequest{Box: ref}, ) b.box = boxResponse.Box return } } func BoxWithLogger(log hclog.Logger) BoxOption { return func(b *Box) (err error) { b.logger = log return } } func BoxWithBasis(basis *Basis) BoxOption { return func(b *Box) (err error) { b.basis = basis return } } func BoxWithName(name string) BoxOption { return func(b *Box) (err error) { b.box.Name = name return } } func BoxWithVersion(ver string) BoxOption { return func(b *Box) (err error) { _, err = version.NewVersion(ver) if err != nil { return err } b.box.Version = ver return } } func BoxWithProvider(provider string) BoxOption { return func(b *Box) (err error) { b.box.Provider = provider return } } func BoxWithMetadataUrl(url string) BoxOption { return func(b *Box) (err error) { b.box.MetadataUrl = url return } } func BoxWithDirectory(dir string) BoxOption { return func(b *Box) (err error) { if _, err := os.Stat(dir); err != nil { return err } b.box.Directory = dir return } } func BoxWithBox(box *vagrant_server.Box) BoxOption { return func(b *Box) (err error) { b.box = box return } } func (b *Box) loadMetadata() (metadata *BoxMetadata, err error) { client := &http.Client{} req, err := http.NewRequest("GET", b.box.MetadataUrl, nil) req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } return LoadBoxMetadata(data) } func (b *Box) matches(box core.Box) (bool, error) { name, err := box.Name() if err != nil { return false, err } version, err := box.Version() if err != nil { return false, err } provider, err := box.Provider() if err != nil { return false, err } if b.box.Name == name && b.box.Version == version && b.box.Provider == provider { return true, nil } return false, nil } // Check if a box update check is allowed. Returns true if the // BOX_UPDATE_CHECK_INTERVAL has passed. func (b *Box) AutomaticUpdateCheckAllowed() (allowed bool, err error) { now := time.Now() lastUpdate := b.box.LastUpdate.AsTime() if lastUpdate.Add(BoxUpdateCheckInterval * time.Second).After(now) { return false, nil } b.box.LastUpdate = timestamppb.Now() b.Save() return true, nil } // This deletes the box. This is NOT undoable. func (b *Box) Destroy() (err error) { b.m.Lock() defer b.m.Unlock() b.logger.Trace("deleting box from db", "box", b.box.Name) _, err = b.basis.client.DeleteBox( b.basis.ctx, &vagrant_server.DeleteBoxRequest{Box: &vagrant_plugin_sdk.Ref_Box{ ResourceId: b.box.Id, Name: b.box.Name, Version: b.box.Version, Provider: b.box.Provider, }}, ) if err != nil { b.logger.Trace("failed to delete box", "box", b.box.Name) } if fs, _ := os.Stat(b.box.Directory); fs != nil { b.logger.Trace("Removing box files", "path", b.box.Directory) return os.RemoveAll(b.box.Directory) } return } func (b *Box) Directory() (path string, err error) { return b.box.Directory, nil } // Checks if the box has an update and returns the metadata, version, // and provider. If the box doesn't have an update that satisfies the // constraints, it will return nil. func (b *Box) HasUpdate(version string) (updateAvailable bool, err error) { updateAvailable, _, _, _, err = b.UpdateInfo(version) return } func (b *Box) UpdateInfo(version string) (updateAvailable bool, meta core.BoxMetadata, newVersion string, newProvider string, err error) { metadata, err := b.loadMetadata() if err != nil { return false, nil, "", "", err } versionConstraint := "" if version == "" { versionConstraint = "> " + b.box.Version } else { versionConstraint = version + ", " + "> " + b.box.Version } result, err := metadata.Version( versionConstraint, &core.BoxProvider{Name: b.box.Provider}, ) if err != nil { return false, nil, "", "", err } if result == nil { return false, nil, "", "", nil } return true, metadata, result.Version, b.box.Provider, nil } // Checks if this box is in use according to the given machine // index and returns the entries that appear to be using the box. func (b *Box) InUse(index core.TargetIndex) (inUse bool, err error) { targets, err := index.All() if err != nil { return false, err } for _, t := range targets { m, err := t.Specialize((*core.Machine)(nil)) if err != nil { continue } machineBox, err := m.(core.Machine).Box() if err != nil { return false, err } ok, err := b.matches(machineBox) if err != nil { return false, err } if ok { return true, nil } } return false, nil } func (b *Box) Machines(index core.TargetIndex) (machines []core.Machine, err error) { machines = []core.Machine{} targets, err := index.All() if err != nil { return nil, err } for _, t := range targets { if s, _ := t.State(); s == core.DESTROYED || s == core.NOT_CREATED { continue } m, err := t.Specialize((*core.Machine)(nil)) if err != nil { continue } machineBox, err := m.(core.Machine).Box() if err != nil { return nil, err } if ok, _ := b.matches(machineBox); ok { machines = append(machines, m.(core.Machine)) } } return } func (b *Box) Metadata() (metadata core.BoxMetadata, err error) { meta, err := b.loadMetadata() if err != nil { return nil, err } return metadata, mapstructure.Decode(meta, &metadata) } func (b *Box) BoxMetadata() (metadata map[string]interface{}, err error) { return b.box.Metadata.AsMap(), nil } func (b *Box) MetadataURL() (url string, err error) { return b.box.MetadataUrl, nil } func (b *Box) Name() (name string, err error) { return b.box.Name, nil } func (b *Box) Provider() (name string, err error) { return b.box.Provider, nil } // This repackages this box and outputs it to the given path. func (b *Box) Repackage(outputPath string) (err error) { b.logger.Trace("repackaging box", b.box.Name, "to", outputPath) tarFile, err := os.Create(outputPath) if err != nil { return err } defer tarFile.Close() tw := tar.NewWriter(tarFile) defer tw.Close() err = filepath.Walk(b.box.Directory, func(path string, info os.FileInfo, err error) error { header, err := tar.FileInfoHeader(info, path) if err != nil { return err } header.Name = filepath.ToSlash(path) if err := tw.WriteHeader(header); err != nil { return err } if !info.IsDir() { data, err := os.Open(path) if err != nil { return err } defer data.Close() if _, err := io.Copy(tw, data); err != nil { return err } } return nil }) if err != nil { return err } return } func (b *Box) Version() (version string, err error) { return b.box.Version, nil } func (b *Box) Compare(box core.Box) (int, error) { name, err := box.Name() if err != nil { return 0, err } ver, err := box.Version() if err != nil { return 0, err } provider, err := box.Provider() if err != nil { return 0, err } if b.box.Name == name && b.box.Provider == provider { boxVersion, err := version.NewVersion(b.box.Version) if err != nil { return 0, nil } otherVersion, err := version.NewVersion(ver) if err != nil { return 0, nil } res := otherVersion.Compare(boxVersion) return res, nil } return 0, errors.New("Box name and provider does not match, can't compare") } func (b *Box) ToProto() *vagrant_server.Box { return b.box } func (b *Box) Save() error { b.m.Lock() defer b.m.Unlock() b.logger.Trace("saving box to db", "box", b.box.Name) _, err := b.basis.client.UpsertBox( b.basis.ctx, &vagrant_server.UpsertBoxRequest{Box: b.box}, ) if err != nil { b.logger.Trace("filed to save box", "box", b.box.Name) } return err } var _ core.Box = (*Box)(nil)