1password-secrets 0.0.1.dev241__tar.gz → 0.0.1.dev247__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/PKG-INFO +1 -1
  2. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/PKG-INFO +1 -1
  3. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/cli.py +23 -14
  4. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/commands.py +45 -28
  5. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/fly.py +41 -14
  6. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/op.py +7 -2
  7. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/prompts.py +41 -5
  8. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/SOURCES.txt +0 -0
  9. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/dependency_links.txt +0 -0
  10. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/entry_points.txt +0 -0
  11. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/requires.txt +0 -0
  12. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/1password_secrets.egg-info/top_level.txt +0 -0
  13. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/README.md +0 -0
  14. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/__init__.py +0 -0
  15. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/__main__.py +0 -0
  16. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/constants.py +0 -0
  17. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/envs.py +0 -0
  18. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/errors.py +0 -0
  19. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/git.py +0 -0
  20. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/logs.py +0 -0
  21. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/onepassword_secrets/utils.py +0 -0
  22. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/pyproject.toml +0 -0
  23. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/setup.cfg +0 -0
  24. {1password_secrets-0.0.1.dev241 → 1password_secrets-0.0.1.dev247}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1password-secrets
3
- Version: 0.0.1.dev241
3
+ Version: 0.0.1.dev247
4
4
  Summary: 1password-secrets is a set of utilities to sync 1password secrets.
5
5
  Home-page: https://github.com/significa/fly-1password-secrets
6
6
  Author: Significa
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1password-secrets
3
- Version: 0.0.1.dev241
3
+ Version: 0.0.1.dev247
4
4
  Summary: 1password-secrets is a set of utilities to sync 1password secrets.
5
5
  Home-page: https://github.com/significa/fly-1password-secrets
6
6
  Author: Significa
@@ -12,7 +12,7 @@ from .commands import (
12
12
  )
13
13
  from .constants import DEFAULT_REMOTE_NAME
14
14
  from .errors import UserError
15
- from .logs import logger
15
+ from .logs import console, logger
16
16
 
17
17
  try:
18
18
  APP_VERSION = version("1password-secrets")
@@ -85,19 +85,28 @@ def main():
85
85
  logger.setLevel(logging.DEBUG)
86
86
 
87
87
  try:
88
- if args.subcommand == "fly":
89
- if args.action == "import":
90
- import_1password_secrets_to_fly(args.app_name, vault=args.vault)
91
- elif args.action == "edit":
92
- edit_1password_fly_secrets(args.app_name, vault=args.vault)
93
-
94
- elif args.subcommand == "local":
95
- if args.action == "pull":
96
- pull_local_secrets(remote=args.remote, vault=args.vault)
97
- elif args.action == "push":
98
- push_local_secrets(remote=args.remote, vault=args.vault)
99
- elif args.action == "create":
100
- create_local_secrets(args.secrets_file_path, vault=args.vault, remote=args.remote)
88
+ run_subcommand(args)
101
89
 
102
90
  except UserError:
103
91
  sys.exit(1)
92
+
93
+ except KeyboardInterrupt:
94
+ console.print()
95
+ console.print("[yellow]Aborted by user[/yellow]")
96
+ sys.exit(130)
97
+
98
+
99
+ def run_subcommand(args):
100
+ if args.subcommand == "fly":
101
+ if args.action == "import":
102
+ import_1password_secrets_to_fly(args.app_name, vault=args.vault)
103
+ elif args.action == "edit":
104
+ edit_1password_fly_secrets(args.app_name, vault=args.vault)
105
+
106
+ elif args.subcommand == "local":
107
+ if args.action == "pull":
108
+ pull_local_secrets(remote=args.remote, vault=args.vault)
109
+ elif args.action == "push":
110
+ push_local_secrets(remote=args.remote, vault=args.vault)
111
+ elif args.action == "create":
112
+ create_local_secrets(args.secrets_file_path, vault=args.vault, remote=args.remote)
@@ -1,6 +1,7 @@
1
1
  """High level workflows wiring 1Password, Fly and the local filesystem together."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import subprocess
5
6
  from datetime import datetime, timezone
6
7
  from tempfile import NamedTemporaryFile
@@ -21,7 +22,7 @@ from .op import (
21
22
  update_1password_custom_field,
22
23
  update_1password_secrets,
23
24
  )
24
- from .prompts import boolean_prompt, prompt_secret_diff
25
+ from .prompts import SecretDiff, boolean_prompt, prompt_secret_diff
25
26
  from .utils import get_file_contents
26
27
 
27
28
 
@@ -43,37 +44,47 @@ def edit_1password_fly_secrets(app_id, vault=None):
43
44
 
44
45
  current_raw_secrets = get_envs_from_1password(item_id, vault=vault)
45
46
 
46
- with NamedTemporaryFile("w+", suffix=".env") as file:
47
- file.writelines(current_raw_secrets)
48
- file.flush()
49
-
50
- console.print()
51
- console.print(
52
- Panel(
53
- "[bold]Edit the secrets in your editor, then save and close the file to continue.[/bold]\n"
54
- "[dim]Waiting for editor to close...[/dim]",
55
- title="Editor",
56
- border_style="cyan",
47
+ # delete=False so that the file is still around for the editor to open on every platform,
48
+ # cleaned up in the finally block below no matter how we leave this function.
49
+ file = NamedTemporaryFile("w+", suffix=".env", delete=False) # noqa: SIM115
50
+
51
+ try:
52
+ with file:
53
+ file.writelines(current_raw_secrets)
54
+ file.flush()
55
+
56
+ console.print()
57
+ console.print(
58
+ Panel(
59
+ "[bold]Edit the secrets in your editor, then save and close the file to continue.[/bold]\n"
60
+ "[dim]Waiting for editor to close...[/dim]",
61
+ title="Editor",
62
+ border_style="cyan",
63
+ )
57
64
  )
58
- )
59
65
 
60
- editor_command_args = ["code", "--wait", "--disable-extensions", file.name]
61
- debug_command(editor_command_args)
62
- editor_output = subprocess.check_output(editor_command_args) # noqa: S603
63
- debug_command(editor_command_args, output=editor_output, returncode=0)
66
+ editor_command_args = ["code", "--wait", "--disable-extensions", file.name]
67
+ debug_command(editor_command_args)
68
+ editor_output = subprocess.check_output(editor_command_args) # noqa: S603
69
+ debug_command(editor_command_args, output=editor_output, returncode=0)
64
70
 
65
- console.print("[green]Editor closed.[/green]")
66
- console.print()
71
+ console.print("[green]Editor closed.[/green]")
72
+ console.print()
67
73
 
68
- file.seek(0)
69
- new_raw_secrets = file.read()
74
+ file.seek(0)
75
+ new_raw_secrets = file.read()
70
76
 
71
- update_1password_secrets(
72
- item_id,
73
- new_raw_secrets=new_raw_secrets,
74
- previous_raw_secrets=current_raw_secrets,
75
- vault=vault,
76
- )
77
+ update_1password_secrets(
78
+ item_id,
79
+ new_raw_secrets=new_raw_secrets,
80
+ previous_raw_secrets=current_raw_secrets,
81
+ vault=vault,
82
+ )
83
+ finally:
84
+ try:
85
+ os.unlink(file.name)
86
+ except FileNotFoundError:
87
+ pass
77
88
 
78
89
  console.print()
79
90
  if boolean_prompt(f"Secrets updated in 1Password. Import to Fly app '{app_id}'"):
@@ -91,11 +102,17 @@ def pull_local_secrets(remote=DEFAULT_REMOTE_NAME, vault=None):
91
102
  previous_raw_secrets = get_file_contents(env_file_name, raise_if_not_found=False)
92
103
 
93
104
  if previous_raw_secrets:
94
- prompt_secret_diff(
105
+ diff = prompt_secret_diff(
95
106
  previous_raw_secrets=previous_raw_secrets,
96
107
  new_raw_secrets=secrets,
108
+ apply_prompt=f"Pull these changes into {env_file_name}",
109
+ cosmetic_prompt=f"Pull the file into {env_file_name} anyway",
97
110
  )
98
111
 
112
+ if diff == SecretDiff.IDENTICAL:
113
+ console.print(f"[dim]No changes detected, {env_file_name} is already up to date[/dim]")
114
+ return
115
+
99
116
  with open(env_file_name, "w") as file:
100
117
  file.writelines(secrets)
101
118
 
@@ -4,6 +4,7 @@ import json
4
4
  import subprocess
5
5
 
6
6
  import requests
7
+ from rich.table import Table
7
8
 
8
9
  from .constants import FLY_API_TIMEOUT_SECONDS, FLY_MACHINES_API_ENDPOINT
9
10
  from .errors import raise_error
@@ -90,13 +91,8 @@ def set_fly_secrets(app_id, values, token=None) -> None:
90
91
  )
91
92
 
92
93
 
93
- def deploy_fly_secrets(app_id):
94
- """Deploy secrets to a Fly app using the fly CLI."""
95
- console.print()
96
- console.print(f"[bold cyan]Deploying secrets to Fly app '{app_id}'...[/bold cyan]")
97
- console.print()
98
-
99
- command_args = ["fly", "secrets", "deploy", "-a", app_id]
94
+ def run_fly_command(command_args, failure_message):
95
+ """Run a fly CLI command, raising a user-facing error if it is unavailable or fails."""
100
96
  debug_command(command_args)
101
97
 
102
98
  try:
@@ -107,7 +103,26 @@ def deploy_fly_secrets(app_id):
107
103
  debug_command(command_args, returncode=result.returncode)
108
104
 
109
105
  if result.returncode != 0:
110
- raise_error(f"Failed to deploy secrets (exit code {result.returncode})")
106
+ raise_error(f"{failure_message} (exit code {result.returncode})")
107
+
108
+
109
+ def deploy_fly_secrets(app_id):
110
+ """Sync and deploy secrets to a Fly app using the fly CLI."""
111
+ console.print()
112
+ console.print(
113
+ f"[bold cyan]Syncing secrets on Fly app '{app_id}' (fly secrets sync)...[/bold cyan]"
114
+ )
115
+ console.print()
116
+
117
+ run_fly_command(["fly", "secrets", "sync", "-a", app_id], "Failed to sync secrets")
118
+
119
+ console.print()
120
+ console.print(
121
+ f"[bold cyan]Deploying secrets to Fly app '{app_id}' (fly secrets deploy)...[/bold cyan]"
122
+ )
123
+ console.print()
124
+
125
+ run_fly_command(["fly", "secrets", "deploy", "-a", app_id], "Failed to deploy secrets")
111
126
 
112
127
  console.print()
113
128
  console.print(f"[bold green]Secrets deployed to Fly app '{app_id}'[/bold green]")
@@ -122,12 +137,24 @@ def update_fly_secrets(app_id, secrets):
122
137
 
123
138
  deletions = {}
124
139
 
125
- if len(secrets_names_in_fly_only) > 0 and boolean_prompt(
126
- "The following secrets will be deleted from Fly.io: {}. Are you sure".format(
127
- ", ".join(sorted(secrets_names_in_fly_only))
140
+ if len(secrets_names_in_fly_only) > 0:
141
+ table = Table(
142
+ title="Secrets that only exist on Fly.io",
143
+ show_header=True,
144
+ header_style="bold",
128
145
  )
129
- ):
130
- deletions = dict.fromkeys(secrets_names_in_fly_only, None)
146
+ table.add_column("#", style="dim", justify="right")
147
+ table.add_column("Secret", style="red")
148
+
149
+ for index, name in enumerate(sorted(secrets_names_in_fly_only), start=1):
150
+ table.add_row(str(index), name)
151
+
152
+ console.print()
153
+ console.print(table)
154
+ console.print()
155
+
156
+ if boolean_prompt(f"Delete these {len(secrets_names_in_fly_only)} secrets from Fly.io"):
157
+ deletions = dict.fromkeys(secrets_names_in_fly_only, None)
131
158
 
132
159
  values = {**secrets, **deletions}
133
160
 
@@ -141,5 +168,5 @@ def update_fly_secrets(app_id, secrets):
141
168
  console.print()
142
169
  console.print(f"[bold green]Secrets staged on Fly app '{app_id}'[/bold green]")
143
170
 
144
- if boolean_prompt("Deploy secrets now (run fly secrets deploy)?"):
171
+ if boolean_prompt("Deploy secrets now ( fly secrets sync && fly secrets deploy)?"):
145
172
  deploy_fly_secrets(app_id)
@@ -12,7 +12,7 @@ from .constants import (
12
12
  )
13
13
  from .errors import raise_error
14
14
  from .logs import console, debug_command, logger
15
- from .prompts import prompt_secret_diff
15
+ from .prompts import SecretDiff, boolean_prompt, prompt_secret_diff
16
16
  from .utils import first
17
17
 
18
18
 
@@ -159,11 +159,16 @@ def update_1password_secrets(item_id, new_raw_secrets, previous_raw_secrets=None
159
159
  if previous_raw_secrets is None:
160
160
  previous_raw_secrets = get_envs_from_1password(item_id)
161
161
 
162
- prompt_secret_diff(
162
+ diff = prompt_secret_diff(
163
163
  previous_raw_secrets=previous_raw_secrets,
164
164
  new_raw_secrets=new_raw_secrets,
165
165
  )
166
166
 
167
+ if diff == SecretDiff.IDENTICAL:
168
+ console.print("[dim]No changes detected[/dim]")
169
+ if not boolean_prompt("Proceed anyway"):
170
+ raise_error("Aborted by user")
171
+
167
172
  logger.debug(f"Updating 1password secret note content for item {item_id!r}")
168
173
  run_1password_command(
169
174
  "item",
@@ -1,3 +1,5 @@
1
+ from enum import Enum
2
+
1
3
  import inquirer
2
4
  from rich.table import Table
3
5
  from rich.text import Text
@@ -22,7 +24,30 @@ def boolean_prompt(prompt: str, default: bool = False) -> bool:
22
24
  return answers["confirm"]
23
25
 
24
26
 
25
- def prompt_secret_diff(previous_raw_secrets, new_raw_secrets):
27
+ class SecretDiff(str, Enum):
28
+ """Outcome of comparing two raw env file contents."""
29
+
30
+ CHANGED = "changed"
31
+ """Env var keys were added, removed or had their value changed."""
32
+
33
+ COSMETIC = "cosmetic"
34
+ """No env var differences, but the file contents differ (comments, ordering, spacing)."""
35
+
36
+ IDENTICAL = "identical"
37
+ """Both files are equivalent."""
38
+
39
+
40
+ def prompt_secret_diff(
41
+ previous_raw_secrets,
42
+ new_raw_secrets,
43
+ apply_prompt: str = "Apply these changes",
44
+ cosmetic_prompt: str = "Apply these changes anyway",
45
+ ) -> SecretDiff:
46
+ """Show the diff between two env files and ask for confirmation when relevant.
47
+
48
+ Returns the kind of difference found. Nothing is prompted when the contents are
49
+ equivalent - it is up to the caller to decide what to do in that case.
50
+ """
26
51
  previous_parsed_secrets = get_secrets_from_envs(previous_raw_secrets)
27
52
  new_parsed_secrets = get_secrets_from_envs(new_raw_secrets)
28
53
 
@@ -38,10 +63,19 @@ def prompt_secret_diff(previous_raw_secrets, new_raw_secrets):
38
63
  ]
39
64
 
40
65
  if len(deleted_keys) == 0 and len(added_keys) == 0 and len(keys_whos_value_changed) == 0:
41
- console.print("[dim]No changes detected[/dim]")
42
- if not boolean_prompt("Proceed anyway"):
66
+ if previous_raw_secrets.strip() == new_raw_secrets.strip():
67
+ return SecretDiff.IDENTICAL
68
+
69
+ console.print(
70
+ "[dim]No environment variable differences, "
71
+ "but the file structure or comments changed[/dim]"
72
+ )
73
+ console.print()
74
+
75
+ if not boolean_prompt(cosmetic_prompt, default=True):
43
76
  raise_error("Aborted by user")
44
- return
77
+
78
+ return SecretDiff.COSMETIC
45
79
 
46
80
  # Build a rich table for the diff
47
81
  table = Table(title="Change Summary", show_header=True, header_style="bold")
@@ -67,5 +101,7 @@ def prompt_secret_diff(previous_raw_secrets, new_raw_secrets):
67
101
  console.print(table)
68
102
  console.print()
69
103
 
70
- if not boolean_prompt("Apply these changes"):
104
+ if not boolean_prompt(apply_prompt):
71
105
  raise_error("Aborted by user")
106
+
107
+ return SecretDiff.CHANGED