diff --git a/copy.go b/copy.go new file mode 100644 index 0000000..2fbcc66 --- /dev/null +++ b/copy.go @@ -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 +} diff --git a/copy_test.go b/copy_test.go new file mode 100644 index 0000000..0f920c7 --- /dev/null +++ b/copy_test.go @@ -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) + } + }) +} diff --git a/exec_test.go b/exec_test.go index 9641616..9d2a56b 100644 --- a/exec_test.go +++ b/exec_test.go @@ -14,7 +14,7 @@ import ( func TestNixWriteStdin(t *testing.T) { t.Run("already set", func(t *testing.T) { 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 if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil { t.Fatal("WriteStdinCommand unexpectedly succeeded") diff --git a/format.go b/format.go index a846d33..b4aa208 100644 --- a/format.go +++ b/format.go @@ -21,6 +21,8 @@ const ( CommandStore = "store" // CommandStoreSign sign store paths with a local key CommandStoreSign = "sign" + // CommandCopy copy paths between Nix stores + CommandCopy = "copy" // FlagAll apply the operation to every store path. FlagAll = "--all" @@ -36,6 +38,8 @@ const ( // FlagKeyFile file containing the secret signing key. FlagKeyFile = "--key-file" + // FlagTo URL of the destination Nix store. + FlagTo = "--to" // FlagDryRun show what this command would do without doing it. FlagDryRun = "--dry-run" @@ -97,8 +101,8 @@ const ( nixosSuffix1 = ".config.system.build.toplevel" ) -// NixOSInstallable returns the nixos installable for a given flake and host. -func NixOSInstallable(flake, host string) string { return flake + nixosSuffix0 + host + nixosSuffix1 } +// InstallableNixOS returns the nixos installable for a given flake and host. +func InstallableNixOS(flake, host string) string { return flake + nixosSuffix0 + host + nixosSuffix1 } // WriteStdin writes installables for a nix process running with [FlagStdin]. func WriteStdin(w io.Writer, installables iter.Seq[string]) (int, error) { diff --git a/format_test.go b/format_test.go index 2ece1ed..8af2a6e 100644 --- a/format_test.go +++ b/format_test.go @@ -65,7 +65,7 @@ func TestInstallable(t *testing.T) { } for _, tc := range testCases { 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 { t.Errorf("Installable(%q, %q): %q, want %q", tc.flake, tc.host, got, tc.want) diff --git a/sign_test.go b/sign_test.go index 44f232b..636d280 100644 --- a/sign_test.go +++ b/sign_test.go @@ -22,7 +22,7 @@ func init() { flagSignStdin bool ) 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 } @@ -47,7 +47,7 @@ func TestSign(t *testing.T) { stubNixCommand(t) if err := nix.Sign( newStubContext(t.Context(), nil, os.Stdout, os.Stderr), - "/proc/nonexistent", + nonexistent, slices.Values(instWant["pluiedev pappardelle"]), ); err != nil { t.Errorf("Sign: error = %v", err) diff --git a/stub_test.go b/stub_test.go index 616cddb..7ef6a9d 100644 --- a/stub_test.go +++ b/stub_test.go @@ -17,6 +17,8 @@ import ( ) const ( + nonexistent = "/proc/nonexistent" + 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. func breakNixCommand(t *testing.T) { n := nix.Nix - nix.Nix = "/proc/nonexistent" + nix.Nix = nonexistent t.Cleanup(func() { nix.Nix = n }) }