feat: e opens a repo's SOPS or .env file, in nvim, sops edit or your editor
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6fab41703e
commit
c29319b821
11 files changed
+395
-12
No files matched your search
@@ -169,3 +169,18 @@ type Current interface {
|
||||
type Locator interface {
|
||||
Locate(ctx context.Context, address string) (sourceID string, ok bool, err error)
|
||||
}
|
||||
|
||||
// Files is implemented by providers whose containers are files on disk
|
||||
// (SOPS and .env files in repos), so they can be opened in an editor.
|
||||
type Files interface {
|
||||
// File is where c is, and for o (nil for the whole file) the line its
|
||||
// key is on.
|
||||
File(c Container, o *Object) (File, error)
|
||||
}
|
||||
|
||||
// File is a container's file, to open.
|
||||
type File struct {
|
||||
Path string
|
||||
Line int // 1-based; 0 for the top
|
||||
Sops string // the sops command to edit it with; empty for a plain file
|
||||
}
|
||||
@@ -412,6 +412,37 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
||||
return provider.NewSecretValue(stdout.Bytes()), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
if path == "" { // made from a link
|
||||
path = filepath.Join(c.Source.ID, filepath.FromSlash(c.Name))
|
||||
}
|
||||
f := provider.File{Path: path}
|
||||
if _, isSops := readMeta(path); isSops {
|
||||
f.Sops = p.s.Sops
|
||||
}
|
||||
if o == nil {
|
||||
return f, nil
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
keys, err := parseKeys(path, b)
|
||||
if err != nil {
|
||||
return f, nil // still worth opening, at the top
|
||||
}
|
||||
_, extract, _ := strings.Cut(o.ID, "\x00")
|
||||
for _, k := range keys {
|
||||
if k.extract == extract || (extract == "" && k.name == o.Name) {
|
||||
f.Line = k.line
|
||||
break
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// sopsError is sops' own message, whole: which keys it tried and why
|
||||
// each failed can all matter.
|
||||
func sopsError(stderr string, err error) string {
|
||||
@@ -871,6 +902,7 @@ type key struct {
|
||||
name string // db.password, list[0]
|
||||
extract string // sops --extract: ["db"]["password"]
|
||||
encrypted bool
|
||||
line int // 1-based
|
||||
}
|
||||
|
||||
// parseKeys lists a sops file's keys without decrypting anything.
|
||||
@@ -889,18 +921,18 @@ func parseKeys(file string, b []byte) ([]key, error) {
|
||||
return out, nil
|
||||
case "dotenv":
|
||||
var out []key
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
for i, line := range strings.Split(string(b), "\n") {
|
||||
k, v, ok := envLine(line)
|
||||
if !ok || strings.HasPrefix(k, "sops_") {
|
||||
continue
|
||||
}
|
||||
out = append(out, key{name: k, extract: quote(k), encrypted: strings.HasPrefix(v, "ENC[")})
|
||||
out = append(out, key{name: k, extract: quote(k), encrypted: strings.HasPrefix(v, "ENC["), line: i + 1})
|
||||
}
|
||||
return out, nil
|
||||
case "ini":
|
||||
var out []key
|
||||
section := ""
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
for i, line := range strings.Split(string(b), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";"):
|
||||
@@ -912,7 +944,7 @@ func parseKeys(file string, b []byte) ([]key, error) {
|
||||
continue
|
||||
}
|
||||
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
|
||||
out = append(out, key{name: section + "." + k, extract: quote(section) + quote(k), encrypted: strings.HasPrefix(v, "ENC[")})
|
||||
out = append(out, key{name: section + "." + k, extract: quote(section) + quote(k), encrypted: strings.HasPrefix(v, "ENC["), line: i + 1})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
@@ -944,7 +976,7 @@ func walk(n *yaml.Node, name, extract string, top bool, out *[]key) {
|
||||
walk(n.Alias, name, extract, false, out)
|
||||
}
|
||||
default:
|
||||
*out = append(*out, key{name: name, extract: extract, encrypted: strings.HasPrefix(n.Value, "ENC[")})
|
||||
*out = append(*out, key{name: name, extract: extract, encrypted: strings.HasPrefix(n.Value, "ENC["), line: n.Line})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package repo
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -435,3 +436,44 @@ func TestEnvFilesSkipPackages(t *testing.T) {
|
||||
t.Errorf("files %v, want %v (nothing from node_modules or vendor)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
root := gitRepo(t, map[string]string{
|
||||
".gitignore": ".env\n",
|
||||
"secrets/app.yaml": sopsYAML,
|
||||
})
|
||||
os.WriteFile(filepath.Join(root, ".env"), []byte("# local\nTOKEN=x\n\nURL=y\n"), 0o600)
|
||||
p := New(Settings{Dir: root, Sops: "/opt/sops"})
|
||||
ctx := context.Background()
|
||||
srcs, _ := p.Sources(ctx)
|
||||
files, err := p.Containers(ctx, srcs[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := map[string]int{}
|
||||
for _, c := range files {
|
||||
f, err := p.File(c, nil)
|
||||
if err != nil || f.Path != filepath.Join(root, c.Name) || f.Line != 0 {
|
||||
t.Errorf("%s: %+v, %v", c.Name, f, err)
|
||||
}
|
||||
if sops := f.Sops != ""; sops != (c.Name == "secrets/app.yaml") || (sops && f.Sops != "/opt/sops") {
|
||||
t.Errorf("%s: sops %q", c.Name, f.Sops)
|
||||
}
|
||||
keys, _ := p.Objects(ctx, c, provider.TypeSecret)
|
||||
for _, k := range keys {
|
||||
f, err := p.File(c, &k)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines[c.Name+" "+k.Name] = f.Line
|
||||
}
|
||||
}
|
||||
want := map[string]int{
|
||||
".env TOKEN": 2, ".env URL": 4,
|
||||
"secrets/app.yaml db.password": 2, "secrets/app.yaml db.port": 3,
|
||||
"secrets/app.yaml list[0]": 5, "secrets/app.yaml public_unencrypted": 6,
|
||||
}
|
||||
if !maps.Equal(lines, want) {
|
||||
t.Errorf("lines %v\nwant %v", lines, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user