From 777c8cb532855e3687835c0725ab1d3e59a8808d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Tue, 13 Oct 2026 13:11:59 +0200 Subject: [PATCH] feat: share env files in --demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/development.md | 5 ++++ internal/provider/fake/fake.go | 4 +++ internal/provider/fake/repos.go | 15 +++++++++++ internal/provider/provider.go | 6 +++++ internal/provider/repo/repo.go | 9 +++++++ internal/ui/app/sharefiles.go | 14 ++++------ internal/ui/app/sharefiles_test.go | 42 ++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 9 deletions(-) diff --git a/docs/development.md b/docs/development.md index 40e1a24..9fdede4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -7,6 +7,11 @@ go test ./... 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: ```sh diff --git a/internal/provider/fake/fake.go b/internal/provider/fake/fake.go index cd86071..d7b021c 100644 --- a/internal/provider/fake/fake.go +++ b/internal/provider/fake/fake.go @@ -331,6 +331,10 @@ func Azure(delay time.Duration) *Provider { }, demoCert) 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{ Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"), }, "smtp-demo-password") diff --git a/internal/provider/fake/repos.go b/internal/provider/fake/repos.go index 1dc5038..2757c62 100644 --- a/internal/provider/fake/repos.go +++ b/internal/provider/fake/repos.go @@ -134,3 +134,18 @@ func format(path string) string { } 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() +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7ad3fa8..a10ea66 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -189,6 +189,12 @@ type Files interface { 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. type File struct { Path string diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 234917f..1f53ad7 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -412,6 +412,15 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre 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. func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) { path := c.ID diff --git a/internal/ui/app/sharefiles.go b/internal/ui/app/sharefiles.go index faed1df..3f61a7e 100644 --- a/internal/ui/app/sharefiles.go +++ b/internal/ui/app/sharefiles.go @@ -40,9 +40,9 @@ type sealedMsg struct { } // 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 { - _, ok := m.providerFor(src).(provider.Files) + _, ok := m.providerFor(src).(provider.Contents) return src.Kind == provider.KindRepo && ok } @@ -50,7 +50,7 @@ func (m Model) canShareFiles(src provider.Source) bool { // plain ones. func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd { p := m.providerFor(src) - files, ok := p.(provider.Files) + files, ok := p.(provider.Contents) if !ok { m.flash = warning("these files can't be shared") return nil @@ -78,16 +78,12 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd } } for _, c := range cs { - f, err := files.File(c, nil) - if err == nil { - var b []byte - b, err = os.ReadFile(f.Path) - msg.files = append(msg.files, share.File{Path: c.Name, Data: b}) - } + b, err := files.Contents(ctx, c) if err != nil { msg.err = err return msg } + msg.files = append(msg.files, share.File{Path: c.Name, Data: b}) } key, ok, err := share.DefaultKey(src.ID, general) if ok { diff --git a/internal/ui/app/sharefiles_test.go b/internal/ui/app/sharefiles_test.go index 8535f95..526f8a7 100644 --- a/internal/ui/app/sharefiles_test.go +++ b/internal/ui/app/sharefiles_test.go @@ -10,6 +10,7 @@ import ( 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/repo" "git.kebcloud.no/keb/lazysecrets/internal/share" @@ -175,3 +176,44 @@ func TestShareLinkGarbled(t *testing.T) { 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) + } +}