Skip to content

Commit 230ddce

Browse files
authored
[plugins] Fix github canonical name (#1897)
## Summary Fixes #1848 github canonical names were `owner-repo` but this breaks when there are multiple plugins in a single repo and creates possible conflicts with other plugins. This renames them to: `owner.repo.name` where `name` is the name in the plugin.json. For backward compatibility we don't require the name to be present, if it's missing the name is the directory (with slashes replaced with hyphens) ## How was it tested? Installed a few plugins from the same repo and inspected virtenv dir.
1 parent 1373e65 commit 230ddce

File tree

4 files changed

+92
-26
lines changed

4 files changed

+92
-26
lines changed

internal/plugin/github.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,49 @@ import (
55
"io"
66
"net/http"
77
"net/url"
8+
"regexp"
9+
"strings"
810

11+
"github.com/pkg/errors"
12+
"github.com/samber/lo"
913
"go.jetpack.io/devbox/internal/boxcli/usererr"
1014
"go.jetpack.io/devbox/internal/cachehash"
1115
"go.jetpack.io/devbox/nix/flake"
1216
)
1317

1418
type githubPlugin struct {
15-
ref flake.Ref
19+
ref flake.Ref
20+
name string
21+
}
22+
23+
// Github only allows alphanumeric, hyphen, underscore, and period in repo names.
24+
// but we clean up just in case.
25+
var githubNameRegexp = regexp.MustCompile("[^a-zA-Z0-9-_.]+")
26+
27+
func newGithubPlugin(ref flake.Ref) (*githubPlugin, error) {
28+
plugin := &githubPlugin{ref: ref}
29+
// For backward compatibility, we don't strictly require name to be present
30+
// in github plugins. If it's missing, we just use the directory as the name.
31+
name, err := getPluginNameFromContent(plugin)
32+
if err != nil && !errors.Is(err, errNameMissing) {
33+
return nil, err
34+
}
35+
if name == "" {
36+
name = strings.ReplaceAll(ref.Dir, "/", "-")
37+
}
38+
plugin.name = githubNameRegexp.ReplaceAllString(
39+
strings.Join(lo.Compact([]string{ref.Owner, ref.Repo, name}), "."),
40+
" ",
41+
)
42+
return plugin, nil
1643
}
1744

1845
func (p *githubPlugin) Fetch() ([]byte, error) {
1946
return p.FileContent(pluginConfigName)
2047
}
2148

2249
func (p *githubPlugin) CanonicalName() string {
23-
return p.ref.Owner + "-" + p.ref.Repo
50+
return p.name
2451
}
2552

2653
func (p *githubPlugin) Hash() string {

internal/plugin/github_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package plugin
22

33
import (
4+
"strings"
45
"testing"
56

7+
"github.com/samber/lo"
68
"github.com/stretchr/testify/assert"
79
"go.jetpack.io/devbox/nix/flake"
810
)
@@ -23,6 +25,7 @@ func TestNewGithubPlugin(t *testing.T) {
2325
Owner: "jetpack-io",
2426
Repo: "devbox-plugins",
2527
},
28+
name: "jetpack-io.devbox-plugins",
2629
},
2730
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/master",
2831
},
@@ -36,6 +39,7 @@ func TestNewGithubPlugin(t *testing.T) {
3639
Repo: "devbox-plugins",
3740
Dir: "mongodb",
3841
},
42+
name: "jetpack-io.devbox-plugins.mongodb",
3943
},
4044
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/master/mongodb",
4145
},
@@ -50,6 +54,7 @@ func TestNewGithubPlugin(t *testing.T) {
5054
Ref: "my-branch",
5155
Dir: "mongodb",
5256
},
57+
name: "jetpack-io.devbox-plugins.mongodb",
5358
},
5459
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/my-branch/mongodb",
5560
},
@@ -64,18 +69,36 @@ func TestNewGithubPlugin(t *testing.T) {
6469
Ref: "initials/my-branch",
6570
Dir: "mongodb",
6671
},
72+
name: "jetpack-io.devbox-plugins.mongodb",
6773
},
6874
expectedURL: "https://raw.githubusercontent.com/jetpack-io/devbox-plugins/initials/my-branch/mongodb",
6975
},
7076
}
7177

7278
for _, testCase := range testCases {
7379
t.Run(testCase.name, func(t *testing.T) {
74-
actual, _ := parseIncludable(testCase.Include, "")
80+
actual, err := newGithubPluginForTest(testCase.Include)
81+
assert.NoError(t, err)
7582
assert.Equal(t, &testCase.expected, actual)
7683
u, err := testCase.expected.url("")
7784
assert.Nil(t, err)
7885
assert.Equal(t, testCase.expectedURL, u)
7986
})
8087
}
8188
}
89+
90+
// keep in sync with newGithubPlugin
91+
func newGithubPluginForTest(include string) (*githubPlugin, error) {
92+
ref, err := flake.ParseRef(include)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
plugin := &githubPlugin{ref: ref}
98+
name := strings.ReplaceAll(ref.Dir, "/", "-")
99+
plugin.name = githubNameRegexp.ReplaceAllString(
100+
strings.Join(lo.Compact([]string{ref.Owner, ref.Repo, name}), "."),
101+
" ",
102+
)
103+
return plugin, nil
104+
}

internal/plugin/includable.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package plugin
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"regexp"
57

8+
"go.jetpack.io/devbox/internal/boxcli/usererr"
69
"go.jetpack.io/devbox/nix/flake"
710
)
811

912
type Includable interface {
1013
CanonicalName() string
11-
Hash() string
1214
FileContent(subpath string) ([]byte, error)
15+
Hash() string
1316
LockfileKey() string
1417
}
1518

@@ -22,8 +25,41 @@ func parseIncludable(includableRef, workingDir string) (Includable, error) {
2225
case flake.TypePath:
2326
return newLocalPlugin(ref, workingDir)
2427
case flake.TypeGitHub:
25-
return &githubPlugin{ref: ref}, nil
28+
return newGithubPlugin(ref)
2629
default:
2730
return nil, fmt.Errorf("unsupported ref type %q", ref.Type)
2831
}
2932
}
33+
34+
type fetcher interface {
35+
Includable
36+
Fetch() ([]byte, error)
37+
}
38+
39+
var (
40+
nameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\- ]+$`)
41+
errNameMissing = usererr.New("'name' is missing")
42+
)
43+
44+
func getPluginNameFromContent(plugin fetcher) (string, error) {
45+
content, err := plugin.Fetch()
46+
if err != nil {
47+
return "", err
48+
}
49+
m := map[string]any{}
50+
if err := json.Unmarshal(content, &m); err != nil {
51+
return "", err
52+
}
53+
name, ok := m["name"].(string)
54+
if !ok || name == "" {
55+
return "",
56+
fmt.Errorf("%w in plugin %s", errNameMissing, plugin.LockfileKey())
57+
}
58+
if !nameRegex.MatchString(name) {
59+
return "", usererr.New(
60+
"plugin %s has an invalid name %q. Name must match %s",
61+
plugin.LockfileKey(), name, nameRegex,
62+
)
63+
}
64+
return name, nil
65+
}

internal/plugin/local.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package plugin
22

33
import (
4-
"encoding/json"
54
"os"
65
"path/filepath"
7-
"regexp"
86
"strings"
97

10-
"go.jetpack.io/devbox/internal/boxcli/usererr"
118
"go.jetpack.io/devbox/internal/cachehash"
129
"go.jetpack.io/devbox/nix/flake"
1310
)
@@ -18,29 +15,12 @@ type LocalPlugin struct {
1815
pluginDir string
1916
}
2017

21-
var nameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\- ]+$`)
22-
2318
func newLocalPlugin(ref flake.Ref, pluginDir string) (*LocalPlugin, error) {
2419
plugin := &LocalPlugin{ref: ref, pluginDir: pluginDir}
25-
content, err := plugin.Fetch()
20+
name, err := getPluginNameFromContent(plugin)
2621
if err != nil {
2722
return nil, err
2823
}
29-
m := map[string]any{}
30-
if err := json.Unmarshal(content, &m); err != nil {
31-
return nil, err
32-
}
33-
name, ok := m["name"].(string)
34-
if !ok || name == "" {
35-
return nil,
36-
usererr.New("plugin %s is missing a required field 'name'", plugin.Path())
37-
}
38-
if !nameRegex.MatchString(name) {
39-
return nil, usererr.New(
40-
"plugin %s has an invalid name %q. Name must match %s",
41-
plugin.Path(), name, nameRegex,
42-
)
43-
}
4424
plugin.name = name
4525
return plugin, nil
4626
}

0 commit comments

Comments
 (0)