feat: share env files in --demo
CI / Checks (pull_request) Successful in 42s
PR title / Conventional title (pull_request) Successful in 2s

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:
kebandClaude Opus 5.5 committed 2026-10-13 13:11:59 +02:00
1 parent 86fb79a915
commit 777c8cb532
7 files changed
+86 -9

No files matched your search

+5
View File
@@ -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
+4
View File
@@ -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")
+15
View File
@@ -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()
}
+6
View File
@@ -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
+9
View File
@@ -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
+5 -9
View File
@@ -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 {
+42
View File
@@ -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)
}
}