Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmd/satisfy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func main() {
checkMain(os.Args[2:])
case "latest":
latestMain(os.Args[2:])
case "tags":
tagsMain(os.Args[2:])
case "version":
versionMain()
case "download":
Expand Down Expand Up @@ -58,6 +60,15 @@ func downloadMain(args []string) {
transfer.NewDownloadApp(config).Run()
}

func tagsMain(args []string) {
loader := core.NewTagsConfigLoader(shell.NewDiskFileSystem(""), shell.NewEnvironment(), os.Stdin, os.Stderr)
config, err := loader.LoadConfig(args)
if err != nil {
log.Fatal(err)
}
transfer.NewTagsApp(config).Run()
}

func latestMain(args []string) {
config, err := transfer.ParseLatestConfig(args)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions contracts/config_tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package contracts

import (
"net/url"
"path"

"github.com/smarty/gcs"
)

type TagsConfig struct {
MaxRetry int
GoogleCredentials gcs.Credentials
JSONPath string
Modification TagModificationConfig
}

type TagModificationConfig struct {
PackageName string `json:"package_name"`
RemoteAddress *URL `json:"remote_address"`
Add []Tag `json:"add"`
Delete []Tag `json:"delete"` // only the name of each entry is considered
}

func (this TagModificationConfig) ComposeRootManifestAddress() url.URL {
address := url.URL(*this.RemoteAddress)
address.Path = path.Join("/", address.Path, this.PackageName, RemoteManifestFilename)
return address
}

func (this TagModificationConfig) ComposeVersionedManifestAddress(version string) url.URL {
return AppendRemotePath(url.URL(*this.RemoteAddress), this.PackageName, version, RemoteManifestFilename)
}
17 changes: 9 additions & 8 deletions contracts/config_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ type UploadConfig struct {
}

type PackageConfig struct {
CompressionAlgorithm string `json:"compression_algorithm"`
CompressionLevel int `json:"compression_level"`
SourceDirectory string `json:"source_directory"`
SourceFile string `json:"source_file"`
SourcePath string `json:"source_path"`
PackageName string `json:"package_name"`
PackageVersion string `json:"package_version"`
RemoteAddressPrefix *URL `json:"remote_address"`
CompressionAlgorithm string `json:"compression_algorithm"`
CompressionLevel int `json:"compression_level"`
SourceDirectory string `json:"source_directory"`
SourceFile string `json:"source_file"`
SourcePath string `json:"source_path"`
PackageName string `json:"package_name"`
PackageVersion string `json:"package_version"`
RemoteAddressPrefix *URL `json:"remote_address"`
Tags []string `json:"tags,omitempty"`
}

func (this PackageConfig) ComposeRemoteAddress(filename string) url.URL {
Expand Down
1 change: 1 addition & 0 deletions contracts/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type IntegrityCheck interface {

type PackageInstaller interface {
DownloadManifest(remoteAddress url.URL) (manifest Manifest, err error)
ManifestExists(remoteAddress url.URL) (exists bool, err error)
InstallManifest(request InstallationRequest) (manifest Manifest, err error)
InstallPackage(manifest Manifest, request InstallationRequest) error
}
15 changes: 15 additions & 0 deletions contracts/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ type Manifest struct {
Name string `json:"name"` //a-z 0-9 _-/
Version string `json:"version"`
Archive Archive `json:"archive"`
Tags []Tag `json:"tags,omitempty"` // only present in the root (latest) manifest
}

type Tag struct {
Name string `json:"name"`
Version string `json:"version"`
}

func (this Manifest) TagVersion(name string) (version string, found bool) {
for _, tag := range this.Tags {
if tag.Name == name {
return tag.Version, true
}
}
return "", false
}

type Archive struct {
Expand Down
36 changes: 36 additions & 0 deletions contracts/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ func (this *ManifestFixture) TestMarshalManifest() {
this.So(clone, should.Resemble, original)
}

func (this *ManifestFixture) TestMarshalManifestWithTags() {
original := Manifest{
Name: "package-name",
Version: "1.2.3",
Tags: []Tag{
{Name: "stable", Version: "1.2.2"},
{Name: "experimental", Version: "1.2.3"},
},
}
clone := this.unmarshal(this.marshal(original))
this.So(clone, should.Resemble, original)
}

func (this *ManifestFixture) TestTagsOmittedFromJSONWhenEmpty() {
raw := this.marshal(Manifest{Name: "package-name", Version: "1.2.3"})
this.So(string(raw), should.NotContainSubstring, "tags")
}

func (this *ManifestFixture) TestTagVersion() {
manifest := Manifest{
Version: "1.2.3",
Tags: []Tag{
{Name: "stable", Version: "1.2.2"},
{Name: "experimental", Version: "1.2.3"},
},
}

version, found := manifest.TagVersion("stable")
this.So(found, should.BeTrue)
this.So(version, should.Equal, "1.2.2")

version, found = manifest.TagVersion("nope")
this.So(found, should.BeFalse)
this.So(version, should.BeBlank)
}

func (this *ManifestFixture) unmarshal(raw []byte) Manifest {
var clone Manifest
err := json.Unmarshal(raw, &clone)
Expand Down
6 changes: 6 additions & 0 deletions contracts/remote_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
Expand Down Expand Up @@ -51,6 +52,11 @@ func AppendRemotePath(prefix url.URL, packageName, version, fileName string) url

var RetryErr = errors.New("retry")

func IsNotFound(err error) bool {
var statusError *StatusCodeError
return errors.As(err, &statusError) && statusError.StatusCode() == http.StatusNotFound
}

type StatusCodeError struct {
actualStatusCode int
expectedStatusCode []int
Expand Down
81 changes: 72 additions & 9 deletions core/dependency_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ func (this *DependencyResolver) Resolve() error {
return err
}

if this.isInstalledCorrectly(localManifest) {
installed, err := this.isInstalledCorrectly(localManifest)
if err != nil {
return err
}
if installed {
return nil
}

Expand Down Expand Up @@ -80,34 +84,78 @@ func (this *DependencyResolver) localManifestExists(manifestPath string) bool {
return !os.IsNotExist(err)
}

func (this *DependencyResolver) isInstalledCorrectly(localManifest contracts.Manifest) bool {
func (this *DependencyResolver) isInstalledCorrectly(localManifest contracts.Manifest) (bool, error) {
if localManifest.Name != this.dependency.PackageName {
if strings.HasSuffix(localManifest.Name, "/"+this.dependency.PackageName) {
// no-op
} else {
log.Printf("incorrect package installed (%s), proceeding to installation of specified package: %s",
localManifest.Name, this.dependency.Title())
return false
return false, nil
}
}
if this.dependency.PackageVersion == "latest" && !this.localManifestIsLatest(localManifest) {
log.Printf("incorrect version installed (%s), proceeding to installation of specified package: %s",
localManifest.Version, this.dependency.Title())
return false
return false, nil
} else if this.dependency.PackageVersion != "latest" && localManifest.Version != this.dependency.PackageVersion {
log.Printf("incorrect version installed (%s), proceeding to installation of specified package: %s",
localManifest.Version, this.dependency.Title())
return false
matchesTag, err := this.localManifestMatchesRequestedTag(localManifest)
if err != nil {
return false, err
}
if !matchesTag {
log.Printf("incorrect version installed (%s), proceeding to installation of specified package: %s",
localManifest.Version, this.dependency.Title())
return false, nil
}
}

verifyErr := this.integrityChecker.Verify(localManifest, this.dependency.LocalDirectory)
if verifyErr != nil {
log.Printf("%s in %s", verifyErr.Error(), this.dependency.Title())
return false
return false, nil
}

log.Printf("Dependency already installed: %s", this.dependency.Title())
return true
return true, nil
}

// localManifestMatchesRequestedTag is consulted when the requested version does
// not match the locally installed version. A remote manifest at the literal
// version path takes precedence over a tag of the same name; only when no such
// version exists is the requested string resolved against the root manifest tags.
func (this *DependencyResolver) localManifestMatchesRequestedTag(localManifest contracts.Manifest) (bool, error) {
exists, err := this.packageInstaller.ManifestExists(this.dependency.ComposeRemoteAddress(contracts.RemoteManifestFilename))
if err != nil {
return false, fmt.Errorf("failed to check for manifest of %s: %w", this.dependency.Title(), err)
}
if exists {
return false, nil
}

version, err := this.resolveTag()
if err != nil {
return false, err
}
this.dependency.PackageVersion = version
return version == localManifest.Version, nil
}

// resolveTag translates the requested version into a concrete version by way of
// the tag listing in the root manifest.
func (this *DependencyResolver) resolveTag() (string, error) {
requested := this.dependency.PackageVersion
rootManifest, err := this.packageInstaller.DownloadManifest(this.dependency.ComposeLatestManifestRemoteAddress())
if err != nil {
return "", fmt.Errorf("failed to download root manifest while resolving %q for package %q: %w",
requested, this.dependency.PackageName, err)
}
version, found := rootManifest.TagVersion(requested)
if !found {
return "", fmt.Errorf("no version or tag %q exists for package %q", requested, this.dependency.PackageName)
}
log.Printf("Resolved tag %q to version %q for package %q", requested, version, this.dependency.PackageName)
return version, nil
}

func (this *DependencyResolver) installPackage() error {
Expand All @@ -117,6 +165,21 @@ func (this *DependencyResolver) installPackage() error {
LocalPath: this.dependency.LocalDirectory,
PackageName: this.dependency.PackageName,
})
if contracts.IsNotFound(err) && this.dependency.PackageVersion != "latest" {
// No manifest exists at the literal version path; the requested
// version may be a tag defined in the root manifest.
var version string
version, err = this.resolveTag()
if err != nil {
return fmt.Errorf("failed to install manifest for %s: %w", this.dependency.Title(), err)
}
this.dependency.PackageVersion = version
manifest, err = this.packageInstaller.InstallManifest(contracts.InstallationRequest{
RemoteAddress: this.dependency.ComposeRemoteManifestAddress(),
LocalPath: this.dependency.LocalDirectory,
PackageName: this.dependency.PackageName,
})
}
if err != nil {
return fmt.Errorf("failed to install manifest for %s: %w", this.dependency.Title(), err)
}
Expand Down
Loading