copy: wrap copy from stdin
This commit is contained in:
parent
cb39dc5fcf
commit
149b9c6a2a
57
copy.go
Normal file
57
copy.go
Normal 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¶llel-compression=%t®ion=%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
103
copy_test.go
Normal 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¶llel-compression=false®ion=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¶llel-compression=false®ion=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¶llel-compression=true®ion=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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -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")
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user