copy: wrap copy from stdin

This commit is contained in:
Ophestra 2025-07-20 00:51:45 +09:00
parent cb39dc5fcf
commit 149b9c6a2a
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
7 changed files with 173 additions and 7 deletions

57
copy.go Normal file
View File

@ -0,0 +1,57 @@
package nix
import (
"context"
"fmt"
"iter"
"os"
)
const (
EnvAwsSharedCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
)
// A BinaryCache holds credentials and parameters to a s3 binary cache.
type BinaryCache struct {
// Compression is the name of the compression algorithm to use. Example: "zstd".
Compression string `json:"compression"`
// ParallelCompression determines whether parallel compression is enabled.
ParallelCompression bool `json:"parallel_compression,omitempty"`
// Bucket is the s3 bucket name.
Bucket string `json:"bucket"`
// Endpoint is the s3 endpoint. Example: "s3.example.org".
Endpoint string `json:"endpoint,omitempty"`
// Region is the s3 region. Example: "ap-northeast-1".
Region string `json:"region"`
// Scheme is the s3 protocol. Example: "https".
Scheme string `json:"scheme"`
// CredentialsPath is the path to the s3 shared credentials file.
CredentialsPath string `json:"credentials_path"`
}
func (store *BinaryCache) String() string {
return fmt.Sprintf(
"s3://%s?compression=%s&parallel-compression=%t&region=%s&scheme=%s&endpoint=%s",
store.Bucket, store.Compression, store.ParallelCompression, store.Region, store.Scheme, store.Endpoint,
)
}
// Copy copies installables to the binary cache store, signing all paths using the key at keyPath.
func Copy(ctx Context, keyPath string, store *BinaryCache, installables iter.Seq[string]) error {
if store == nil {
return os.ErrInvalid
}
c, cancel := context.WithCancel(ctx.Unwrap())
defer cancel()
cmd := ctx.Nix(c, CommandCopy,
FlagTo, store.String()+"&secret-key="+keyPath,
FlagStdin)
cmd.Env = append(os.Environ(), EnvAwsSharedCredentialsFile+"="+store.CredentialsPath)
cmd.Stdout, cmd.Stderr = ctx.Streams()
_, err := ctx.WriteStdin(cmd, installables, nil)
return err
}

103
copy_test.go Normal file
View File

@ -0,0 +1,103 @@
package nix_test
import (
"errors"
"os"
"slices"
"syscall"
"testing"
"gensokyo.uk/nix"
"hakurei.app/command"
)
func init() {
stubCommandInit = append(stubCommandInit, func(c command.Command) {
var (
flagCopyTo string
flagCopyStdin bool
)
c.NewCommand(nix.CommandCopy, "emit samples for various `nix copy` cases", func(args []string) error {
if flagCopyTo != "s3://example?compression=none&parallel-compression=false&region=us-east-1&scheme=http&endpoint=s3.example.org&secret-key="+nonexistent || !flagCopyStdin {
return syscall.ENOSYS
}
installables, err := nix.ReadStdin(os.Stdin)
if err != nil {
return err
}
if !slices.Equal(installables, instWant["pluiedev pappardelle"]) {
return syscall.EINVAL
}
return nil
}).
Flag(&flagCopyTo, trimFlagName(nix.FlagTo), command.StringFlag(""), nix.FlagTo).
Flag(&flagCopyStdin, trimFlagName(nix.FlagStdin), command.BoolFlag(false), nix.FlagStdin)
})
}
func TestBinaryCache(t *testing.T) {
testCases := []struct {
name string
store *nix.BinaryCache
want string
}{
{"example", &nix.BinaryCache{
Compression: "none",
ParallelCompression: false,
Bucket: "example",
Endpoint: "s3.example.org",
Region: "us-east-1",
Scheme: "http",
CredentialsPath: "/dev/null",
}, "s3://example?compression=none&parallel-compression=false&region=us-east-1&scheme=http&endpoint=s3.example.org"},
{"gensokyo", &nix.BinaryCache{
Compression: "zstd",
ParallelCompression: true,
Bucket: "nix-cache",
Endpoint: "s3.gensokyo.uk",
Region: "ap-northeast-1",
Scheme: "https",
CredentialsPath: "/var/lib/persist/cache/s3",
}, "s3://nix-cache?compression=zstd&parallel-compression=true&region=ap-northeast-1&scheme=https&endpoint=s3.gensokyo.uk"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.store.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}
func TestCopy(t *testing.T) {
stubNixCommand(t)
if err := nix.Copy(
newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
nonexistent,
&nix.BinaryCache{
Compression: "none",
ParallelCompression: false,
Bucket: "example",
Endpoint: "s3.example.org",
Region: "us-east-1",
Scheme: "http",
CredentialsPath: "/dev/null",
},
slices.Values(instWant["pluiedev pappardelle"]),
); err != nil {
t.Errorf("Copy: error = %v", err)
}
t.Run("nil store", func(t *testing.T) {
if err := nix.Copy(
newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
nonexistent,
nil,
nil,
); !errors.Is(err, os.ErrInvalid) {
t.Errorf("Copy: error = %v, want %v", err, os.ErrInvalid)
}
})
}

View File

@ -14,7 +14,7 @@ import (
func TestNixWriteStdin(t *testing.T) { func TestNixWriteStdin(t *testing.T) {
t.Run("already set", func(t *testing.T) { t.Run("already set", func(t *testing.T) {
ctx := nix.New(t.Context(), nil, os.Stdout, os.Stderr) ctx := nix.New(t.Context(), nil, os.Stdout, os.Stderr)
cmd := exec.CommandContext(t.Context(), "/proc/nonexistent") cmd := exec.CommandContext(t.Context(), nonexistent)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil { if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil {
t.Fatal("WriteStdinCommand unexpectedly succeeded") t.Fatal("WriteStdinCommand unexpectedly succeeded")

View File

@ -21,6 +21,8 @@ const (
CommandStore = "store" CommandStore = "store"
// CommandStoreSign sign store paths with a local key // CommandStoreSign sign store paths with a local key
CommandStoreSign = "sign" CommandStoreSign = "sign"
// CommandCopy copy paths between Nix stores
CommandCopy = "copy"
// FlagAll apply the operation to every store path. // FlagAll apply the operation to every store path.
FlagAll = "--all" FlagAll = "--all"
@ -36,6 +38,8 @@ const (
// FlagKeyFile file containing the secret signing key. // FlagKeyFile file containing the secret signing key.
FlagKeyFile = "--key-file" FlagKeyFile = "--key-file"
// FlagTo URL of the destination Nix store.
FlagTo = "--to"
// FlagDryRun show what this command would do without doing it. // FlagDryRun show what this command would do without doing it.
FlagDryRun = "--dry-run" FlagDryRun = "--dry-run"
@ -97,8 +101,8 @@ const (
nixosSuffix1 = ".config.system.build.toplevel" nixosSuffix1 = ".config.system.build.toplevel"
) )
// NixOSInstallable returns the nixos installable for a given flake and host. // InstallableNixOS returns the nixos installable for a given flake and host.
func NixOSInstallable(flake, host string) string { return flake + nixosSuffix0 + host + nixosSuffix1 } func InstallableNixOS(flake, host string) string { return flake + nixosSuffix0 + host + nixosSuffix1 }
// WriteStdin writes installables for a nix process running with [FlagStdin]. // WriteStdin writes installables for a nix process running with [FlagStdin].
func WriteStdin(w io.Writer, installables iter.Seq[string]) (int, error) { func WriteStdin(w io.Writer, installables iter.Seq[string]) (int, error) {

View File

@ -65,7 +65,7 @@ func TestInstallable(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got := nix.NixOSInstallable(tc.flake, tc.host) got := nix.InstallableNixOS(tc.flake, tc.host)
if got != tc.want { if got != tc.want {
t.Errorf("Installable(%q, %q): %q, want %q", t.Errorf("Installable(%q, %q): %q, want %q",
tc.flake, tc.host, got, tc.want) tc.flake, tc.host, got, tc.want)

View File

@ -22,7 +22,7 @@ func init() {
flagSignStdin bool flagSignStdin bool
) )
commandStore.NewCommand(nix.CommandStoreSign, "emit samples for various `nix store sign` cases", func(args []string) error { commandStore.NewCommand(nix.CommandStoreSign, "emit samples for various `nix store sign` cases", func(args []string) error {
if !flagSignPBL || !flagSignVerbose || !flagSignRecursive || flagSignKeyFile != "/proc/nonexistent" { if !flagSignPBL || !flagSignVerbose || !flagSignRecursive || flagSignKeyFile != nonexistent || !flagSignStdin {
return syscall.ENOSYS return syscall.ENOSYS
} }
@ -47,7 +47,7 @@ func TestSign(t *testing.T) {
stubNixCommand(t) stubNixCommand(t)
if err := nix.Sign( if err := nix.Sign(
newStubContext(t.Context(), nil, os.Stdout, os.Stderr), newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
"/proc/nonexistent", nonexistent,
slices.Values(instWant["pluiedev pappardelle"]), slices.Values(instWant["pluiedev pappardelle"]),
); err != nil { ); err != nil {
t.Errorf("Sign: error = %v", err) t.Errorf("Sign: error = %v", err)

View File

@ -17,6 +17,8 @@ import (
) )
const ( const (
nonexistent = "/proc/nonexistent"
runAsNixStub = "TEST_RUN_AS_NIX_STUB" runAsNixStub = "TEST_RUN_AS_NIX_STUB"
) )
@ -77,7 +79,7 @@ func newStubContextCommand(f func(*exec.Cmd), ctx context.Context, extraArgs []s
// breakNixCommand makes all nix invocations fail for the current test. // breakNixCommand makes all nix invocations fail for the current test.
func breakNixCommand(t *testing.T) { func breakNixCommand(t *testing.T) {
n := nix.Nix n := nix.Nix
nix.Nix = "/proc/nonexistent" nix.Nix = nonexistent
t.Cleanup(func() { nix.Nix = n }) t.Cleanup(func() { nix.Nix = n })
} }