mirror of
https://github.com/foomo/posh-providers.git
synced 2025-10-16 12:35:41 +00:00
feat(usebruno/bruno): add provider
This commit is contained in:
parent
a24f934ee1
commit
e51a7d1d12
3
go.mod
3
go.mod
@ -11,9 +11,11 @@ require (
|
||||
github.com/cloudrecipes/packagejson v1.0.0
|
||||
github.com/digitalocean/godo v1.131.0
|
||||
github.com/foomo/posh v0.8.2
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1
|
||||
github.com/google/go-github/v47 v47.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/muesli/go-app-paths v0.2.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pterm/pterm v0.12.80
|
||||
github.com/samber/lo v1.47.0
|
||||
@ -53,6 +55,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-tty v0.0.3 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/neilotoole/slogt v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@ -53,6 +53,8 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@ -123,8 +125,12 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
|
||||
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
|
||||
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
|
||||
github.com/neilotoole/slogt v1.1.0 h1:c7qE92sq+V0yvCuaxph+RQ2jOKL61c4hqS1Bv9W7FZE=
|
||||
github.com/neilotoole/slogt v1.1.0/go.mod h1:RCrGXkPc/hYybNulqQrMHRtvlQ7F6NktNVLuLwk6V+w=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
|
||||
78
usebruno/bruno/README.md
Normal file
78
usebruno/bruno/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# POSH Bruno provider
|
||||
|
||||
## Usage
|
||||
|
||||
### Plugin
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/foomo/posh-providers/onepassword"
|
||||
"github.com/foomo/posh-providers/usebruno/bruno"
|
||||
"github.com/foomo/posh/pkg/cache"
|
||||
"github.com/foomo/posh/pkg/command"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
l log.Logger
|
||||
cache cache.Cache
|
||||
op *onepassword.OnePassword
|
||||
}
|
||||
|
||||
func New(l log.Logger) (plugin.Plugin, error) {
|
||||
var err error
|
||||
inst := &Plugin{
|
||||
l: l,
|
||||
cache: cache.MemoryCache{},
|
||||
commands: command.Commands{},
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
inst.op, err = onepassword.New(l, inst.cache)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create onepassword")
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
inst.commands.Add(bruno.NewCommand(l, bruno.CommandWithOnePassword(inst.op)))
|
||||
|
||||
// ...
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```yaml
|
||||
## Bruno
|
||||
bruno:
|
||||
path: '${PROJECT_ROOT}/.posh/bruno'
|
||||
```
|
||||
|
||||
### OnePassword
|
||||
|
||||
To inject secrets from 1Password, create a `bruno.env` file:
|
||||
|
||||
```text
|
||||
JWT_TOKEN=*********************
|
||||
```
|
||||
|
||||
Render the file to `.env`:
|
||||
|
||||
```shell
|
||||
> bruno env
|
||||
```
|
||||
|
||||
And use the secret in your environment `environments/local.bru`:
|
||||
|
||||
```text
|
||||
vars {
|
||||
host: http://localhost:5005
|
||||
jwtToken: {{process.env.JWT_TOKEN}}
|
||||
}
|
||||
```
|
||||
251
usebruno/bruno/command.go
Normal file
251
usebruno/bruno/command.go
Normal file
@ -0,0 +1,251 @@
|
||||
package bruno
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/foomo/posh-providers/onepassword"
|
||||
"github.com/foomo/posh/pkg/command/tree"
|
||||
"github.com/foomo/posh/pkg/log"
|
||||
"github.com/foomo/posh/pkg/prompt/goprompt"
|
||||
"github.com/foomo/posh/pkg/readline"
|
||||
"github.com/foomo/posh/pkg/shell"
|
||||
"github.com/foomo/posh/pkg/util/suggests"
|
||||
gap "github.com/muesli/go-app-paths"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type (
|
||||
Command struct {
|
||||
l log.Logger
|
||||
op *onepassword.OnePassword
|
||||
name string
|
||||
appName string
|
||||
config Config
|
||||
configKey string
|
||||
commandTree tree.Root
|
||||
}
|
||||
CommandOption func(*Command)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Options
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func CommandWithName(v string) CommandOption {
|
||||
return func(o *Command) {
|
||||
o.name = v
|
||||
}
|
||||
}
|
||||
|
||||
func CommandWithAppName(v string) CommandOption {
|
||||
return func(o *Command) {
|
||||
o.appName = v
|
||||
}
|
||||
}
|
||||
|
||||
func CommandWithConfigKey(v string) CommandOption {
|
||||
return func(o *Command) {
|
||||
o.configKey = v
|
||||
}
|
||||
}
|
||||
|
||||
func CommandWithOnePassword(v *onepassword.OnePassword) CommandOption {
|
||||
return func(o *Command) {
|
||||
o.op = v
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func NewCommand(l log.Logger, opts ...CommandOption) (*Command, error) {
|
||||
inst := &Command{
|
||||
l: l.Named("bruno"),
|
||||
name: "bruno",
|
||||
appName: "Bruno",
|
||||
configKey: "bruno",
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(inst)
|
||||
}
|
||||
}
|
||||
|
||||
if err := viper.UnmarshalKey(inst.configKey, &inst.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inst.commandTree = tree.New(&tree.Node{
|
||||
Name: inst.name,
|
||||
Description: "Run Bruno requests",
|
||||
Nodes: tree.Nodes{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List available requests",
|
||||
Execute: inst.list,
|
||||
},
|
||||
{
|
||||
Name: "env",
|
||||
Description: "Render secrets env",
|
||||
Execute: inst.env,
|
||||
},
|
||||
{
|
||||
Name: "run",
|
||||
Description: "Run the Bruno cli",
|
||||
Args: tree.Args{
|
||||
{
|
||||
Name: "env",
|
||||
Description: "Environment name",
|
||||
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
||||
return suggests.List(inst.config.Environments())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "request",
|
||||
Description: "Request to run",
|
||||
Repeat: true,
|
||||
Optional: true,
|
||||
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
||||
return suggests.List(inst.config.Requests())
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
||||
fs.Default().String("format", "", "Environment variables")
|
||||
fs.Default().String("env", "", "Overwrite a single environment variable")
|
||||
fs.Default().String("env-var", "", "Overwrite a single environment variable")
|
||||
fs.Default().Bool("bail", false, "Stop execution after a failure of a request, test, or assertion")
|
||||
fs.Default().Bool("insecure", false, "Allow insecure server connections")
|
||||
fs.Default().Bool("tests-only", false, "Only run requests that have tests")
|
||||
fs.Default().Bool("verbose", false, "Allow verbose output for debugging purpose")
|
||||
return nil
|
||||
},
|
||||
Execute: inst.run,
|
||||
},
|
||||
{
|
||||
Name: "open",
|
||||
Description: "Open the Bruno app",
|
||||
Execute: inst.open,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (c *Command) Name() string {
|
||||
return c.commandTree.Node().Name
|
||||
}
|
||||
|
||||
func (c *Command) Description() string {
|
||||
return c.commandTree.Node().Description
|
||||
}
|
||||
|
||||
func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
|
||||
return c.commandTree.Complete(ctx, r)
|
||||
}
|
||||
|
||||
func (c *Command) Execute(ctx context.Context, r *readline.Readline) error {
|
||||
return c.commandTree.Execute(ctx, r)
|
||||
}
|
||||
|
||||
func (c *Command) Help(ctx context.Context, r *readline.Readline) string {
|
||||
return c.commandTree.Help(ctx, r)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Private methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (c *Command) list(ctx context.Context, r *readline.Readline) error {
|
||||
t := pterm.TreeNode{
|
||||
Text: c.config.Filename() + ":",
|
||||
}
|
||||
{
|
||||
child := pterm.TreeNode{
|
||||
Text: "Environments:",
|
||||
}
|
||||
for _, request := range c.config.Environments() {
|
||||
child.Children = append(child.Children, pterm.TreeNode{
|
||||
Text: request,
|
||||
})
|
||||
}
|
||||
if len(child.Children) > 0 {
|
||||
t.Children = append(t.Children, child)
|
||||
}
|
||||
}
|
||||
{
|
||||
child := pterm.TreeNode{
|
||||
Text: "Requests:",
|
||||
}
|
||||
for _, request := range c.config.Requests() {
|
||||
child.Children = append(child.Children, pterm.TreeNode{
|
||||
Text: request,
|
||||
})
|
||||
}
|
||||
if len(child.Children) > 0 {
|
||||
t.Children = append(t.Children, child)
|
||||
}
|
||||
}
|
||||
return pterm.DefaultTree.WithRoot(t).Render()
|
||||
}
|
||||
|
||||
func (c *Command) run(ctx context.Context, r *readline.Readline) error {
|
||||
fs := r.FlagSets().Default()
|
||||
args := []string{
|
||||
"--env", r.Args().At(1),
|
||||
}
|
||||
if r.Args().LenGte(3) {
|
||||
args = append(args, r.Args().From(2)...)
|
||||
}
|
||||
return shell.New(ctx, c.l, "bru", "run").
|
||||
Args(args...).
|
||||
Args(fs.Visited().Args()...).
|
||||
Args(r.AdditionalArgs()...).
|
||||
Dir(c.config.Filename()).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *Command) env(ctx context.Context, r *readline.Readline) error {
|
||||
if c.op == nil {
|
||||
return errors.New("you must provide a one-password configuration")
|
||||
}
|
||||
|
||||
envFilename := path.Join(c.config.Filename(), ".env")
|
||||
templateFilename := path.Join(c.config.Filename(), "bruno.env")
|
||||
if _, err := os.Stat(templateFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.l.Info("rendering secrets file:", envFilename)
|
||||
return shell.New(ctx, c.l, "op", "inject", "-f", "-i", templateFilename, "-o", envFilename).Quiet().Run()
|
||||
}
|
||||
|
||||
func (c *Command) open(ctx context.Context, r *readline.Readline) error {
|
||||
prefFilename, err := gap.NewScope(gap.User, c.name).DataPath("preferences.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pref, err := NewPreferences(prefFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pref.AddLastOpenedCollection(c.config.Filename()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pref.Save(prefFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return shell.New(ctx, c.l, "open", "-a", c.appName).Run()
|
||||
}
|
||||
48
usebruno/bruno/config.go
Normal file
48
usebruno/bruno/config.go
Normal file
@ -0,0 +1,48 @@
|
||||
package bruno
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Path string `json:"path" yaml:"path" mapstructure:"path"`
|
||||
}
|
||||
|
||||
func (c Config) Filename() string {
|
||||
return os.ExpandEnv(c.Path)
|
||||
}
|
||||
|
||||
func (c Config) Environments() []string {
|
||||
entries, err := fs.Glob(os.DirFS(path.Join(c.Filename(), "environments")), "*.bru")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var ret []string
|
||||
for _, entry := range entries {
|
||||
ret = append(ret, strings.TrimSuffix(entry, ".bru"))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c Config) Requests() []string {
|
||||
var ret []string
|
||||
var files []string
|
||||
if value, err := fs.Glob(os.DirFS(c.Filename()), "*.bru"); err == nil {
|
||||
files = append(files, value...)
|
||||
}
|
||||
if value, err := fs.Glob(os.DirFS(c.Filename()), "**/*.bru"); err == nil {
|
||||
files = append(files, value...)
|
||||
}
|
||||
slices.Sort(files)
|
||||
for _, entry := range files {
|
||||
if !strings.HasPrefix(entry, "environments") {
|
||||
ret = append(ret, entry)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
56
usebruno/bruno/preferences.go
Normal file
56
usebruno/bruno/preferences.go
Normal file
@ -0,0 +1,56 @@
|
||||
package bruno
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Preferences struct {
|
||||
path string
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func NewPreferences(path string) (*Preferences, error) {
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(file, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Preferences{
|
||||
path: path,
|
||||
data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Preferences) AddLastOpenedCollection(path string) error {
|
||||
if _, ok := p.data["lastOpenedCollections"]; !ok {
|
||||
p.data["lastOpenedCollections"] = make(map[string][]any)
|
||||
}
|
||||
if lastOpenedCollections, ok := p.data["lastOpenedCollections"].([]any); ok {
|
||||
var lastOpenedCollectionsStrings []string
|
||||
if err := mapstructure.Decode(lastOpenedCollections, &lastOpenedCollectionsStrings); err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(lastOpenedCollectionsStrings, path) {
|
||||
lastOpenedCollections = append(lastOpenedCollections, path)
|
||||
p.data["lastOpenedCollections"] = lastOpenedCollections
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Preferences) Save(path string) error {
|
||||
data, err := json.MarshalIndent(p.data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user