Skip to content

Commit

Permalink
[plugins] Use reflikes for plugins (#1845)
Browse files Browse the repository at this point in the history
## Summary

This changes plugins to use new `reflike` struct. It brings it more in
line with how flake refs work. For now, only some types are supported.

## How was it tested?

CICD
  • Loading branch information
mikeland73 committed Feb 29, 2024
1 parent c3efb8f commit 1cb6e09
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 160 deletions.
3 changes: 2 additions & 1 deletion examples/plugins/github/devbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"include": [
"github:jetpack-io/devbox-plugin-example",
"github:jetpack-io/devbox-plugin-example?dir=custom-dir"
"github:jetpack-io/devbox-plugin-example?dir=custom-dir",
"github:jetpack-io/devbox-plugin-example/test/branch",
]
}
3 changes: 3 additions & 0 deletions examples/plugins/github/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ else
echo "ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'"
exit 1
fi

echo BRANCH_ENV_VAR=$BRANCH_ENV_VAR
if [ "$BRANCH_ENV_VAR" != "I AM A BRANCH VAR" ]; then exit 1; fi;
8 changes: 6 additions & 2 deletions internal/plugin/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ func getConfigIfAny(pkg Includable, projectDir string) (*config, error) {
case *devpkg.Package:
return getBuiltinPluginConfigIfExists(pkg, projectDir)
case *githubPlugin:
return pkg.buildConfig(projectDir)
content, err := pkg.Fetch()
if err != nil {
return nil, errors.WithStack(err)
}
return buildConfig(pkg, projectDir, string(content))
case *localPlugin:
content, err := os.ReadFile(pkg.path)
content, err := os.ReadFile(pkg.ref.Path)
if err != nil && !os.IsNotExist(err) {
return nil, errors.WithStack(err)
}
Expand Down
79 changes: 21 additions & 58 deletions internal/plugin/github.go
Original file line number Diff line number Diff line change
@@ -1,59 +1,25 @@
package plugin

import (
"cmp"
"io"
"net/http"
"net/url"
"strings"

"github.com/pkg/errors"
"go.jetpack.io/devbox/internal/boxcli/usererr"
"go.jetpack.io/devbox/internal/cachehash"
)

type githubPlugin struct {
raw string
org string
repo string
revision string
dir string
ref RefLike
}

// newGithubPlugin returns a plugin that is hosted on github.
// url is of the form org/repo?dir=<dir>
// The (optional) dir must have a plugin.json"
func newGithubPlugin(rawURL string) (*githubPlugin, error) {
pluginURL, err := url.Parse(rawURL)
if err != nil {
return nil, err
}

parts := strings.SplitN(pluginURL.Path, "/", 3)

if len(parts) < 2 {
return nil, usererr.New(
"invalid github plugin url %q. Must be of the form org/repo/[revision]",
rawURL,
)
}

plugin := &githubPlugin{
raw: rawURL,
org: parts[0],
repo: parts[1],
revision: "master",
dir: pluginURL.Query().Get("dir"),
}

if len(parts) == 3 {
plugin.revision = parts[2]
}

return plugin, nil
func (p *githubPlugin) Fetch() ([]byte, error) {
return p.FileContent(pluginConfigName)
}

func (p *githubPlugin) CanonicalName() string {
return p.org + "-" + p.repo
return p.ref.Owner + "-" + p.ref.Repo
}

func (p *githubPlugin) Hash() string {
Expand All @@ -62,16 +28,7 @@ func (p *githubPlugin) Hash() string {
}

func (p *githubPlugin) FileContent(subpath string) ([]byte, error) {
// Github redirects "master" to "main" in new repos. They don't do the reverse
// so setting master here is better.
contentURL, err := url.JoinPath(
"https://raw.githubusercontent.com/",
p.org,
p.repo,
p.revision,
p.dir,
subpath,
)
contentURL, err := p.url(subpath)
if err != nil {
return nil, err
}
Expand All @@ -83,19 +40,25 @@ func (p *githubPlugin) FileContent(subpath string) ([]byte, error) {
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, usererr.New(
"failed to get plugin github:%s (Status code %d). \nPlease make sure a "+
"plugin.json file exists in plugin directory.",
p.raw,
"failed to get plugin github:%s @ %s (Status code %d). \nPlease make "+
"sure a plugin.json file exists in plugin directory.",
p.ref.String(),
contentURL,
res.StatusCode,
)
}
return io.ReadAll(res.Body)
}

func (p *githubPlugin) buildConfig(projectDir string) (*config, error) {
content, err := p.FileContent("plugin.json")
if err != nil {
return nil, errors.WithStack(err)
}
return buildConfig(p, projectDir, string(content))
func (p *githubPlugin) url(subpath string) (string, error) {
// Github redirects "master" to "main" in new repos. They don't do the reverse
// so setting master here is better.
return url.JoinPath(
"https://raw.githubusercontent.com/",
p.ref.Owner,
p.ref.Repo,
cmp.Or(p.ref.Rev, p.ref.Ref.Ref, "master"),
p.ref.Dir,
subpath,
)
}
86 changes: 59 additions & 27 deletions internal/plugin/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,90 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"go.jetpack.io/devbox/nix/flake"
)

func TestNewGithubPlugin(t *testing.T) {
testCases := []struct {
name string
expected githubPlugin
name string
Include string
expected githubPlugin
expectedURL string
}{
{
name: "parse basic github plugin",
name: "parse basic github plugin",
Include: "github:jetpack-io/devbox-plugins",
expected: githubPlugin{
raw: "jetpack-io/devbox-plugins",
org: "jetpack-io",
repo: "devbox-plugins",
revision: "master",
ref: RefLike{
Ref: flake.Ref{
Type: "github",
Owner: "jetpack-io",
Repo: "devbox-plugins",
},
filename: pluginConfigName,
},
},
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/master",
},
{
name: "parse github plugin with dir param",
name: "parse github plugin with dir param",
Include: "github:jetpack-io/devbox-plugins?dir=mongodb",
expected: githubPlugin{
raw: "jetpack-io/devbox-plugins?dir=mongodb",
org: "jetpack-io",
repo: "devbox-plugins",
revision: "master",
dir: "mongodb",
ref: RefLike{
Ref: flake.Ref{
Type: "github",
Owner: "jetpack-io",
Repo: "devbox-plugins",
Dir: "mongodb",
},
filename: pluginConfigName,
},
},
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/master/mongodb",
},
{
name: "parse github plugin with dir param and rev",
name: "parse github plugin with dir param and rev",
Include: "github:jetpack-io/devbox-plugins/my-branch?dir=mongodb",
expected: githubPlugin{
raw: "jetpack-io/devbox-plugins/my-branch?dir=mongodb",
org: "jetpack-io",
repo: "devbox-plugins",
revision: "my-branch",
dir: "mongodb",
ref: RefLike{
Ref: flake.Ref{
Type: "github",
Owner: "jetpack-io",
Repo: "devbox-plugins",
Ref: "my-branch",
Dir: "mongodb",
},
filename: pluginConfigName,
},
},
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/my-branch/mongodb",
},
{
name: "parse github plugin with dir param and rev",
name: "parse github plugin with dir param and rev",
Include: "github:jetpack-io/devbox-plugins/initials/my-branch?dir=mongodb",
expected: githubPlugin{
raw: "jetpack-io/devbox-plugins/initials/my-branch?dir=mongodb",
org: "jetpack-io",
repo: "devbox-plugins",
revision: "initials/my-branch",
dir: "mongodb",
ref: RefLike{
Ref: flake.Ref{
Type: "github",
Owner: "jetpack-io",
Repo: "devbox-plugins",
Ref: "initials/my-branch",
Dir: "mongodb",
},
filename: pluginConfigName,
},
},
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/initials/my-branch/mongodb",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual, _ := newGithubPlugin(testCase.expected.raw)
assert.Equal(t, actual, &testCase.expected)
actual, _ := parseReflike(testCase.Include)
assert.Equal(t, &testCase.expected, actual)
u, err := testCase.expected.url("")
assert.Nil(t, err)
assert.Equal(t, testCase.expectedURL, u)
})
}
}
74 changes: 2 additions & 72 deletions internal/plugin/includes.go
Original file line number Diff line number Diff line change
@@ -1,84 +1,14 @@
package plugin

import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"strings"

"go.jetpack.io/devbox/internal/boxcli/usererr"
"go.jetpack.io/devbox/internal/cachehash"
"go.jetpack.io/devbox/internal/devpkg"
)

type Includable interface {
CanonicalName() string
Hash() string
FileContent(subpath string) ([]byte, error)
}

func (m *Manager) ParseInclude(include string) (Includable, error) {
includeType, name, _ := strings.Cut(include, ":")
if name == "" {
return nil, usererr.New("include name is required")
} else if includeType == "plugin" {
if t, name, _ := strings.Cut(include, ":"); t == "plugin" {
return devpkg.PackageFromStringWithDefaults(name, m.lockfile), nil
} else if includeType == "path" {
absPath := filepath.Join(m.ProjectDir(), name)
return newLocalPlugin(absPath)
} else if includeType == "github" {
return newGithubPlugin(name)
}
return nil, usererr.New("unknown include type %q", includeType)
}

type localPlugin struct {
name string
path string
}

var nameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\- ]+$`)

func newLocalPlugin(path string) (*localPlugin, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
m := map[string]any{}
if err := json.Unmarshal(content, &m); err != nil {
return nil, err
}
name, ok := m["name"].(string)
if !ok || name == "" {
return nil,
usererr.New("plugin %s is missing a required field 'name'", path)
}
if !nameRegex.MatchString(name) {
return nil, usererr.New(
"plugin %s has an invalid name %q. Name must match %s",
path, name, nameRegex,
)
}
return &localPlugin{
name: name,
path: path,
}, nil
}

func (l *localPlugin) CanonicalName() string {
return l.name
}

func (l *localPlugin) IsLocal() bool {
return true
}

func (l *localPlugin) Hash() string {
h, _ := cachehash.Bytes([]byte(l.path))
return h
}

func (l *localPlugin) FileContent(subpath string) ([]byte, error) {
return os.ReadFile(filepath.Join(filepath.Dir(l.path), subpath))
return parseReflike(include)
}
Loading

0 comments on commit 1cb6e09

Please sign in to comment.