Skip to content

Commit cd2818a

Browse files
authored
Merge pull request #12 from hathitrust/ETT-241_json2yaml_format
Ett 241 json2yaml format
2 parents b37b5f2 + 17d48c3 commit cd2818a

16 files changed

Lines changed: 490 additions & 90 deletions

README.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ poetry run ruff format src/ tests/
5353
# Type checking
5454
poetry run mypy src/
5555

56-
Note: The file `py.typed` has added to the package and specified in pyproject.toml to ensure mypy treats
56+
Note: The file `py.typed` has been added to the package and specified in pyproject.toml to ensure mypy treats
5757
tkseal as a typed package and avoids "Skipping analyzing 'tkseal': found module but no type hints or library stubs" warnings.
5858
```
5959

@@ -63,8 +63,8 @@ tkseal as a typed package and avoids "Skipping analyzing 'tkseal': found module
6363
-`tkseal ready` - Check dependencies (WIP)
6464
-`tkseal diff PATH` - Show differences between plain_secrets.json and cluster
6565
-`tkseal pull PATH` - Extracting secrets from cluster to plain_secrets.json
66-
- 💻 `tkseal seal PATH` - Convert plain_secrets.json to sealed_secrets.json
67-
-
66+
- `tkseal seal PATH` - Convert plain_secrets.json to sealed_secrets.json
67+
6868

6969
## Logic documentation
7070

@@ -103,13 +103,18 @@ tkseal as a typed package and avoids "Skipping analyzing 'tkseal': found module
103103

104104
`tkseal pull environments/testing/`
105105

106-
This shows how "plain_secrets.json" would change based on what's in the Kubernetes cluster
106+
This shows how "plain_secrets.json" would change based on what's in the Kubernetes cluster, and it will
107+
warn about forbidden secrets:
107108

108-
`⚠ Warning: Found forbidden secrets in namespace that cannot be pulled:
109-
- default-token-abc (type: kubernetes.io/service-account-token)
110-
- helm-release-v1 (type: helm.sh/release.v1)`
109+
```
110+
These secrets are system-managed and will not be included in plain_secrets.json:
111+
- oidc-saml-proxy-tls (type: kubernetes.io/tls)
112+
This shows how "plain_secrets.json" would change based on what's in the Kubernetes cluster
113+
--- plain_secrets.json
114+
+++ cluster
115+
```
111116

112-
### ready Command
117+
## ready Command
113118

114119
**Core Functionality**
115120

@@ -156,7 +161,7 @@ which can then be safely stored in version control systems like Git.
156161
- Example usage: `printf "secret" | kubeseal --raw --namespace ns --name secret-name --context ctx`
157162

158163

159-
### diff command
164+
## diff command
160165

161166
**Core Functionality**
162167
The diff command compares the local `plain_secrets.json` file in a specified Tanka environment directory with
@@ -175,6 +180,7 @@ Usage Example
175180
**Show what would change in cluster**
176181

177182
```tkseal diff /path/to/tanka/environments/production```
183+
Use `tkseal diff /path/to/env --format yaml` to output plain secrets in YAML format.
178184

179185
**If there are differences, shows a unified diff**
180186

@@ -202,7 +208,7 @@ No differences
202208
Error: Path '/nonexistent/path' does not exist.
203209
```
204210

205-
### pull command
211+
## pull command
206212

207213
**Core Functionality**
208214
The pull command extracts existing Kubernetes secrets from the cluster and writes them to a local plain_secrets.json
@@ -215,8 +221,11 @@ The flow is:
215221
3. Prompt user for confirmation
216222
4. Write kube secrets to plain_secrets.json
217223

224+
### pull secrets (with confirmation)
225+
`tkseal pull /path/to/tanka/environment`
226+
Use `tkseal pull /path/to/env --format yaml` to output plain secrets in YAML format.
218227

219-
### seal command
228+
## seal command
220229

221230
**Core Functionality**
222231
The seal command reads the local plain_secrets.json file in a specified Tanka environment directory,
@@ -232,11 +241,21 @@ The flow is:
232241
3. Seal each secret using kubeseal
233242
4. Write sealed secrets to sealed_secrets.json
234243

235-
# Seal secrets (with confirmation)
244+
### Seal secrets (with confirmation)
236245
`tkseal seal /path/to/tanka/environment`
246+
Use `tkseal seal /path/to/env --format yaml` to output sealed secrets in YAML format.
237247

238248
The command will:
239249
1. Show yellow warning about cluster changes
240250
2. Display diff of what would change
241251
3. Ask for confirmation
242252
4. Seal secrets to sealed_secrets.json
253+
254+
# Example of errors running tkseal commands
255+
256+
This error means that you probably are not in a Tanka environment directory or the directory structure is incorrect.
257+
Remember that Tanka expects a specific directory structure with `main.jsonnet` file in the environment's base directory.
258+
259+
```Error: Failed to initialize Tanka environment: Command failed with exit code 1: Error: Unable to identify the environments base directory.
260+
Tried to find 'main.jsonnet' in the parent directories.
261+
Please refer to https://tanka.dev/directory-structure for more information```

src/tkseal/cli.py

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,28 @@ def ready() -> None:
4949

5050
@cli.command()
5151
@click.argument("path", type=click.Path(exists=True))
52-
def diff(path: str) -> None:
53-
"""Show differences between plain_secrets.json and cluster secrets.
52+
@click.option(
53+
"--format",
54+
type=click.Choice(["json", "yaml"], case_sensitive=False),
55+
default="json",
56+
help="Output format for secret files (default: json)",
57+
)
58+
def diff(path: str, format: str) -> None:
59+
"""Show differences between plain_secrets file and cluster secrets.
5460
5561
PATH: Path to Tanka environment directory or .jsonnet file
5662
57-
This shows what would change in the cluster based on plain_secrets.json
63+
This shows what would change in the cluster based on plain_secrets file
5864
"""
5965
try:
60-
# Create SecretState from path
61-
secret_state = SecretState.from_path(path)
66+
# Create SecretState from path with specified format
67+
secret_state = SecretState.from_path(path, format=format)
6268

6369
# Create a Diff instance and run comparison
6470
diff_obj = Diff(secret_state)
6571
result = diff_obj.plain()
6672

67-
# Display results
73+
# Display results - Always in JSON format, it is independent of the sealed_secrets format (YAML/JSON)
6874
if result.has_differences:
6975
click.echo(result.diff_output)
7076
else:
@@ -77,17 +83,23 @@ def diff(path: str) -> None:
7783

7884
@cli.command()
7985
@click.argument("path", type=click.Path(exists=True))
80-
def pull(path: str) -> None:
81-
"""Pull secrets from the cluster to plain_secrets.json.
86+
@click.option(
87+
"--format",
88+
type=click.Choice(["json", "yaml"], case_sensitive=False),
89+
default="json",
90+
help="Output format for secret files (default: json)",
91+
)
92+
def pull(path: str, format: str) -> None:
93+
"""Pull secrets from the cluster to plain_secrets file.
8294
8395
PATH: Path to Tanka environment directory or .jsonnet file
8496
8597
This extracts unencrypted secrets from the Kubernetes cluster
86-
and saves them to plain_secrets.json in the environment directory.
98+
and saves them to plain_secrets.json or plain_secrets.yaml in the environment directory.
8799
"""
88100
try:
89-
# Create SecretState from path
90-
secret_state = SecretState.from_path(path)
101+
# Create SecretState from path with specified format
102+
secret_state = SecretState.from_path(path, format=format)
91103

92104
# Create Pull instance and show differences
93105
pull_obj = Pull(secret_state)
@@ -104,18 +116,24 @@ def pull(path: str) -> None:
104116
click.secho(f" - {secret.name} (type: {secret.type})", fg="yellow")
105117

106118
# Show informational message
119+
plain_secrets_file = f"plain_secrets.{format}"
107120
click.secho(
108-
'This shows how "plain_secrets.json" would change based on what\'s in the Kubernetes cluster',
121+
f'This shows how "{plain_secrets_file}" would change based on what\'s in the Kubernetes cluster',
109122
fg="yellow",
110123
)
124+
125+
# Create Pull instance and show differences
126+
pull_obj = Pull(secret_state)
127+
result = pull_obj.run()
128+
111129
# Display diff results
112130
if result.has_differences:
113131
click.echo(result.diff_output)
114132

115133
# Confirm before writing
116134
if click.confirm("Are you sure?"):
117135
pull_obj.write()
118-
click.echo("Successfully pulled secrets to plain_secrets.json")
136+
click.echo(f"Successfully pulled secrets to {plain_secrets_file}")
119137
else:
120138
click.echo("No differences")
121139

@@ -126,21 +144,29 @@ def pull(path: str) -> None:
126144

127145
@cli.command()
128146
@click.argument("path", type=click.Path(exists=True))
129-
def seal(path: str) -> None:
130-
"""Seal plain_secrets.json to sealed_secrets.json.
147+
@click.option(
148+
"--format",
149+
type=click.Choice(["json", "yaml"], case_sensitive=False),
150+
default="json",
151+
help="Output format for secret files (default: json)",
152+
)
153+
def seal(path: str, format: str) -> None:
154+
"""Seal plain_secrets file to sealed_secrets file.
131155
132156
PATH: Path to Tanka environment directory or .jsonnet file
133157
134-
Takes secrets from plain_secrets.json, encrypts them using kubeseal,
135-
and saves the resulting SealedSecret resources to sealed_secrets.json.
158+
Takes secrets from plain_secrets file, encrypts them using kubeseal,
159+
and saves the resulting SealedSecret resources to sealed_secrets file.
136160
"""
137161
try:
138-
# Create SecretState from path
139-
secret_state = SecretState.from_path(path)
162+
# Create SecretState from path with specified format
163+
secret_state = SecretState.from_path(path, format=format)
164+
165+
sealed_secrets_file = f"sealed_secrets.{format}"
140166

141167
# Show informational message
142168
# click.secho(
143-
# 'This shows what would change in the cluster based on "plain_secrets.json"',
169+
# f'This shows what would change in the cluster based on "plain_secrets.{format}"',
144170
# fg="yellow",
145171
# )
146172

@@ -156,7 +182,7 @@ def seal(path: str) -> None:
156182
if click.confirm("Are you sure?"):
157183
seal_obj = Seal(secret_state)
158184
seal_obj.run()
159-
click.echo("Successfully sealed secrets to sealed_secrets.json")
185+
click.echo(f"Successfully sealed secrets to {sealed_secrets_file}")
160186
# else:
161187
# click.echo("No differences")
162188

src/tkseal/configuration.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
and defined in the configuration module.
99
"""
1010

11-
# File name for plain (unencrypted) secrets JSON file
12-
PLAIN_SECRETS_FILE = "plain_secrets.json"
11+
# File name for plain (unencrypted) secrets JSON/YAML file
12+
PLAIN_SECRETS_FILE = "plain_secrets"
1313

1414
# File name for sealed (encrypted) secrets JSON file
15-
SEALED_SECRETS_FILE = "sealed_secrets.json"
15+
SEALED_SECRETS_FILE = "sealed_secrets"
1616

1717
# Allowed secret types that tkseal can manage
1818
MANAGED_SECRET_TYPES = {

src/tkseal/diff.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from tkseal.configuration import PLAIN_SECRETS_FILE
77
from tkseal.secret_state import SecretState
8+
from tkseal.tkseal_utils import normalize_to_json
89

910

1011
@dataclass
@@ -27,11 +28,18 @@ def plain(self) -> DiffResult:
2728
kube_secrets = self.secret_state.kube_secrets()
2829
plain_secrets = self.secret_state.plain_secrets()
2930

31+
# Normalize both to JSON for comparison
32+
kube_secrets_normalized = normalize_to_json(kube_secrets, "json")
33+
plain_secrets_normalized = normalize_to_json(
34+
plain_secrets, self.secret_state.format
35+
)
36+
37+
#
3038
return self._generate_diff(
31-
from_text=kube_secrets,
32-
to_text=plain_secrets,
39+
from_text=kube_secrets_normalized,
40+
to_text=plain_secrets_normalized,
3341
from_label="cluster",
34-
to_label=PLAIN_SECRETS_FILE,
42+
to_label=f"{PLAIN_SECRETS_FILE}.{self.secret_state.format}",
3543
)
3644

3745
def pull(self) -> DiffResult:
@@ -41,10 +49,16 @@ def pull(self) -> DiffResult:
4149
plain_secrets = self.secret_state.plain_secrets()
4250
kube_secrets = self.secret_state.kube_secrets()
4351

52+
# Normalize both to JSON for comparison
53+
plain_secrets_normalized = normalize_to_json(
54+
plain_secrets, self.secret_state.format
55+
)
56+
kube_secrets_normalized = normalize_to_json(kube_secrets, "json")
57+
4458
return self._generate_diff(
45-
from_text=plain_secrets,
46-
to_text=kube_secrets,
47-
from_label=PLAIN_SECRETS_FILE,
59+
from_text=plain_secrets_normalized,
60+
to_text=kube_secrets_normalized,
61+
from_label=f"{PLAIN_SECRETS_FILE}.{self.secret_state.format}",
4862
to_label="cluster",
4963
)
5064

src/tkseal/pull.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from tkseal.diff import Diff, DiffResult
22
from tkseal.secret_state import SecretState
3+
from tkseal.serializers import get_serializer
34

45

56
class Pull:
67
"""Handles pulling secrets from Kubernetes cluster to local files.
78
89
This class coordinates the process of retrieving secrets from a Kubernetes
9-
cluster and saving them to the local plain_secrets.json file.
10+
cluster and saving them to the local plain_secrets.json or plain_secrets.yaml file.
1011
"""
1112

1213
def __init__(self, secret_state: SecretState):
@@ -20,8 +21,8 @@ def __init__(self, secret_state: SecretState):
2021
def run(self) -> DiffResult:
2122
"""Show differences between local and cluster secrets.
2223
23-
This method displays what would change in the local plain_secrets.json
24-
file if secrets were pulled from the cluster.
24+
This method displays what would change in the local plain_secrets file
25+
if secrets were pulled from the cluster.
2526
2627
Returns:
2728
DiffResult: Result containing information about the difference
@@ -33,15 +34,27 @@ def run(self) -> DiffResult:
3334
return diff.pull()
3435

3536
def write(self) -> None:
36-
"""Write cluster secrets to the plain_secrets.json file.
37+
"""Write cluster secrets to the plain_secrets file in the specified format.
3738
38-
This method retrieves secrets from the Kubernetes cluster and writes
39-
them to the local plain_secrets.json file, overwriting any existing content.
39+
This method retrieves secrets from the Kubernetes cluster (as JSON) and writes
40+
them to the local plain_secrets file, converting to YAML if needed.
4041
4142
Raises:
4243
TKSealError: If there's an error retrieving secrets from cluster
4344
PermissionError: If there's an error writing to the file
4445
OSError: If there's an I/O error writing to the file
4546
"""
46-
kube_secrets = self.secret_state.kube_secrets()
47-
self.secret_state.plain_secrets_file_path.write_text(kube_secrets)
47+
# Get secrets from cluster as JSON string
48+
kube_secrets_json = self.secret_state.kube_secrets()
49+
50+
# Convert to the desired format if needed
51+
if self.secret_state.format == "yaml":
52+
# Deserialize JSON and re-serialize to YAML
53+
secret_serializer = get_serializer(self.secret_state.format)
54+
secrets_data = secret_serializer.deserialize_secrets(kube_secrets_json)
55+
output = secret_serializer.serialize_secrets(secrets_data)
56+
else:
57+
# Keep as JSON (no conversion needed)
58+
output = kube_secrets_json
59+
60+
self.secret_state.plain_secrets_file_path.write_text(output)

0 commit comments

Comments
 (0)