feat: share env files in --demo
Providers whose containers are files give their contents through provider.Contents, which sharing reads instead of the disk. The demo repos build their env files from their keys, and the second demo account has a key to share with (kv-shared-prod / env-share), so S → f and opening the link work in --demo; the demo repos aren't on disk, so opening asks where to save. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7 files changed
+86
-9
No files matched your search
@@ -7,6 +7,11 @@ go test ./...
|
|||||||
go test ./internal/ui/app -run TestGolden -update # after intentional UI changes
|
go test ./internal/ui/app -run TestGolden -update # after intentional UI changes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In `--demo`, sharing env files works end to end: `S` → `f` on a demo
|
||||||
|
repo, pick `kv-shared-prod / env-share` (the second account's) as the
|
||||||
|
key, then paste the link back. The demo repos aren't on disk, so opening
|
||||||
|
asks where to save, as for a repo you haven't cloned.
|
||||||
|
|
||||||
A stripped binary is about 21 MB instead of 30 MB:
|
A stripped binary is about 21 MB instead of 30 MB:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -331,6 +331,10 @@ func Azure(delay time.Duration) *Provider {
|
|||||||
}, demoCert)
|
}, demoCert)
|
||||||
|
|
||||||
shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...)
|
shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...)
|
||||||
|
// A key to share env files with (S → f): random, as share.keyRules wants.
|
||||||
|
p.add(shared, provider.TypeSecret, provider.Object{
|
||||||
|
Name: "env-share", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "env-share"),
|
||||||
|
}, "q8Zt3VwL1mXc7Rk2Pj9Ys4Hn6Bf0Ga5Ud+Te/OiWlE=")
|
||||||
p.add(shared, provider.TypeSecret, provider.Object{
|
p.add(shared, provider.TypeSecret, provider.Object{
|
||||||
Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"),
|
Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"),
|
||||||
}, "smtp-demo-password")
|
}, "smtp-demo-password")
|
||||||
|
|||||||
@@ -134,3 +134,18 @@ func format(path string) string {
|
|||||||
}
|
}
|
||||||
return "dotenv"
|
return "dotenv"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ provider.Contents = (*Provider)(nil)
|
||||||
|
|
||||||
|
// Contents is a plain env file as KEY=value lines, from its keys, so the
|
||||||
|
// demo repos' env files can be shared.
|
||||||
|
func (p *Provider) Contents(ctx context.Context, c provider.Container) ([]byte, error) {
|
||||||
|
if c.Detail != provider.DetailPlainEnv {
|
||||||
|
return nil, provider.ErrUnsupported
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, o := range p.objects[listKey{c.ID, provider.TypeSecret}] {
|
||||||
|
b.WriteString(o.Name + "=" + p.values[o.ID] + "\n")
|
||||||
|
}
|
||||||
|
return []byte(b.String()), ctx.Err()
|
||||||
|
}
|
||||||
@@ -189,6 +189,12 @@ type Files interface {
|
|||||||
File(c Container, o *Object) (File, error)
|
File(c Container, o *Object) (File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contents is implemented by providers whose containers are files, so
|
||||||
|
// they can be shared (package share): the file as it is.
|
||||||
|
type Contents interface {
|
||||||
|
Contents(ctx context.Context, c Container) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
// File is a container's file, to open.
|
// File is a container's file, to open.
|
||||||
type File struct {
|
type File struct {
|
||||||
Path string
|
Path string
|
||||||
|
|||||||
@@ -412,6 +412,15 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
|||||||
return provider.NewSecretValue(stdout.Bytes()), nil
|
return provider.NewSecretValue(stdout.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contents reads the file c is.
|
||||||
|
func (p *Provider) Contents(_ context.Context, c provider.Container) ([]byte, error) {
|
||||||
|
f, err := p.File(c, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return os.ReadFile(f.Path)
|
||||||
|
}
|
||||||
|
|
||||||
// File is the file c is, and the line o's key is on.
|
// File is the file c is, and the line o's key is on.
|
||||||
func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) {
|
func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) {
|
||||||
path := c.ID
|
path := c.ID
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ type sealedMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// canShareFiles reports whether src's env files can be shared: it's a
|
// canShareFiles reports whether src's env files can be shared: it's a
|
||||||
// repo whose files are on disk.
|
// repo whose files can be read.
|
||||||
func (m Model) canShareFiles(src provider.Source) bool {
|
func (m Model) canShareFiles(src provider.Source) bool {
|
||||||
_, ok := m.providerFor(src).(provider.Files)
|
_, ok := m.providerFor(src).(provider.Contents)
|
||||||
return src.Kind == provider.KindRepo && ok
|
return src.Kind == provider.KindRepo && ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ func (m Model) canShareFiles(src provider.Source) bool {
|
|||||||
// plain ones.
|
// plain ones.
|
||||||
func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd {
|
func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd {
|
||||||
p := m.providerFor(src)
|
p := m.providerFor(src)
|
||||||
files, ok := p.(provider.Files)
|
files, ok := p.(provider.Contents)
|
||||||
if !ok {
|
if !ok {
|
||||||
m.flash = warning("these files can't be shared")
|
m.flash = warning("these files can't be shared")
|
||||||
return nil
|
return nil
|
||||||
@@ -78,16 +78,12 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, c := range cs {
|
for _, c := range cs {
|
||||||
f, err := files.File(c, nil)
|
b, err := files.Contents(ctx, c)
|
||||||
if err == nil {
|
|
||||||
var b []byte
|
|
||||||
b, err = os.ReadFile(f.Path)
|
|
||||||
msg.files = append(msg.files, share.File{Path: c.Name, Data: b})
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.err = err
|
msg.err = err
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
msg.files = append(msg.files, share.File{Path: c.Name, Data: b})
|
||||||
}
|
}
|
||||||
key, ok, err := share.DefaultKey(src.ID, general)
|
key, ok, err := share.DefaultKey(src.ID, general)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
||||||
@@ -175,3 +176,44 @@ func TestShareLinkGarbled(t *testing.T) {
|
|||||||
t.Errorf("not reported:\n%s", d.screen())
|
t.Errorf("not reported:\n%s", d.screen())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestShareInDemo shares a demo repo's env files with the demo key and
|
||||||
|
// opens the link, as lazysecrets --demo does: the demo repos aren't on
|
||||||
|
// disk, so it asks where to save.
|
||||||
|
func TestShareInDemo(t *testing.T) {
|
||||||
|
clip := &fakeClipboard{}
|
||||||
|
d := newDriverOpts(t, Options{Clipboard: clip},
|
||||||
|
fake.Azure(0), fake.AzureOps(0).As("Azure:admin", "Azure · admin"), fake.Repos())
|
||||||
|
d.wait = scryptWait
|
||||||
|
var src provider.Source
|
||||||
|
for _, s := range d.m.sourceResults["Repos"].sources {
|
||||||
|
if s.Here {
|
||||||
|
src = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.ID == "" {
|
||||||
|
t.Fatal("no demo repo")
|
||||||
|
}
|
||||||
|
src.Provider = "Repos"
|
||||||
|
d.run(d.m.shareFiles(src, nil))
|
||||||
|
if d.m.search == nil || d.m.search.pick == nil {
|
||||||
|
t.Fatalf("no picker: %s", d.m.flash.text)
|
||||||
|
}
|
||||||
|
d.send(tea.PasteMsg{Content: "https://kv-shared-prod.vault.azure.net/secrets/env-share"})
|
||||||
|
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
|
||||||
|
t.Fatalf("copied %q (%s)", clip.content, d.m.flash.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, _ := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
d.m.placeDir = dir
|
||||||
|
d.send(tea.PasteMsg{Content: clip.content})
|
||||||
|
d.keys("o")
|
||||||
|
if d.m.prompt == nil || d.m.prompt.input == nil || !strings.Contains(d.screen(), "isn't among your repos") {
|
||||||
|
t.Fatalf("doesn't ask where:\n%s", d.screen())
|
||||||
|
}
|
||||||
|
d.keys("enter", "w")
|
||||||
|
entries, _ := os.ReadDir(dir)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
t.Fatalf("nothing saved (%s)", d.m.flash.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user