Skip to content

Commit 3d300fc

Browse files
authored
Handle KeyboardInterrupt, improve configure error handling (#168)
1 parent bdf2d18 commit 3d300fc

File tree

9 files changed

+319
-256
lines changed

9 files changed

+319
-256
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ To check whether the installation succeeded, run the following command and verif
4444

4545
```bash
4646
$ scfw --version
47-
2.3.0
47+
2.4.0
4848
```
4949

5050
### Post-installation steps

requirements-dev.txt

Lines changed: 96 additions & 108 deletions
Large diffs are not rendered by default.

requirements.txt

Lines changed: 120 additions & 86 deletions
Large diffs are not rendered by default.

scfw/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
A supply-chain "firewall" for preventing the installation of vulnerable or malicious `pip` and `npm` packages.
33
"""
44

5-
__version__ = "2.3.0"
5+
__version__ = "2.4.0"

scfw/configure/__init__.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
"""
44

55
from argparse import Namespace
6+
import logging
67

78
import scfw.configure.dd_agent as dd_agent
89
import scfw.configure.env as env
910
import scfw.configure.interactive as interactive
1011
from scfw.configure.interactive import GREETING
1112

13+
_log = logging.getLogger(__name__)
14+
1215

1316
def run_configure(args: Namespace) -> int:
1417
"""
@@ -18,27 +21,27 @@ def run_configure(args: Namespace) -> int:
1821
args: A `Namespace` containing the parsed `configure` subcommand command line.
1922
2023
Returns:
21-
An integer status code indicating normal exit.
24+
An integer status code indicating normal or error exit.
2225
"""
26+
dd_agent_status = 0
27+
env_status = 0
28+
2329
if args.remove:
24-
# These options result in the firewall's configuration block being removed
25-
env.update_config_files({
26-
"alias_npm": False,
27-
"alias_pip": False,
28-
"alias_poetry": False,
29-
"dd_agent_port": None,
30-
"dd_api_key": None,
31-
"dd_log_level": None,
32-
"scfw_home": None,
33-
})
34-
dd_agent.remove_agent_logging()
30+
try:
31+
dd_agent.remove_agent_logging()
32+
except Exception as e:
33+
_log.warning(f"Failed to remove Datadog Agent configuration: {e}")
34+
dd_agent_status = 1
35+
36+
env_status = env.remove_config()
37+
3538
print(
3639
"All Supply-Chain Firewall-managed configuration has been removed from your environment."
3740
"\n\nPost-removal tasks:"
3841
"\n* Update your current shell environment by sourcing from your .bashrc/.zshrc file."
3942
"\n* If you had previously configured Datadog Agent log forwarding, restart the Agent."
4043
)
41-
return 0
44+
return dd_agent_status or env_status
4245

4346
# The CLI parser guarantees that all of these arguments are present
4447
is_interactive = not any({
@@ -60,12 +63,18 @@ def run_configure(args: Namespace) -> int:
6063
if not answers:
6164
return 0
6265

63-
env.update_config_files(answers)
64-
6566
if (port := answers.get("dd_agent_port")):
66-
dd_agent.configure_agent_logging(port)
67+
try:
68+
dd_agent.configure_agent_logging(port)
69+
except Exception as e:
70+
_log.warning(f"Failed to configure Datadog Agent for Supply-Chain Firewall: {e}")
71+
dd_agent_status = 1
72+
# Don't set the Agent port environment variable if Agent configuration failed
73+
answers["dd_agent_port"] = None
74+
75+
env_status = env.update_config_files(answers)
6776

6877
if is_interactive:
6978
print(interactive.get_farewell(answers))
7079

71-
return 0
80+
return dd_agent_status or env_status

scfw/configure/dd_agent.py

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88
import shutil
99
import subprocess
10+
from typing import Optional
1011

1112
from scfw.constants import DD_SERVICE, DD_SOURCE
1213

@@ -22,9 +23,9 @@ def configure_agent_logging(port: str):
2223
2324
Raises:
2425
ValueError: An invalid port number was provided.
25-
RuntimeError: An error occurred while querying the Agent's status.
26+
RuntimeError: Failed to determine Datadog Agent configuration directory.
2627
"""
27-
if not (0 < int(port) < 2 ** 16):
28+
if not (0 < int(port) < 65536):
2829
raise ValueError("Invalid port number provided for Datadog Agent logging")
2930

3031
config_file = (
@@ -36,6 +37,9 @@ def configure_agent_logging(port: str):
3637
)
3738

3839
scfw_config_dir = _dd_agent_scfw_config_dir()
40+
if not scfw_config_dir:
41+
raise RuntimeError("Failed to determine Datadog Agent configuration directory")
42+
3943
scfw_config_file = scfw_config_dir / "conf.yaml"
4044

4145
if not scfw_config_dir.is_dir():
@@ -49,63 +53,57 @@ def configure_agent_logging(port: str):
4953
def remove_agent_logging():
5054
"""
5155
Remove Datadog Agent configuration for Supply-Chain Firewall, if it exists.
52-
53-
Raises:
54-
RuntimeError: An error occurred while attempting to remove the configuration directory.
5556
"""
56-
try:
57-
scfw_config_dir = _dd_agent_scfw_config_dir()
58-
except FileNotFoundError:
59-
_log.info("Datadog Agent binary is not available; no configuration to remove")
60-
return
61-
62-
if not scfw_config_dir.is_dir():
57+
scfw_config_dir = _dd_agent_scfw_config_dir()
58+
if not (scfw_config_dir and scfw_config_dir.is_dir()):
6359
_log.info("No Datadog Agent configuration directory to remove")
6460
return
6561

6662
try:
6763
shutil.rmtree(scfw_config_dir)
68-
_log.info(f"Deleted directory {scfw_config_dir} with Datadog Agent configuration")
69-
except Exception:
70-
raise RuntimeError(
71-
f"Failed to delete directory {scfw_config_dir} with Datadog Agent configuration for Supply-Chain Firewall"
64+
_log.info(f"Removed directory {scfw_config_dir} with Datadog Agent configuration")
65+
except Exception as e:
66+
_log.warning(
67+
f"Failed to remove Datadog Agent configuration directory {scfw_config_dir}: {e}"
7268
)
7369

7470

75-
def _dd_agent_scfw_config_dir() -> Path:
71+
def _dd_agent_scfw_config_dir() -> Optional[Path]:
7672
"""
77-
Get the filesystem path to the firewall's configuration directory for
78-
Datadog Agent log forwarding.
73+
Return the filesystem path to Supply-Chain Firewall's configuration directory
74+
for Datadog Agent log forwarding.
7975
8076
Returns:
81-
A `Path` indicating the absolute filesystem path to this directory.
77+
A `Path` containing the local filesystem path to Supply-Chain Firewall's
78+
configuration directory for the Datadog Agent or `None` if the Agent binary
79+
is inaccessible or the Agent's global configuration directory (always the
80+
returned directory's parent) does not exist.
81+
82+
The returned path is what Supply-Chain Firewall's configuration directory
83+
would be if it existed, but this function does not check that this directory
84+
actually exists. It is the caller's responsibility to do so.
8285
8386
Raises:
84-
RuntimeError:
85-
* Unable to query Datadog Agent status to read the location of its global
86-
configuration directory
87-
* Datadog Agent global configuration directory is not set or does not exist
88-
ValueError: Failed to parse Datadog Agent status JSON report.
87+
RuntimeError: Failed to query the Datadog Agent's status.
8988
"""
89+
agent_path = shutil.which("datadog-agent")
90+
if not agent_path:
91+
_log.info("No Datadog Agent binary is accessible in the current environment")
92+
return None
93+
94+
agent_config_dir = None
9095
try:
9196
agent_status = subprocess.run(
92-
["datadog-agent", "status", "--json"], check=True, text=True, capture_output=True
97+
[agent_path, "status", "--json"], check=True, text=True, capture_output=True
9398
)
94-
config_confd_path = json.loads(agent_status.stdout).get("config", {}).get("confd_path")
95-
agent_config_dir = Path(config_confd_path) if config_confd_path else None
99+
if (config_confd_path := json.loads(agent_status.stdout).get("config", {}).get("confd_path")):
100+
agent_config_dir = Path(config_confd_path).absolute()
96101

97-
except subprocess.CalledProcessError:
98-
raise RuntimeError(
99-
"Unable to query Datadog Agent status: please ensure the Agent is running. "
100-
"Linux users may need sudo to run this command."
101-
)
102-
103-
except json.JSONDecodeError:
104-
raise ValueError("Failed to parse Datadog Agent status report as JSON")
102+
except Exception as e:
103+
raise RuntimeError(f"Failed to query Datadog Agent status: {e}")
105104

106105
if not (agent_config_dir and agent_config_dir.is_dir()):
107-
raise RuntimeError(
108-
"Datadog Agent global configuration directory is not set or does not exist"
109-
)
106+
_log.info("No Datadog Agent global configuration directory found")
107+
return None
110108

111109
return agent_config_dir / "scfw.d"

scfw/configure/env.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,52 @@
1616
_BLOCK_END = "# END SCFW MANAGED BLOCK"
1717

1818

19-
def update_config_files(answers: dict):
19+
def remove_config() -> int:
20+
"""
21+
Remove Supply-Chain Firewall configuration from all supported files.
22+
23+
Returns:
24+
An integer status code indicating normal or error exit.
25+
"""
26+
# These options result in the firewall's configuration block being removed
27+
return update_config_files({
28+
"alias_npm": False,
29+
"alias_pip": False,
30+
"alias_poetry": False,
31+
"dd_agent_port": None,
32+
"dd_api_key": None,
33+
"dd_log_level": None,
34+
"scfw_home": None,
35+
})
36+
37+
38+
def update_config_files(answers: dict) -> int:
2039
"""
2140
Update the Supply-Chain Firewall configuration in all supported files.
2241
2342
Args:
2443
answers: A `dict` of configuration options to format and write to each file.
44+
45+
Returns:
46+
An integer status code indicating normal or error exit.
2547
"""
48+
error_count = 0
2649
scfw_config = _format_answers(answers)
2750

2851
for config_file in [Path.home() / file for file in _CONFIG_FILES]:
2952
if not config_file.exists():
3053
_log.info(f"Skipped adding configuration to file {config_file}: file does not already exist")
3154
continue
3255

33-
_update_config_file(config_file, scfw_config)
34-
_log.info(f"Successfully added configuration to file {config_file}")
56+
try:
57+
_update_config_file(config_file, scfw_config)
58+
_log.info(f"Successfully updated configuration in file {config_file}")
59+
60+
except Exception as e:
61+
_log.warning(f"Failed to update configuration in file {config_file}: {e}")
62+
error_count += 1
63+
64+
return 1 if error_count else 0
3565

3666

3767
def _update_config_file(config_file: Path, scfw_config: str):

scfw/loggers/dd_agent_logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __init__(self, agent_port: int):
2828
Raises:
2929
ValueError: An invalid port number was given.
3030
"""
31-
if not 0 < agent_port < 2 ** 16:
31+
if not 0 < agent_port < 65536:
3232
raise ValueError(f"Invalid port number {agent_port}")
3333
self._agent_port = agent_port
3434

scfw/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def main() -> int:
4747
_log.error(e)
4848
return 1
4949

50+
except KeyboardInterrupt:
51+
_log.info("Exiting after receiving keyboard interrupt")
52+
return 1
53+
5054

5155
def _configure_logging(level: int):
5256
"""

0 commit comments

Comments
 (0)