Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 76 additions & 5 deletions cmd/zb/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ import (
"sync"

jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/tailscale/hujson"
"zb.256lights.llc/pkg/internal/jsonrpc"
"zb.256lights.llc/pkg/internal/zbstorerpc"
"zb.256lights.llc/pkg/zbstore"
)

type globalConfig struct {
Debug bool `json:"debug"`
Directory zbstore.Directory `json:"storeDirectory"`
StoreSocket string `json:"storeSocket"`
CacheDB string `json:"cacheDB"`
AllowEnv stringAllowList `json:"allowEnvironment"`
Debug bool `json:"debug"`
Directory zbstore.Directory `json:"storeDirectory"`
StoreSocket string `json:"storeSocket"`
CacheDB string `json:"cacheDB"`
AllowEnv stringAllowList `json:"allowEnvironment"`
TrustedPublicKeys []*zbstore.RealizationPublicKey `json:"trustedPublicKeys"`
}

// defaultGlobalConfig returns
Expand Down Expand Up @@ -77,6 +79,68 @@ func (g *globalConfig) mergeFiles(paths iter.Seq[string]) error {
return nil
}

// UnmarshalJSONFrom unmarshals the configuration object from the JSON decoder,
// merging any fields in the JSON object with existing values.
func (g *globalConfig) UnmarshalJSONFrom(in *jsontext.Decoder) error {
tok, err := in.ReadToken()
if err != nil {
return err
}
if got := tok.Kind(); got != '{' {
return fmt.Errorf("config must be an object not a %v", got)
}

for {
keyToken, err := in.ReadToken()
if err != nil {
return err
}
switch kind := keyToken.Kind(); kind {
case '}':
return nil
case '"':
// Keep going.
default:
return fmt.Errorf("unexpected non-string key (%v) in object", kind)
}

switch k := keyToken.String(); k {
case "debug":
if err := jsonv2.UnmarshalDecode(in, &g.Debug); err != nil {
return fmt.Errorf("unmarshal config.debug: %w", err)
}
case "storeDirectory":
if err := jsonv2.UnmarshalDecode(in, &g.Directory); err != nil {
return fmt.Errorf("unmarshal config.storeDirectory: %w", err)
}
case "storeSocket":
if err := jsonv2.UnmarshalDecode(in, &g.StoreSocket); err != nil {
return fmt.Errorf("unmarshal config.storeSocket: %w", err)
}
case "cacheDB":
if err := jsonv2.UnmarshalDecode(in, &g.CacheDB); err != nil {
return fmt.Errorf("unmarshal config.cacheDB: %w", err)
}
case "allowEnvironment":
if err := jsonv2.UnmarshalDecode(in, &g.AllowEnv); err != nil {
return fmt.Errorf("unmarshal config.allowEnvironment: %w", err)
}
case "trustedPublicKeys":
// Use any unused capacity at end of the slice.
newKeys := g.TrustedPublicKeys[len(g.TrustedPublicKeys):]

if err := jsonv2.UnmarshalDecode(in, &newKeys); err != nil {
return fmt.Errorf("unmarshal config.trustedPublicKeys: %w", err)
}
g.TrustedPublicKeys = append(g.TrustedPublicKeys, newKeys...)
default:
if reject, _ := jsonv2.GetOption(in.Options(), jsonv2.RejectUnknownMembers); reject {
return fmt.Errorf("unmarshal config: unknown field %q", k)
}
}
}
}

func (g *globalConfig) validate() error {
if !filepath.IsAbs(string(g.Directory)) {
// The directory must be in the format of the local OS.
Expand All @@ -92,6 +156,13 @@ func (g *globalConfig) validate() error {
return nil
}

func (g *globalConfig) reusePolicy() *zbstorerpc.ReusePolicy {
if len(g.TrustedPublicKeys) == 0 {
return &zbstorerpc.ReusePolicy{All: true}
}
return &zbstorerpc.ReusePolicy{PublicKeys: g.TrustedPublicKeys}
}

func (g *globalConfig) storeClient(opts *zbstorerpc.CodecOptions) (_ *jsonrpc.Client, wait func()) {
var wg sync.WaitGroup
c := jsonrpc.NewClient(func(ctx context.Context) (jsonrpc.ClientCodec, error) {
Expand Down
66 changes: 65 additions & 1 deletion cmd/zb/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"slices"
"testing"

jsonv2 "github.com/go-json-experiment/json"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"zb.256lights.llc/pkg/zbstore"
)

func TestDefaultGlobalConfig(t *testing.T) {
Expand Down Expand Up @@ -40,6 +43,34 @@ func TestGlobalConfigMergeFiles(t *testing.T) {
Directory: "/bar",
},
},
{
name: "MergePublicKeys",
files: []string{
`{"trustedPublicKeys": [{"format": "ed25519", "publicKey": "+NMDNfvjCmdT9mLr9zadYQXwF/mPLsToMw36yX7w6HCVCSK9J2WsMGPCAT9U2Y959NFgAfdiSWGRvWbXYlGUcA=="}]}` + "\n",
`{"trustedPublicKeys": [{"format": "foo", "publicKey": "YmFy"}]}` + "\n",
},
want: globalConfig{
TrustedPublicKeys: []*zbstore.RealizationPublicKey{
{
Format: "ed25519",
Data: []byte{
0xf8, 0xd3, 0x03, 0x35, 0xfb, 0xe3, 0x0a, 0x67,
0x53, 0xf6, 0x62, 0xeb, 0xf7, 0x36, 0x9d, 0x61,
0x05, 0xf0, 0x17, 0xf9, 0x8f, 0x2e, 0xc4, 0xe8,
0x33, 0x0d, 0xfa, 0xc9, 0x7e, 0xf0, 0xe8, 0x70,
0x95, 0x09, 0x22, 0xbd, 0x27, 0x65, 0xac, 0x30,
0x63, 0xc2, 0x01, 0x3f, 0x54, 0xd9, 0x8f, 0x79,
0xf4, 0xd1, 0x60, 0x01, 0xf7, 0x62, 0x49, 0x61,
0x91, 0xbd, 0x66, 0xd7, 0x62, 0x51, 0x94, 0x70,
},
},
{
Format: "foo",
Data: []byte{0x62, 0x61, 0x72},
},
},
},
},
}

for _, test := range tests {
Expand All @@ -59,9 +90,42 @@ func TestGlobalConfigMergeFiles(t *testing.T) {
if err != nil {
t.Error("mergeFiles:", err)
}
if diff := cmp.Diff(&test.want, got, cmp.AllowUnexported(stringAllowList{})); diff != "" {
if diff := cmp.Diff(&test.want, got, globalConfigCompareOptions); diff != "" {
t.Errorf("-want +got:\n%s", diff)
}
})
}
}

func FuzzConfigMarshal(f *testing.F) {
f.Add([]byte(`{"debug": true, "storeDirectory": "/foo"}` + "\n"))
f.Add([]byte(`{"storeDirectory": "/bar"}` + "\n"))
f.Add([]byte(`{"storeSocket": "/var/foo.socket"}` + "\n"))
f.Add([]byte(`{"cacheDB": "/var/cache.db"}` + "\n"))
f.Add([]byte(`{"trustedPublicKeys": []}` + "\n"))
f.Add([]byte(`{"trustedPublicKeys": [{"format": "ed25519", "publicKey": "+NMDNfvjCmdT9mLr9zadYQXwF/mPLsToMw36yX7w6HCVCSK9J2WsMGPCAT9U2Y959NFgAfdiSWGRvWbXYlGUcA=="}]}` + "\n"))
f.Add([]byte(`{"trustedPublicKeys": [{"format": "foo", "publicKey": "YmFy"}]}`))

f.Fuzz(func(t *testing.T, in []byte) {
init := defaultGlobalConfig()
if err := jsonv2.Unmarshal(in, &init); err != nil {
t.Skip(err)
}
marshalled, err := jsonv2.Marshal(init)
if err != nil {
t.Fatal(err)
}
got := new(globalConfig)
if err := jsonv2.Unmarshal(marshalled, got, jsonv2.RejectUnknownMembers(true)); err != nil {
t.Error("Unmarshal:", err)
}
if diff := cmp.Diff(init, got, globalConfigCompareOptions); diff != "" {
t.Errorf("Marshal result -want +got:\n%s", diff)
}
})
}

var globalConfigCompareOptions = cmp.Options{
cmp.AllowUnexported(stringAllowList{}),
cmpopts.EquateEmpty(),
}
3 changes: 3 additions & 0 deletions cmd/zb/derivation.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func newDerivationShowCommand(g *globalConfig) *cobra.Command {
opts := new(derivationShowOptions)
c.Flags().BoolVarP(&opts.expression, "expression", "e", false, "interpret argument as Lua expression")
addEnvAllowListFlag(c.Flags(), &g.AllowEnv)
addCleanFlag(c.Flags(), &opts.clean)
c.Flags().BoolVar(&opts.jsonFormat, "json", false, "print derivation as JSON")
c.RunE = func(cmd *cobra.Command, args []string) error {
opts.args = args
Expand Down Expand Up @@ -345,6 +346,7 @@ func newDerivationEnvCommand(g *globalConfig) *cobra.Command {
opts := new(derivationEnvOptions)
c.Flags().BoolVarP(&opts.expression, "expression", "e", false, "interpret argument as Lua expression")
addEnvAllowListFlag(c.Flags(), &g.AllowEnv)
addCleanFlag(c.Flags(), &opts.clean)
c.Flags().BoolVar(&opts.jsonFormat, "json", false, "print environments as JSON")
c.Flags().StringVar(&opts.tempDir, "temp-dir", os.TempDir(), "temporary `dir`ectory to fill in")
c.RunE = func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -398,6 +400,7 @@ func runDerivationEnv(ctx context.Context, g *globalConfig, opts *derivationEnvO
err = jsonrpc.Do(ctx, storeClient, zbstorerpc.ExpandMethod, expandResponse, &zbstorerpc.ExpandRequest{
DrvPath: drv.Path,
TemporaryDirectory: opts.tempDir,
Reuse: opts.reusePolicy(g),
})
if err != nil {
return err
Expand Down
7 changes: 2 additions & 5 deletions cmd/zb/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,11 @@ func runShowPublicKey(ctx context.Context, dst io.Writer, src io.Reader) error {
if err := keyFile.appendToKeyring(k); err != nil {
return err
}
var result struct {
Format zbstore.RealizationSignatureFormat `json:"format"`
PublicKey []byte `json:"publicKey,format:base64"`
}
var result zbstore.RealizationPublicKey
switch {
case len(k.Ed25519) > 0:
result.Format = zbstore.Ed25519SignatureFormat
result.PublicKey = k.Ed25519[0].Public().(ed25519.PublicKey)
result.Data = k.Ed25519[0].Public().(ed25519.PublicKey)
default:
return nil
}
Expand Down
18 changes: 18 additions & 0 deletions cmd/zb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type evalOptions struct {
expression bool
args []string
keepFailed bool
clean bool
}

func (opts *evalOptions) newEval(g *globalConfig, storeClient *jsonrpc.Client, di *zbstorerpc.DeferredImporter) (*frontend.Eval, error) {
Expand All @@ -125,6 +126,7 @@ func (opts *evalOptions) newEval(g *globalConfig, storeClient *jsonrpc.Client, d
Store: zbstorerpc.Store{
Handler: storeClient,
},
reuse: opts.reusePolicy(g),
}
di.SetImporter(store)
return frontend.NewEval(&frontend.Options{
Expand All @@ -144,6 +146,13 @@ func (opts *evalOptions) newEval(g *globalConfig, storeClient *jsonrpc.Client, d
})
}

func (opts *evalOptions) reusePolicy(g *globalConfig) *zbstorerpc.ReusePolicy {
if opts.clean {
return nil
}
return g.reusePolicy()
}

func newEvalCommand(g *globalConfig) *cobra.Command {
c := &cobra.Command{
Use: "eval [options] [INSTALLABLE [...]]",
Expand All @@ -162,6 +171,7 @@ func newEvalCommand(g *globalConfig) *cobra.Command {
c.Flags().BoolVarP(&opts.expression, "expression", "e", false, "interpret argument as Lua expression")
c.Flags().BoolVarP(&opts.keepFailed, "keep-failed", "k", false, "keep temporary directories of failed builds")
addEnvAllowListFlag(c.Flags(), &g.AllowEnv)
addCleanFlag(c.Flags(), &opts.clean)
c.RunE = func(cmd *cobra.Command, args []string) error {
opts.args = args
return runEval(cmd.Context(), g, opts)
Expand Down Expand Up @@ -230,6 +240,7 @@ func newBuildCommand(g *globalConfig) *cobra.Command {
c.Flags().BoolVarP(&opts.keepFailed, "keep-failed", "k", false, "keep temporary directories of failed builds")
addEnvAllowListFlag(c.Flags(), &g.AllowEnv)
c.Flags().StringVarP(&opts.outLink, "out-link", "o", "result", "change the name of the output path symlink to `path`")
addCleanFlag(c.Flags(), &opts.clean)
c.RunE = func(cmd *cobra.Command, args []string) error {
opts.args = args
return runBuild(cmd.Context(), g, opts)
Expand Down Expand Up @@ -282,6 +293,7 @@ func runBuild(ctx context.Context, g *globalConfig, opts *buildOptions) error {
err = jsonrpc.Do(ctx, storeClient, zbstorerpc.RealizeMethod, realizeResponse, &zbstorerpc.RealizeRequest{
DrvPaths: drvPaths,
KeepFailed: opts.keepFailed,
Reuse: opts.reusePolicy(g),
})
if err != nil {
return err
Expand Down Expand Up @@ -311,6 +323,7 @@ type rpcStore struct {
zbstorerpc.Store
dir zbstore.Directory
keepFailed bool
reuse *zbstorerpc.ReusePolicy
}

func (store *rpcStore) Realize(ctx context.Context, want sets.Set[zbstore.OutputReference]) ([]*zbstorerpc.BuildResult, error) {
Expand All @@ -324,6 +337,7 @@ func (store *rpcStore) Realize(ctx context.Context, want sets.Set[zbstore.Output
}
}),
KeepFailed: store.keepFailed,
Reuse: store.reuse,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -521,6 +535,10 @@ func addEnvAllowListFlag(fset *pflag.FlagSet, list *stringAllowList) {
all.NoOptDefVal = "true"
}

func addCleanFlag(fset *pflag.FlagSet, p *bool) {
fset.BoolVar(p, "clean", false, "ignore any previous realizations in the store")
}

var initLogOnce sync.Once

func initLogging(showDebug bool) {
Expand Down
Loading