Skip to content

Commit

Permalink
feat: add ddev_version_constraint for add-ons, remove `#ddev-nodisp…
Browse files Browse the repository at this point in the history
…lay`, fixes ddev#5969 (ddev#6433)
  • Loading branch information
stasadev authored Aug 1, 2024
1 parent 4beb1a6 commit 5eadabd
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 108 deletions.
8 changes: 8 additions & 0 deletions cmd/ddev/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ ddev get --remove ddev-someaddonname
}
}
}

if s.DdevVersionConstraint != "" {
err := ddevapp.CheckDdevVersionConstraint(s.DdevVersionConstraint, fmt.Sprintf("Unable to install the '%s' add-on", s.Name), "")
if err != nil {
util.Failed(err.Error())
}
}

if len(s.PreInstallActions) > 0 {
util.Success("\nExecuting pre-install actions:")
}
Expand Down
44 changes: 39 additions & 5 deletions cmd/ddev/cmd/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ package cmd

import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"testing"

"github.com/ddev/ddev/pkg/ddevapp"
"github.com/ddev/ddev/pkg/exec"
"github.com/ddev/ddev/pkg/fileutil"
"github.com/ddev/ddev/pkg/globalconfig"
copy2 "github.com/otiai10/copy"
"github.com/stretchr/testify/require"
"os"
"path/filepath"
"regexp"
"runtime"
"testing"

asrt "github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -246,6 +247,39 @@ func TestCmdGetDependencies(t *testing.T) {
require.NoError(t, err, "out=%s", out)
}

// TestCmdGetDdevVersionConstraint tests the ddev_version_constraint behavior is correct
func TestCmdGetDdevVersionConstraint(t *testing.T) {
assert := asrt.New(t)

origDir, _ := os.Getwd()
site := TestSites[0]
err := os.Chdir(site.Dir)
require.NoError(t, err)
app, err := ddevapp.GetActiveApp("")
require.NoError(t, err)

err = copy2.Copy(filepath.Join(origDir, "testdata", t.Name(), "project"), app.GetAppRoot())
require.NoError(t, err)

t.Cleanup(func() {
out, err := exec.RunHostCommand(DdevBin, "get", "--remove", "invalid_constraint_recipe")
assert.Error(err, "output='%s'", out)
out, err = exec.RunHostCommand(DdevBin, "get", "--remove", "valid_constraint_recipe")
assert.NoError(err, "output='%s'", out)
err = os.Chdir(origDir)
assert.NoError(err)
})

// Add-on with invalid constraint should not be installed
out, err := exec.RunHostCommand(DdevBin, "get", filepath.Join(origDir, "testdata", t.Name(), "invalid_constraint_recipe"))
require.Error(t, err, "out=%s", out)
require.Contains(t, out, "constraint is not valid")

// Add-on with valid constraint should be installed
out, err = exec.RunHostCommand(DdevBin, "get", filepath.Join(origDir, "testdata", t.Name(), "valid_constraint_recipe"))
require.NoError(t, err, "out=%s", out)
}

// getManifestFromLogs returns the manifest built from 'raw' section of
// ddev get <project> -j output
func getManifestFromLogs(t *testing.T, jsonOut string) map[string]interface{} {
Expand Down
3 changes: 0 additions & 3 deletions cmd/ddev/cmd/testdata/TestCmdGetComplex/recipe/install.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: sample_get

pre_install_actions:
- |
#ddev-nodisplay
echo "This action should not have any output"
- |
{{ $ddev := (env "DDEV_BINARY_FULLPATH") }}
{{ if not $ddev }} {{ $ddev = `ddev` }} {{end}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: invalid_constraint_recipe

ddev_version_constraint: ">= 1.twentythree"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: valid_constraint_recipe

ddev_version_constraint: "!= v0.0.0-overridden-by-make"
2 changes: 1 addition & 1 deletion docs/content/users/configuration/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Example: `dbimage_extra_packages: ["less"]` will add the `less` package when the
You can configure a [version constraint](https://github.com/Masterminds/semver#checking-version-constraints) for DDEV that will be validated against the running DDEV executable and prevent `ddev start` from running if it doesn't validate. For example:

```yaml
ddev_version_constraint: '>=v1.23.0-alpha1'
ddev_version_constraint: '>= v1.23.0-alpha1'
```
This is only supported with DDEV versions above v1.22.4; older DDEV versions will ignore this setting.
Expand Down
8 changes: 7 additions & 1 deletion docs/content/users/extend/additional-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,23 @@ Anyone can create an add-on for `ddev get`. See [this screencast](https://www.yo

The `install.yaml` is a simple YAML file with a few main sections:

* `name`: The add-on name. This will be used in `ddev get --remove` and similar commands. If the repository name is `ddev-solr`, for example, a good `name` will be `solr`.
* `pre_install_actions`: an array of Bash statements or scripts to be executed before `project_files` are installed. The actions are executed in the context of the target project’s root directory.
* `project_files`: an array of files or directories to be copied from the add-on into the target project’s `.ddev` directory.
* `global_files`: is an array of files or directories to be copied from the add-on into the target system’s global `.ddev` directory (`~/.ddev/`).
* `ddev_version_constraint` (available with DDEV v1.23.4+): a [version constraint](https://github.com/Masterminds/semver#checking-version-constraints) for DDEV that will be validated against the running DDEV executable and prevent add-on from being installed if it doesn't validate. For example:

```yaml
ddev_version_constraint: '>= v1.23.4'
```

* `dependencies`: an array of add-ons that this add-on depends on.
* `post_install_actions`: an array of Bash statements or scripts to be executed after `project_files` and `global_files` are installed. The actions are executed in the context of the target project’s `.ddev` directory.
* `removal_actions`: an array of Bash statements or scripts to be executed when the add-on is being removed with `ddev get --remove`. The actions are executed in the context of the target project’s `.ddev` directory.
* `yaml_read_files`: a map of `name: file` of YAML files to be read from the target project’s root directory. The contents of these YAML files may be used as templated actions within `pre_install_actions` and `post_install_actions`.

In any stanza of `pre_install_actions` and `post_install_actions` you can:

* Use `#ddev-nodisplay` on a line to suppress any output.
* Use `#ddev-description:<some description of what stanza is doing>` to instruct DDEV to output a description of the action it's taking.

You can see a simple `install.yaml` in [`ddev-addon-template`’s `install.yaml`](https://github.com/ddev/ddev-addon-template/blob/main/install.yaml).
Expand Down
17 changes: 9 additions & 8 deletions pkg/ddevapp/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ const AddonMetadataDir = "addon-metadata"
// Format of install.yaml
type InstallDesc struct {
// Name must be unique in a project; it will overwrite any existing add-on with the same name.
Name string `yaml:"name"`
ProjectFiles []string `yaml:"project_files"`
GlobalFiles []string `yaml:"global_files,omitempty"`
Dependencies []string `yaml:"dependencies,omitempty"`
PreInstallActions []string `yaml:"pre_install_actions,omitempty"`
PostInstallActions []string `yaml:"post_install_actions,omitempty"`
RemovalActions []string `yaml:"removal_actions,omitempty"`
YamlReadFiles map[string]string `yaml:"yaml_read_files"`
Name string `yaml:"name"`
ProjectFiles []string `yaml:"project_files"`
GlobalFiles []string `yaml:"global_files,omitempty"`
DdevVersionConstraint string `yaml:"ddev_version_constraint,omitempty"`
Dependencies []string `yaml:"dependencies,omitempty"`
PreInstallActions []string `yaml:"pre_install_actions,omitempty"`
PostInstallActions []string `yaml:"post_install_actions,omitempty"`
RemovalActions []string `yaml:"removal_actions,omitempty"`
YamlReadFiles map[string]string `yaml:"yaml_read_files"`
}

// format of the add-on manifest file
Expand Down
20 changes: 2 additions & 18 deletions pkg/ddevapp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"text/template"
"time"

"github.com/Masterminds/semver/v3"
"github.com/ddev/ddev/pkg/config/types"
"github.com/ddev/ddev/pkg/docker"
"github.com/ddev/ddev/pkg/dockerutil"
Expand Down Expand Up @@ -453,24 +452,9 @@ func (app *DdevApp) ValidateConfig() error {

// Validate ddev version constraint, if any
if app.DdevVersionConstraint != "" {
constraint := app.DdevVersionConstraint
if !strings.Contains(constraint, "-") {
// Allow pre-releases to be included in the constraint validation
// @see https://github.com/Masterminds/semver#working-with-prerelease-versions
constraint += "-0"
}
c, err := semver.NewConstraint(constraint)
err := CheckDdevVersionConstraint(app.DdevVersionConstraint, fmt.Sprintf("unable to start the '%s' project", app.Name), "or update the `ddev_version_constraint` in your .ddev/config.yaml file")
if err != nil {
return fmt.Errorf("the %s project has '%s' constraint that is not valid. See https://github.com/Masterminds/semver#checking-version-constraints for valid constraints format", app.Name, app.DdevVersionConstraint).(invalidConstraint)
}

// Make sure we do this check with valid released versions
v, err := semver.NewVersion(versionconstants.DdevVersion)
if err == nil {
if !c.Check(v) {
return fmt.Errorf("the %s project has a DDEV version constraint of '%s' and the version of DDEV you are using ('%s') does not meet the constraint. Please update the `ddev_version_constraint` in your .ddev/config.yaml or use a version of DDEV that meets the constraint",
app.Name, app.DdevVersionConstraint, versionconstants.DdevVersion)
}
return err
}
}

Expand Down
105 changes: 33 additions & 72 deletions pkg/ddevapp/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,79 +531,40 @@ func TestConfigValidate(t *testing.T) {
t.Fatalf("Failed to app.ValidateConfig(), err=%v", err)
}

app.DdevVersionConstraint = ">= 1.twentythree"
err = app.ValidateConfig()
assert.Error(err)
assert.Contains(err.Error(), "constraint that is not valid")
app.DdevVersionConstraint = ""

versionconstants.DdevVersion = "v1.22.0"
app.DdevVersionConstraint = ">= 1.23"
err = app.ValidateConfig()
assert.Error(err)
assert.Contains(err.Error(), "project has a DDEV version constraint of '>= 1.23' and the version of DDEV you are using ('v1.22.0') does not meet the constraint")
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "v1.23.0"
app.DdevVersionConstraint = ">= 1.23"
err = app.ValidateConfig()
assert.NoError(err)
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "2134asdf-dirty"
app.DdevVersionConstraint = ">= 1.23"
err = app.ValidateConfig()
assert.NoError(err)
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

// Testing out pre-releases and built PRs versions
versionconstants.DdevVersion = "v1.22.3-11-g8baef014e"
app.DdevVersionConstraint = ">= 1.23"
err = app.ValidateConfig()
assert.Error(err)
assert.Contains(err.Error(), "project has a DDEV version constraint of '>= 1.23' and the version of DDEV you are using ('v1.22.3-11-g8baef014e') does not meet the constraint")
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "v1.22.3-11-g8baef014e"
app.DdevVersionConstraint = ">= 1.22"
err = app.ValidateConfig()
assert.NoError(err)
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "v1.22.3-11-g8baef014e"
app.DdevVersionConstraint = ">= 1.23.0-0"
err = app.ValidateConfig()
assert.Error(err)
assert.Contains(err.Error(), "project has a DDEV version constraint of '>= 1.23.0-0' and the version of DDEV you are using ('v1.22.3-11-g8baef014e') does not meet the constraint")
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "v1.22.3-alpha2"
app.DdevVersionConstraint = ">= v1.22.3-alpha3"
err = app.ValidateConfig()
assert.Error(err)
assert.Contains(err.Error(), "project has a DDEV version constraint of '>= v1.22.3-alpha3' and the version of DDEV you are using ('v1.22.3-alpha2') does not meet the constraint")
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion

versionconstants.DdevVersion = "v1.22.3-beta1"
app.DdevVersionConstraint = ">= v1.22.3-alpha3"
err = app.ValidateConfig()
assert.NoError(err)
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion
testCases := []struct {
description string
ddevVersion string
versionConstraint string
error string
}{
{"Invalid constraint", ddevVersion, ">= 1.twentythree", "constraint is not valid"},
{"Lower version", "v1.22.0", ">= 1.23", "your DDEV version 'v1.22.0' doesn't meet the constraint '>= 1.23'"},
{"Equal version", "v1.23.0", ">= 1.23", ""},
{"Not semver version", "2134asdf-dirty", ">= 1.23", ""},
{"Lower semver version with suffix", "v1.22.3-11-g8baef014e", ">= 1.23", "your DDEV version 'v1.22.3-11-g8baef014e' doesn't meet the constraint '>= 1.23'"},
{"Equal semver version with suffix", "v1.22.3-11-g8baef014e", ">= 1.22", ""},
{"Constraint with suffix", "v1.22.3-11-g8baef014e", ">= 1.23.0-0", "your DDEV version 'v1.22.3-11-g8baef014e' doesn't meet the constraint '>= 1.23.0-0'"},
{"Lower prerelease version", "v1.22.3-alpha2", ">= v1.22.3-alpha3", "your DDEV version 'v1.22.3-alpha2' doesn't meet the constraint '>= v1.22.3-alpha3'"},
{"Greater prerelease version", "v1.22.3-beta1", ">= v1.22.3-alpha3", ""},
{"Compare with suffixes", "v1.22.3-11-g8baef014e", ">= 1.22.0-0", ""},
}

versionconstants.DdevVersion = "v1.22.3-11-g8baef014e"
app.DdevVersionConstraint = ">= 1.22.0-0"
err = app.ValidateConfig()
assert.NoError(err)
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
assert := asrt.New(t)
versionconstants.DdevVersion = tc.ddevVersion
app.DdevVersionConstraint = tc.versionConstraint
err = app.ValidateConfig()
if tc.error == "" {
assert.NoError(err)
} else {
assert.Error(err)
assert.Contains(err.Error(), tc.error)
}
app.DdevVersionConstraint = ""
versionconstants.DdevVersion = ddevVersion
})
}

app.Name = "Invalid!"
err = app.ValidateConfig()
Expand Down
27 changes: 27 additions & 0 deletions pkg/ddevapp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
"strings"
"text/template"

"github.com/Masterminds/semver/v3"
"github.com/Masterminds/sprig/v3"
"github.com/ddev/ddev/pkg/dockerutil"
"github.com/ddev/ddev/pkg/fileutil"
"github.com/ddev/ddev/pkg/globalconfig"
"github.com/ddev/ddev/pkg/nodeps"
"github.com/ddev/ddev/pkg/output"
"github.com/ddev/ddev/pkg/util"
"github.com/ddev/ddev/pkg/versionconstants"
dockerContainer "github.com/docker/docker/api/types/container"
dockerVersions "github.com/docker/docker/api/types/versions"
"github.com/jedib0t/go-pretty/v6/table"
Expand Down Expand Up @@ -534,3 +536,28 @@ func AppSliceToMap(appList []*DdevApp) map[string]*DdevApp {
}
return nameMap
}

// CheckDdevVersionConstraint returns an error if the given constraint does not match the current DDEV version
func CheckDdevVersionConstraint(constraint string, errorPrefix string, errorSuffix string) error {
normalizedConstraint := constraint
if !strings.Contains(normalizedConstraint, "-") {
// Allow pre-releases to be included in the constraint validation
// @see https://github.com/Masterminds/semver#working-with-prerelease-versions
normalizedConstraint += "-0"
}
if errorPrefix == "" {
errorPrefix = "error"
}
c, err := semver.NewConstraint(normalizedConstraint)
if err != nil {
return fmt.Errorf("%s: the '%s' constraint is not valid. See https://github.com/Masterminds/semver#checking-version-constraints for valid constraints format", errorPrefix, constraint).(invalidConstraint)
}
// Make sure we do this check with valid released versions
v, err := semver.NewVersion(versionconstants.DdevVersion)
if err == nil {
if !c.Check(v) {
return fmt.Errorf("%s: your DDEV version '%s' doesn't meet the constraint '%s'. Please update to a DDEV version that meets this constraint %s", errorPrefix, versionconstants.DdevVersion, constraint, strings.TrimSpace(errorSuffix))
}
}
return nil
}

0 comments on commit 5eadabd

Please sign in to comment.