apply treefmt

main
Jörg Thalheim 2 years ago
parent cc78160e6e
commit 16ab6ae069

@ -1,9 +1,10 @@
# systemd-vaultd - load vault credentials with systemd units
> Mostly written in a train
- Jörg Thalheim
systemd-vaultd is a proxy between systemd and [vault agent](https://vaultproject.io).
systemd-vaultd is a proxy between systemd and [vault agent](https://vaultproject.io).
It provides a unix socket that can be used in systemd services in the
`LoadCredential` option and then waits for vault agent to write these secrets in
json format at `/run/systemd-vaultd/<service_name>.service.json`.
@ -61,19 +62,19 @@ vault agent is then expected to write secrets to `/run/systemd-vaultd/` in json
template {
# this exposes all secrets in `secret/my-secret` to the service
contents = "#{{ with secret \"secret/my-secret\" }}{{ .Data.data | toJSON }}{{ end }}"
# an alternative is to expose only selected secrets like this:
# contents = <<EOF
# {{ with secret "secret/my-secret" }}{{ scratch.MapSet "secrets" "foobar" .Data.data.foo }}{{ end }}
# {{ scratch.Get "foobar" | explodeMap | toJSON }}
# EOF
destination = "/run/systemd-vaultd/secrets/myservice.service.json"
}
```
When `myservice` is started, systemd will open a connection to
`systemd-vaultd`'s socket. `systemd-vaultd` then either serve the secrets
`systemd-vaultd`'s socket. `systemd-vaultd` then either serve the secrets
from `/run/systemd-vaultd/secrets/myservice.service.json` or it waits with
inotify on secret directory for vault agent to write the secret.

@ -1,4 +1,4 @@
{pkgs ? import <nixpkgs> {}}:
{ pkgs ? import <nixpkgs> { } }:
pkgs.buildGoModule {
name = "systemd-vaultd";
src = ./.;
@ -7,7 +7,7 @@ pkgs.buildGoModule {
description = "A proxy for secrets between systemd services and vault";
homepage = "https://github.com/numtide/systemd-vaultd";
license = licenses.mit;
maintainers = with maintainers; [mic92];
maintainers = with maintainers; [ mic92 ];
platforms = platforms.unix;
};
}

@ -9,37 +9,34 @@
treefmt-nix.url = "github:numtide/treefmt-nix";
};
outputs = inputs @ {flake-parts, ...}:
flake-parts.lib.mkFlake {inherit inputs;} {
systems = ["x86_64-linux" "aarch64-linux"];
outputs = inputs @ { flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" ];
imports = [
./nix/checks/flake-module.nix
];
perSystem = {
config,
self',
inputs',
pkgs,
system,
...
}: {
packages.default = pkgs.callPackage ./default.nix {};
devShells.default = pkgs.mkShellNoCC {
buildInputs = with pkgs; [
python3.pkgs.pytest
python3.pkgs.mypy
perSystem =
{ config
, pkgs
, ...
}: {
packages.default = pkgs.callPackage ./default.nix { };
devShells.default = pkgs.mkShellNoCC {
buildInputs = with pkgs; [
python3.pkgs.pytest
python3.pkgs.mypy
golangci-lint
vault
systemd
hivemind
go
just
config.packages.treefmt
];
};
golangci-lint
vault
systemd
hivemind
go
just
config.packages.treefmt
];
};
};
};
flake.nixosModules = {
vaultAgent = ./nix/modules/vault-agent.nix;
systemdVaultd = ./nix/modules/systemd-vaultd.nix;

@ -1,10 +1,8 @@
{
config,
lib,
pkgs,
...
{ config
, pkgs
, ...
}: {
environment.systemPackages = [pkgs.vault];
environment.systemPackages = [ pkgs.vault ];
services.vault = {
enable = true;
dev = true;
@ -14,8 +12,8 @@
environment.variables.VAULT_TOKEN = config.services.vault.devRootTokenID;
systemd.services.setup-vault-agent-approle = {
path = [pkgs.jq pkgs.vault pkgs.systemd];
wantedBy = ["multi-user.target"];
path = [ pkgs.jq pkgs.vault pkgs.systemd ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
@ -51,7 +49,7 @@
# Make sure our setup service is started before our vault-agent
systemd.services.vault-agent-test = {
wants = ["setup-vault-agent-approle.service"];
after = ["setup-vault-agent-approle.service"];
wants = [ "setup-vault-agent-approle.service" ];
after = [ "setup-vault-agent-approle.service" ];
};
}

@ -1,48 +1,46 @@
{inputs, ...}: {
{ inputs, ... }: {
imports = [
inputs.treefmt-nix.flakeModule
];
perSystem = {
self',
inputs',
pkgs,
system,
...
}: {
treefmt = {
# Used to find the project root
projectRootFile = "flake.lock";
perSystem =
{ pkgs
, ...
}: {
treefmt = {
# Used to find the project root
projectRootFile = "flake.lock";
programs.gofumpt.enable = true;
programs.prettier.enable = true;
programs.gofumpt.enable = true;
programs.prettier.enable = true;
settings.formatter = {
nix = {
command = "sh";
options = [
"-eucx"
''
# First deadnix
${pkgs.lib.getExe pkgs.deadnix} --edit "$@"
# Then nixpkgs-fmt
${pkgs.lib.getExe pkgs.nixpkgs-fmt} "$@"
''
"--"
];
includes = ["*.nix"];
};
settings.formatter = {
nix = {
command = "sh";
options = [
"-eucx"
''
# First deadnix
${pkgs.lib.getExe pkgs.deadnix} --edit "$@"
# Then nixpkgs-fmt
${pkgs.lib.getExe pkgs.nixpkgs-fmt} "$@"
''
"--"
];
includes = [ "*.nix" ];
};
python = {
command = "sh";
options = [
"-eucx"
''
${pkgs.lib.getExe pkgs.ruff} --fix "$@"
${pkgs.lib.getExe pkgs.python3.pkgs.black} "$@"
''
"--" # this argument is ignored by bash
];
includes = ["*.py"];
python = {
command = "sh";
options = [
"-eucx"
''
${pkgs.lib.getExe pkgs.ruff} --fix "$@"
${pkgs.lib.getExe pkgs.python3.pkgs.black} "$@"
''
"--" # this argument is ignored by bash
];
includes = [ "*.py" ];
};
};
};
@ -56,13 +54,4 @@
inherit (nixosTests) unittests vault-agent systemd-vaultd;
};
};
checks = let
nixosTests = pkgs.callPackages ./nix/checks/nixos-test.nix {
makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix");
};
in {
inherit (nixosTests) unittests vault-agent systemd-vaultd;
};
};
}

@ -1,13 +1,15 @@
{
makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>,
pkgs ? (import <nixpkgs> {}),
}: let
{ makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>
, pkgs ? (import <nixpkgs> { })
,
}:
let
makeTest' = args:
makeTest args {
inherit pkgs;
inherit (pkgs) system;
};
in {
in
{
vault-agent = makeTest' (import ./vault-agent-test.nix);
systemd-vaultd = makeTest' (import ./systemd-vaultd-test.nix);
unittests = makeTest' {

@ -1,76 +1,76 @@
{
name = "systemd-vaultd";
nodes.server = {
config,
pkgs,
...
}: {
imports = [
../modules/vault-agent.nix
../modules/systemd-vaultd.nix
./dev-vault-server.nix
];
nodes.server =
{ config
, pkgs
, ...
}: {
imports = [
../modules/vault-agent.nix
../modules/systemd-vaultd.nix
./dev-vault-server.nix
];
systemd.services.service1 = {
wantedBy = ["multi-user.target"];
script = ''
cat $CREDENTIALS_DIRECTORY/foo > /tmp/service1
echo -n "$SECRET_ENV" > /tmp/service1-env
'';
#serviceConfig = {
# EnvironmentFile = [ "/run/systemd-vaultd/service1.service.EnvironmentFile" ];
#};
vault = {
template = ''
{{ with secret "secret/my-secret" }}{{ .Data.data | toJSON }}{{ end }}
'';
secrets.foo = {};
environmentTemplate = ''
{{ with secret "secret/my-secret" }}
SECRET_ENV={{ .Data.data.foo }}
{{ end }}
systemd.services.service1 = {
wantedBy = [ "multi-user.target" ];
script = ''
cat $CREDENTIALS_DIRECTORY/foo > /tmp/service1
echo -n "$SECRET_ENV" > /tmp/service1-env
'';
#serviceConfig = {
# EnvironmentFile = [ "/run/systemd-vaultd/service1.service.EnvironmentFile" ];
#};
vault = {
template = ''
{{ with secret "secret/my-secret" }}{{ .Data.data | toJSON }}{{ end }}
'';
secrets.foo = { };
environmentTemplate = ''
{{ with secret "secret/my-secret" }}
SECRET_ENV={{ .Data.data.foo }}
{{ end }}
'';
};
};
};
systemd.services.service2 = {
wantedBy = ["multi-user.target"];
script = ''
set -x
while true; do
cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2
sleep 0.1
done
'';
serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true";
serviceConfig.LoadCredential = ["secret:/run/systemd-vaultd/sock"];
vault = {
template = ''
{{ with secret "secret/blocking-secret" }}{{ scratch.MapSet "secrets" "secret" .Data.data.foo }}{{ end }}
{{ scratch.Get "secrets" | explodeMap | toJSON }}
systemd.services.service2 = {
wantedBy = [ "multi-user.target" ];
script = ''
set -x
while true; do
cat $CREDENTIALS_DIRECTORY/secret > /tmp/service2
sleep 0.1
done
'';
secrets.secret = {};
serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true";
serviceConfig.LoadCredential = [ "secret:/run/systemd-vaultd/sock" ];
vault = {
template = ''
{{ with secret "secret/blocking-secret" }}{{ scratch.MapSet "secrets" "secret" .Data.data.foo }}{{ end }}
{{ scratch.Get "secrets" | explodeMap | toJSON }}
'';
secrets.secret = { };
};
};
};
services.vault.agents.default.settings = {
vault = {
address = "http://localhost:8200";
};
auto_auth = {
method = [
{
type = "approle";
config = {
role_id_file_path = "/tmp/roleID";
secret_id_file_path = "/tmp/secretID";
remove_secret_id_file_after_reading = false;
};
}
];
services.vault.agents.default.settings = {
vault = {
address = "http://localhost:8200";
};
auto_auth = {
method = [
{
type = "approle";
config = {
role_id_file_path = "/tmp/roleID";
secret_id_file_path = "/tmp/secretID";
remove_secret_id_file_after_reading = false;
};
}
];
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("vault.service")

@ -1,22 +1,23 @@
{
writeShellScript,
python3,
pkgs,
lib,
coreutils,
}: let
systemd-vaultd = pkgs.callPackage ../../default.nix {};
systemd = pkgs.callPackage ../pkgs/systemd.nix {};
{ writeShellScript
, python3
, pkgs
, lib
, coreutils
,
}:
let
systemd-vaultd = pkgs.callPackage ../../default.nix { };
systemd = pkgs.callPackage ../pkgs/systemd.nix { };
in
writeShellScript "unittests" ''
set -eu -o pipefail
export PATH=${lib.makeBinPath [python3.pkgs.pytest coreutils systemd]}
export SYSTEMD_VAULTD_BIN=${systemd-vaultd}/bin/systemd-vaultd
export TMPDIR=$(mktemp -d)
trap 'rm -rf $TMPDIR' EXIT
cp --no-preserve=mode --preserve=timestamps -r ${../..} "$TMPDIR/source"
cd "$TMPDIR/source"
pytest -s ./tests
# we need this in our nixos tests
touch /tmp/success
''
writeShellScript "unittests" ''
set -eu -o pipefail
export PATH=${lib.makeBinPath [python3.pkgs.pytest coreutils systemd]}
export SYSTEMD_VAULTD_BIN=${systemd-vaultd}/bin/systemd-vaultd
export TMPDIR=$(mktemp -d)
trap 'rm -rf $TMPDIR' EXIT
cp --no-preserve=mode --preserve=timestamps -r ${../..} "$TMPDIR/source"
cd "$TMPDIR/source"
pytest -s ./tests
# we need this in our nixos tests
touch /tmp/success
''

@ -1,38 +1,37 @@
{
name = "vault-agent";
nodes.server = {
config,
pkgs,
...
}: {
imports = [
./dev-vault-server.nix
../modules/vault-agent.nix
];
nodes.server =
{ config
, ...
}: {
imports = [
./dev-vault-server.nix
../modules/vault-agent.nix
];
services.vault.agents.test.settings = {
vault = {
address = "http://localhost:8200";
};
template = {
contents = ''{{ with secret "secret/my-secret" }}{{ .Data.data.foo }}{{ end }}'';
destination = "/run/render.txt";
};
services.vault.agents.test.settings = {
vault = {
address = "http://localhost:8200";
};
template = {
contents = ''{{ with secret "secret/my-secret" }}{{ .Data.data.foo }}{{ end }}'';
destination = "/run/render.txt";
};
auto_auth = {
method = [
{
type = "approle";
config = {
role_id_file_path = "/tmp/roleID";
secret_id_file_path = "/tmp/secretID";
remove_secret_id_file_after_reading = false;
};
}
];
auto_auth = {
method = [
{
type = "approle";
config = {
role_id_file_path = "/tmp/roleID";
secret_id_file_path = "/tmp/secretID";
remove_secret_id_file_after_reading = false;
};
}
];
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")

@ -1,20 +1,19 @@
{ pkgs
, ...
}:
let
systemd-vaultd = pkgs.callPackage ../../default.nix { };
in
{
config,
lib,
pkgs,
...
}: let
systemd-vaultd = pkgs.callPackage ../../default.nix {};
in {
imports = [
./vault-secrets.nix
];
systemd.package = pkgs.callPackage ../pkgs/systemd.nix {};
systemd.package = pkgs.callPackage ../pkgs/systemd.nix { };
systemd.sockets.systemd-vaultd = {
description = "systemd-vaultd socket";
wantedBy = ["sockets.target"];
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/run/systemd-vaultd/sock";
@ -24,8 +23,8 @@ in {
};
systemd.services.systemd-vaultd = {
description = "systemd-vaultd daemon";
requires = ["systemd-vaultd.socket"];
after = ["systemd-vaultd.socket"];
requires = [ "systemd-vaultd.socket" ];
after = [ "systemd-vaultd.socket" ];
# Restarting can break services waiting for secrets
stopIfChanged = false;
serviceConfig = {

@ -1,11 +1,11 @@
{
config,
lib,
pkgs,
...
}: let
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.vault;
settingsFormat = pkgs.formats.json {};
settingsFormat = pkgs.formats.json { };
autoAuthMethodModule = lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.unspecified;
@ -27,7 +27,7 @@
options = {
method = lib.mkOption {
type = lib.types.listOf autoAuthMethodModule;
default = [];
default = [ ];
};
};
};
@ -49,18 +49,19 @@
options = {
auto_auth = lib.mkOption {
type = autoAuthModule;
default = {};
default = { };
};
template_config = lib.mkOption {
type = templateConfigModule;
default = {};
default = { };
};
};
};
in {
in
{
options.services.vault.agents = lib.mkOption {
default = {};
default = { };
description = "Instances of vault agent";
type = lib.types.attrsOf (lib.types.submodule {
options = {
@ -72,22 +73,23 @@ in {
});
};
config = {
systemd.services = lib.mapAttrs' (name: instanceCfg:
lib.nameValuePair "vault-agent-${name}" {
after = ["network.target"];
wantedBy = ["multi-user.target"];
systemd.services = lib.mapAttrs'
(name: instanceCfg:
lib.nameValuePair "vault-agent-${name}" {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# Services that also have `stopIfChanged = false` might wait for secrets
# while `vault-agent` is still stopped. This for example happens with nginx.service.
# Services that also have `stopIfChanged = false` might wait for secrets
# while `vault-agent` is still stopped. This for example happens with nginx.service.
stopIfChanged = false;
# Needs getent in PATH
path = [pkgs.glibc];
serviceConfig = {
Restart = "on-failure";
ExecStart = "${pkgs.vault}/bin/vault agent -config=${settingsFormat.generate "agent.json" instanceCfg.settings}";
};
})
cfg.agents;
stopIfChanged = false;
# Needs getent in PATH
path = [ pkgs.glibc ];
serviceConfig = {
Restart = "on-failure";
ExecStart = "${pkgs.vault}/bin/vault agent -config=${settingsFormat.generate "agent.json" instanceCfg.settings}";
};
})
cfg.agents;
};
}

@ -1,11 +1,11 @@
{
lib,
config,
pkgs,
...
}: let
{ lib
, config
, pkgs
, ...
}:
let
secretType = serviceName:
lib.types.submodule ({config, ...}: {
lib.types.submodule ({ config, ... }: {
options = {
name = lib.mkOption {
type = lib.types.str;
@ -59,80 +59,85 @@
vaultTemplates = config:
(lib.mapAttrsToList
(serviceName: service:
(serviceName: _service:
getSecretTemplate serviceName services.${serviceName}.vault)
(lib.filterAttrs (n: v: v.vault.secrets != {} && v.vault.agent == config._module.args.name) services))
(lib.filterAttrs (_n: v: v.vault.secrets != { } && v.vault.agent == config._module.args.name) services))
++ (lib.mapAttrsToList
(serviceName: service:
(serviceName: _service:
getEnvironmentTemplate serviceName services.${serviceName}.vault)
(lib.filterAttrs (n: v: v.vault.environmentTemplate != null && v.vault.agent == config._module.args.name) services));
in {
(lib.filterAttrs (_n: v: v.vault.environmentTemplate != null && v.vault.agent == config._module.args.name) services));
in
{
options = {
systemd.services = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({config, ...}: let
serviceName = config._module.args.name;
in {
options.vault = {
changeAction = lib.mkOption {
description = ''
What to do with the service if any secrets change
'';
type = lib.types.nullOr (lib.types.enum [
"none"
"reload-or-restart"
"restart"
]);
default = "reload-or-restart";
};
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }:
let
serviceName = config._module.args.name;
in
{
options.vault = {
changeAction = lib.mkOption {
description = ''
What to do with the service if any secrets change
'';
type = lib.types.nullOr (lib.types.enum [
"none"
"reload-or-restart"
"restart"
]);
default = "reload-or-restart";
};
template = lib.mkOption {
type = lib.types.lines;
description = ''
The vault agent template to use for secrets
'';
};
template = lib.mkOption {
type = lib.types.lines;
description = ''
The vault agent template to use for secrets
'';
};
environmentTemplate = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
The vault agent template to use for environment file
'';
};
environmentTemplate = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
The vault agent template to use for environment file
'';
};
agent = lib.mkOption {
type = lib.types.str;
default = "default";
description = ''
Agent instance to use for this service
'';
};
agent = lib.mkOption {
type = lib.types.str;
default = "default";
description = ''
Agent instance to use for this service
'';
};
secrets = lib.mkOption {
type = lib.types.attrsOf (secretType serviceName);
default = {};
description = "List of secrets to load from vault agent template";
example = {
some-secret.template = ''{{ with secret "secret/some-secret" }}{{ .Data.data.some-key }}{{ end }}'';
secrets = lib.mkOption {
type = lib.types.attrsOf (secretType serviceName);
default = { };
description = "List of secrets to load from vault agent template";
example = {
some-secret.template = ''{{ with secret "secret/some-secret" }}{{ .Data.data.some-key }}{{ end }}'';
};
};
};
};
config = let
mkIfHasEnv = lib.mkIf (config.vault.environmentTemplate != null);
in {
after = mkIfHasEnv ["${serviceName}-envfile.service"];
bindsTo = mkIfHasEnv ["${serviceName}-envfile.service"];
config =
let
mkIfHasEnv = lib.mkIf (config.vault.environmentTemplate != null);
in
{
after = mkIfHasEnv [ "${serviceName}-envfile.service" ];
bindsTo = mkIfHasEnv [ "${serviceName}-envfile.service" ];
serviceConfig = {
LoadCredential = lib.mapAttrsToList (_: config: "${config.name}:/run/systemd-vaultd/sock") config.vault.secrets;
EnvironmentFile = mkIfHasEnv ["/run/systemd-vaultd/secrets/${serviceName}.service.EnvironmentFile"];
};
};
}));
serviceConfig = {
LoadCredential = lib.mapAttrsToList (_: config: "${config.name}:/run/systemd-vaultd/sock") config.vault.secrets;
EnvironmentFile = mkIfHasEnv [ "/run/systemd-vaultd/secrets/${serviceName}.service.EnvironmentFile" ];
};
};
}));
};
services.vault.agents = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({config, ...}: {
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
config.settings.template = vaultTemplates config;
}));
};
@ -140,14 +145,17 @@ in {
config = {
# we cannot use `systemd.services` here since this would create infinite recursion
systemd.packages = let
servicesWithEnv = builtins.attrNames (lib.filterAttrs (n: v: v.vault.environmentTemplate != null) services);
in [
(pkgs.runCommand "env-services" {}
(''
systemd.packages =
let
servicesWithEnv = builtins.attrNames (lib.filterAttrs (_n: v: v.vault.environmentTemplate != null) services);
in
[
(pkgs.runCommand "env-services" { }
(''
mkdir -p $out/lib/systemd/system
''
+ (lib.concatMapStringsSep "\n" (service: ''
+ (lib.concatMapStringsSep "\n"
(service: ''
cat > $out/lib/systemd/system/${service}-envfile.service <<EOF
[Unit]
Before=${service}.service
@ -165,6 +173,6 @@ in {
EOF
'')
servicesWithEnv)))
];
];
};
}

@ -1,6 +1,6 @@
{
systemd,
fetchpatch,
{ systemd
, fetchpatch
,
}:
systemd.overrideAttrs (old: {
patches =

@ -0,0 +1,20 @@
[tool.ruff]
line-length = 88
select = ["E", "F", "I"]
ignore = [ "E501" ]
[tool.mypy]
python_version = "3.10"
warn_redundant_casts = true
disallow_untyped_calls = true
disallow_untyped_defs = true
no_implicit_optional = true
[[tool.mypy.overrides]]
module = "setuptools.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "pytest.*"
ignore_missing_imports = true

@ -1,15 +1,15 @@
{pkgs ? import <nixpkgs> {}}:
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShellNoCC {
buildInputs = [
python3.pkgs.pytest
python3.pkgs.mypy
mkShellNoCC {
buildInputs = [
python3.pkgs.pytest
python3.pkgs.mypy
golangci-lint
vault
systemd
hivemind
go
just
];
}
golangci-lint
vault
systemd
hivemind
go
just
];
}

@ -3,8 +3,8 @@
import os
import signal
import subprocess
from typing import IO, Any, Dict, Iterator, List, Union
from pathlib import Path
from typing import IO, Any, Dict, Iterator, List, Union
import pytest

@ -1,8 +1,8 @@
#!/usr/bin/env python3
import json
import random
import string
import json
from dataclasses import dataclass
from pathlib import Path

@ -1,9 +1,10 @@
#!/usr/bin/env python3
import os
import pytest
from pathlib import Path
from typing import Optional
import pytest
from command import run
BIN: Optional[Path] = None

@ -1,10 +1,11 @@
#!/usr/bin/env python3
import pytest
from tempfile import TemporaryDirectory
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Iterator
import pytest
@pytest.fixture
def tempdir() -> Iterator[Path]:

@ -1,6 +1,6 @@
import subprocess
from pathlib import Path
import time
from pathlib import Path
from command import Command
from random_service import random_service

@ -1,7 +1,7 @@
#!/usr/bin/env python3
import time
import subprocess
import time
from pathlib import Path
from command import Command, run

Loading…
Cancel
Save