Skip to content

Commit 8914c0f

Browse files
committed
implement only/except in diff and export
1 parent 392bae0 commit 8914c0f

7 files changed

Lines changed: 120 additions & 11 deletions

File tree

docs/guide/cli-reference.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,6 @@ Combine secrets with template files to create complete configuration files.
312312
| `--append` | boolean | `false` | Append to output file instead of overwriting |
313313
| `--overwrite` | boolean | `false` | Overwrite output file without confirmation |
314314
| `--missing` | string | `fail` | How to handle missing secrets: `fail`, `skip`, `blank`, `remove` |
315-
| `--only` | string | | Only process these placeholders (comma-separated) |
316-
| `--except` | string | | Skip these placeholders (comma-separated) |
317315

318316
**Arguments:**
319317
- `[template]` - Path to template file (required)

docs/guide/managing-secrets/cross-environment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ keep copy DB_USERNAME --from=development --to=staging
118118
keep set API_URL "https://staging-api.example.com" --stage=staging
119119

120120
# 4. Verify the promotion
121-
keep diff --stage=development,staging --keys-only
121+
keep diff --stage=development,staging
122122
```
123123

124124
### Cross-Vault Promotion

src/Commands/DiffCommand.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace STS\Keep\Commands;
44

55
use Illuminate\Support\Collection;
6+
use STS\Keep\Commands\Concerns\GathersInput;
67
use STS\Keep\Data\Context;
78
use STS\Keep\Data\SecretDiff;
89
use STS\Keep\Facades\Keep;
@@ -13,10 +14,12 @@
1314

1415
class DiffCommand extends BaseCommand
1516
{
17+
use GathersInput;
1618
public $signature = 'diff
1719
{--stage= : Comma-separated list of stages to compare (defaults to all configured stages)}
1820
{--vault= : Comma-separated list of vaults to compare (defaults to all configured vaults)}'
19-
.self::UNMASK_SIGNATURE;
21+
.self::UNMASK_SIGNATURE
22+
.self::ONLY_EXCLUDE_SIGNATURE;
2023

2124
public $description = 'Compare secrets across multiple stages and vaults in a matrix view';
2225

@@ -34,7 +37,7 @@ public function process()
3437
}
3538

3639
$diffService = new DiffService;
37-
$diffs = spin(fn () => $diffService->compare($vaults, $stages), 'Gathering secrets for comparison...');
40+
$diffs = spin(fn () => $diffService->compare($vaults, $stages, $this->option('only'), $this->option('except')), 'Gathering secrets for comparison...');
3841

3942
if ($diffs->isNotEmpty()) {
4043
$this->displayTable($diffs, $vaults, $stages, $diffService);

src/Commands/ExportCommand.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22

33
namespace STS\Keep\Commands;
44

5+
use STS\Keep\Commands\Concerns\GathersInput;
56
use STS\Keep\Data\Collections\SecretCollection;
67
use STS\Keep\Facades\Keep;
78

89
class ExportCommand extends BaseCommand
910
{
11+
use GathersInput;
12+
1013
public $signature = 'export
1114
{--format=env : json|env}
1215
{--output= : File where to save the output (defaults to stdout)}
1316
{--overwrite : Overwrite the output file if it exists}
1417
{--append : Append to the output file if it exists}
1518
{--stage= : The stage to export secrets for}
16-
{--vault= : The vault(s) to export from (comma-separated, defaults to all configured vaults)}';
19+
{--vault= : The vault(s) to export from (comma-separated, defaults to all configured vaults)}'
20+
.self::ONLY_EXCLUDE_SIGNATURE;
1721

1822
public $description = 'Export stage secrets from vault(s)';
1923

@@ -63,7 +67,10 @@ protected function loadSecretsFromVaults(array $vaultNames, string $stage): Secr
6367

6468
foreach ($vaultNames as $vaultName) {
6569
$vault = Keep::vault($vaultName, $stage);
66-
$vaultSecrets = $vault->list();
70+
$vaultSecrets = $vault->list()->filterByPatterns(
71+
only: $this->option('only'),
72+
except: $this->option('except')
73+
);
6774

6875
// Merge secrets, later vaults override earlier ones for duplicate keys
6976
$allSecrets = $allSecrets->merge($vaultSecrets);

src/Services/DiffService.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
class DiffService
1111
{
12-
public function compare(array $vaults, array $stages): Collection
12+
public function compare(array $vaults, array $stages, ?string $only = null, ?string $except = null): Collection
1313
{
14-
$allSecrets = $this->gatherSecrets($vaults, $stages);
14+
$allSecrets = $this->gatherSecrets($vaults, $stages, $only, $except);
1515
$allKeys = $this->extractAllKeys($allSecrets);
1616

1717
return $allKeys->map(function (string $key) use ($allSecrets) {
@@ -46,7 +46,7 @@ public function generateSummary(Collection $diffs, array $vaults, array $stages)
4646
];
4747
}
4848

49-
protected function gatherSecrets(array $vaults, array $stages): array
49+
protected function gatherSecrets(array $vaults, array $stages, ?string $only = null, ?string $except = null): array
5050
{
5151
$allSecrets = [];
5252

@@ -55,7 +55,8 @@ protected function gatherSecrets(array $vaults, array $stages): array
5555
$vaultStageKey = "{$vaultName}.{$stage}";
5656

5757
try {
58-
$allSecrets[$vaultStageKey] = Keep::vault($vaultName, $stage)->list();
58+
$secrets = Keep::vault($vaultName, $stage)->list();
59+
$allSecrets[$vaultStageKey] = $secrets->filterByPatterns(only: $only, except: $except);
5960
} catch (\Exception $e) {
6061
// If we can't access a vault/stage combination, treat it as empty
6162
$allSecrets[$vaultStageKey] = new SecretCollection([]);

tests/Feature/DiffCommandTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,48 @@
363363
});
364364
});
365365

366+
describe('filtering options', function () {
367+
it('accepts only option for filtering keys', function () {
368+
$commandTester = runCommand('diff', [
369+
'--only' => 'API_*',
370+
'--stage' => 'testing',
371+
]);
372+
373+
$output = stripAnsi($commandTester->getDisplay());
374+
375+
// Should accept --only option without validation error
376+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
377+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
378+
});
379+
380+
it('accepts except option for excluding keys', function () {
381+
$commandTester = runCommand('diff', [
382+
'--except' => 'SECRET_*',
383+
'--stage' => 'testing',
384+
]);
385+
386+
$output = stripAnsi($commandTester->getDisplay());
387+
388+
// Should accept --except option without validation error
389+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
390+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
391+
});
392+
393+
it('accepts both only and except options together', function () {
394+
$commandTester = runCommand('diff', [
395+
'--only' => 'API_*,DB_*',
396+
'--except' => 'SECRET_*',
397+
'--stage' => 'testing,production',
398+
]);
399+
400+
$output = stripAnsi($commandTester->getDisplay());
401+
402+
// Should accept both filtering options without error
403+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
404+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
405+
});
406+
});
407+
366408
// Tests migrated from Laravel-style tests to new architecture.
367409
// Focus on command structure, parameter validation, error handling, and output format
368410
// rather than full integration tests that depend on external AWS services and pre-populated data.

tests/Feature/ExportCommandTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,64 @@
401401
});
402402
});
403403

404+
describe('filtering options', function () {
405+
it('accepts only option for filtering keys', function () {
406+
$commandTester = runCommand('export', [
407+
'--only' => 'API_*',
408+
'--stage' => 'testing',
409+
]);
410+
411+
$output = stripAnsi($commandTester->getDisplay());
412+
413+
// Should accept --only option without validation error
414+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
415+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
416+
});
417+
418+
it('accepts except option for excluding keys', function () {
419+
$commandTester = runCommand('export', [
420+
'--except' => 'SECRET_*',
421+
'--stage' => 'testing',
422+
]);
423+
424+
$output = stripAnsi($commandTester->getDisplay());
425+
426+
// Should accept --except option without validation error
427+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
428+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
429+
});
430+
431+
it('accepts both only and except options together', function () {
432+
$commandTester = runCommand('export', [
433+
'--only' => 'API_*,DB_*',
434+
'--except' => 'SECRET_*',
435+
'--stage' => 'testing',
436+
'--format' => 'json',
437+
]);
438+
439+
$output = stripAnsi($commandTester->getDisplay());
440+
441+
// Should accept both filtering options without error
442+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
443+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
444+
});
445+
446+
it('combines filtering with other options correctly', function () {
447+
$commandTester = runCommand('export', [
448+
'--only' => 'APP_*',
449+
'--format' => 'env',
450+
'--vault' => 'test',
451+
'--stage' => 'testing',
452+
]);
453+
454+
$output = stripAnsi($commandTester->getDisplay());
455+
456+
// Should handle filtering with other options
457+
expect($output)->not->toMatch('/(invalid.*option|unknown.*option)/i');
458+
expect($commandTester->getStatusCode())->toBeGreaterThanOrEqual(0);
459+
});
460+
});
461+
404462
// Tests migrated from Laravel-style tests to new architecture.
405463
// Focus on command structure, parameter validation, file handling, and format options
406464
// rather than full integration tests that depend on external AWS services and pre-populated data.

0 commit comments

Comments
 (0)