Skip to content

Commit 73c6719

Browse files
committed
fix(oci): per-vault secret names, KMS ENABLED state, and Vault persistence
Address review findings on the OCI Vault branch. Snapshot: providers/oci/vault had no snapshot.go, so vault.Mock was the only providers/oci package that was not Snapshottable and TestSnapshotCompleteness failed. Add it over all four stores, carrying the portable driver's default vault and key so a restored mock does not mint a second one and orphan the secrets it just restored. KMS lifecycle: keys and key versions reported ACTIVE. Per the SDK's KeyLifecycleStateEnum they are ENABLED and never ACTIVE — ACTIVE belongs to vaults and secrets — and terraform-provider-oci's oci_kms_key waits on ENABLED, so an ACTIVE key hangs the apply. Secret names: uniqueness was enforced across every vault, so the same name in two vaults was refused and CancelOCISecretDeletion could wrongly refuse a restore. Scope it to the vault, as OCI does. The portable driver keys by bare name, so it now resolves across vaults and rejects an ambiguous name naming both, rather than silently picking one; its create refuses a name another vault already holds. getByName prefers the live secret over a namesake pending deletion. Also document the Vault surface in docs/services.md, repair the garbled VaultManagement doc comment, record why a secret's deletion actions answer 204 where a key's and a vault's answer 200, and raise coverage on both changed packages (providers 96.9%, wire 79.7% -> 94.1%).
1 parent 0b92e4a commit 73c6719

16 files changed

Lines changed: 1413 additions & 40 deletions

File tree

‎docs/coverage/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
133133
| `sagemaker` | [SageMaker](./aws/sagemaker.md) | — | — | — | 121 |
134134
| `savingsplans` | [SavingsPlans](./aws/savingsplans.md) | — | — | — | 9 |
135135
| `scheduler` | — | — | [Scheduler](./gcp/scheduler.md) | — | 8 |
136-
| `secrets` | [SecretsManager](./aws/secretsmanager.md) | [KeyVault](./azure/keyvault.md) | [SecretManager](./gcp/secretmanager.md) | — | 7 |
136+
| `secrets` | [SecretsManager](./aws/secretsmanager.md) | [KeyVault](./azure/keyvault.md) | [SecretManager](./gcp/secretmanager.md) | [Vault](./oci/vault.md) | 7 |
137137
| `serverless` | [Lambda](./aws/lambda.md) | [Functions](./azure/functions.md) | [CloudFunctions](./gcp/cloudfunctions.md) | — | 27 |
138138
| `servicedirectory` | — | — | [ServiceDirectory](./gcp/servicedirectory.md) | — | 15 |
139139
| `servicenetworking` | — | — | [Servicenetworking](./gcp/servicenetworking.md) | — | 3 |

‎docs/coverage/oci/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ Services cloudemu emulates for OCI, by native name. Back to the [cross-provider
88
| [Identity](./identity.md) | `iam` | 40 |
99
| [Monitoring](./monitoring.md) | `monitoring` | 12 |
1010
| [VCN](./vcn.md) | `networking` | 57 |
11+
| [Vault](./vault.md) | `secrets` | 7 |
1112
| [Workrequest](./workrequest.md) | — (provider-native) | 4 |

‎docs/services.md‎

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,8 @@ a source cluster and detach on promote; clone-on-read on every path.
13961396
## 12. Secrets
13971397

13981398
**Driver interface:** `services/secrets/driver/driver.go`
1399-
**AWS:** Secrets Manager | **Azure:** Key Vault | **GCP:** Secret Manager
1399+
**AWS:** Secrets Manager | **Azure:** Key Vault | **GCP:** Secret Manager |
1400+
**OCI:** Vault
14001401

14011402
### Secret Operations
14021403

@@ -1417,6 +1418,134 @@ a source cluster and detach on promote; clone-on-read on every path.
14171418

14181419
**Total: 7 operations**
14191420

1421+
### OCI Vault
1422+
1423+
**Optional capability:** `server/oci/vault.Extras` — OCI keeps secret storage
1424+
and key management in one service, addresses everything by OCID, scopes it to a
1425+
compartment and a vault, and only ever *schedules* a deletion. None of that fits
1426+
the portable seven. Its value types live in `providers/oci/vault`.
1427+
**Provider:** `providers/oci/vault` | **Wire:** `server/oci/vault`
1428+
1429+
OCI publishes Vault under two API versions and CloudEmu claims both, because one
1430+
HTTP server stands in for every OCI endpoint: `/20180608` carries KMS and secret
1431+
management, `/20190301` the secret-retrieval data plane.
1432+
1433+
#### Vaults (`/20180608`)
1434+
1435+
| Operation | Route |
1436+
|-----------|-------|
1437+
| `CreateVault` | `POST /vaults` |
1438+
| `ListVaults` | `GET /vaults` |
1439+
| `GetVault` | `GET /vaults/{vaultId}` |
1440+
| `UpdateVault` | `PUT /vaults/{vaultId}` |
1441+
| `ScheduleVaultDeletion` | `POST /vaults/{vaultId}/actions/scheduleDeletion` |
1442+
| `CancelVaultDeletion` | `POST /vaults/{vaultId}/actions/cancelDeletion` |
1443+
| `ChangeVaultCompartment` | `POST /vaults/{vaultId}/actions/changeCompartment` |
1444+
1445+
#### Master encryption keys (`/20180608`)
1446+
1447+
| Operation | Route |
1448+
|-----------|-------|
1449+
| `CreateKey` | `POST /keys` |
1450+
| `ListKeys` | `GET /keys` |
1451+
| `GetKey` | `GET /keys/{keyId}` |
1452+
| `UpdateKey` | `PUT /keys/{keyId}` |
1453+
| `ScheduleKeyDeletion` | `POST /keys/{keyId}/actions/scheduleDeletion` |
1454+
| `CancelKeyDeletion` | `POST /keys/{keyId}/actions/cancelDeletion` |
1455+
| `ChangeKeyCompartment` | `POST /keys/{keyId}/actions/changeCompartment` |
1456+
| `CreateKeyVersion` | `POST /keys/{keyId}/keyVersions` |
1457+
| `ListKeyVersions` | `GET /keys/{keyId}/keyVersions` |
1458+
| `GetKeyVersion` | `GET /keys/{keyId}/keyVersions/{keyVersionId}` |
1459+
1460+
The KMS management endpoint is per-vault in real OCI, where the vault is
1461+
addressed by host. CloudEmu serves every vault from one host, so `/keys` takes
1462+
`vaultId` as a query parameter instead.
1463+
1464+
#### Secrets (`/20180608`)
1465+
1466+
| Operation | Route |
1467+
|-----------|-------|
1468+
| `CreateSecret` | `POST /secrets` |
1469+
| `ListSecrets` | `GET /secrets` |
1470+
| `GetSecret` | `GET /secrets/{secretId}` |
1471+
| `UpdateSecret` | `PUT /secrets/{secretId}` |
1472+
| `GetSecretByName` | `GET /secrets/actions/getByName` |
1473+
| `ScheduleSecretDeletion` | `POST /secrets/{secretId}/actions/scheduleDeletion` |
1474+
| `CancelSecretDeletion` | `POST /secrets/{secretId}/actions/cancelDeletion` |
1475+
| `ChangeSecretCompartment` | `POST /secrets/{secretId}/actions/changeCompartment` |
1476+
| `ListSecretVersions` | `GET /secrets/{secretId}/versions` |
1477+
| `GetSecretVersion` | `GET /secrets/{secretId}/versions/{versionNumber}` |
1478+
| `ScheduleSecretVersionDeletion` | `POST /secrets/{secretId}/versions/{n}/actions/scheduleDeletion` |
1479+
| `CancelSecretVersionDeletion` | `POST /secrets/{secretId}/versions/{n}/actions/cancelDeletion` |
1480+
1481+
#### Secret bundles — the retrieval data plane (`/20190301`)
1482+
1483+
| Operation | Route |
1484+
|-----------|-------|
1485+
| `GetSecretBundle` | `GET /secretbundles/{secretId}` |
1486+
| `ListSecretBundleVersions` | `GET /secretbundles/{secretId}/versions` |
1487+
| `GetSecretBundleByName` | `GET /secretbundles/actions/getByName` |
1488+
1489+
A bundle read is the only way to get a secret's *value* back; the management
1490+
routes above return metadata only. The version is named by exactly one of
1491+
`versionNumber`, `secretVersionName` or `stage` — more than one is rejected
1492+
rather than silently ranked.
1493+
1494+
**Total: 32 operations**
1495+
1496+
#### Behavior
1497+
1498+
There is no secret outside a vault, and no secret without a master encryption
1499+
key, so the portable `CreateSecret` mints a default vault and AES key on first
1500+
use and puts every portable secret there. Portable *reads*, though, reach every
1501+
vault, so a secret created through the OCI-shaped surface is addressable
1502+
portably too.
1503+
1504+
OCI scopes secret names to the vault, so the same name in two vaults is two
1505+
different secrets and that reach makes a bare name ambiguous. Rather than
1506+
silently picking one, every portable operation that keys by name fails with
1507+
`InvalidArgument` naming both vaults, and `CreateSecret` refuses to mint a name
1508+
another vault already holds. `ListSecrets` is unaffected — it addresses nothing
1509+
by name. With one vault in play, which is all the AWS, Azure and GCP secret
1510+
mocks can have, none of this is observable.
1511+
1512+
The KMS surface and the secret surface report different live states, as real
1513+
OCI does: a key or key version is `ENABLED` and never `ACTIVE`, while a vault or
1514+
secret is `ACTIVE` and never `ENABLED`. This matters to consumers —
1515+
`terraform-provider-oci`'s `oci_kms_key` waits for `ENABLED` after a create or
1516+
rotate. `enableKey` / `disableKey` and the `ENABLING` / `DISABLING` / `DISABLED`
1517+
states are not served, so a key's lifecycle here is `ENABLED ⇄ PENDING_DELETION`.
1518+
1519+
Deletion is scheduled, never immediate. A vault, key, secret or secret version
1520+
moves to `PENDING_DELETION` and stays there — nothing reaps it — until the
1521+
deletion is cancelled. A secret pending deletion releases its name, so the same
1522+
name can be taken again in that vault; cancelling then fails rather than
1523+
producing two live secrets alike. The portable `DeleteSecret` schedules at the
1524+
soonest OCI permits, one day out, and the portable operations treat the secret
1525+
as gone from that moment, which is the same soft-delete the AWS Secrets Manager
1526+
mock exposes.
1527+
1528+
Secret versions are numbered, and each carries a stage: `CURRENT`, `PENDING`,
1529+
`PREVIOUS`, `LATEST` or `DEPRECATED`. Adding a version stages it, and promoting
1530+
one to `CURRENT` — via `UpdateSecret`'s `currentVersionNumber` — is how OCI
1531+
finishes a rotation staged as `PENDING`. The portable version identifier is
1532+
OCI's version number, so a non-numeric one is rejected.
1533+
1534+
Every list route requires `compartmentId` except the key-version, secret-version
1535+
and bundle routes, whose parent already names one. All paginate with `limit` /
1536+
`page`, returning the cursor as `opc-next-page`. The mutations real OCI runs
1537+
asynchronously record a work request and stamp `opc-work-request-id`;
1538+
`changeCompartment` answers `202` with nothing else, so a waiter must poll.
1539+
1540+
`definedTags` is rejected rather than accepted and dropped, since CloudEmu
1541+
models no tag namespaces — use `freeformTags`. `autoKeyRotationDetails`,
1542+
`externalKeyReference` and `desiredState` are rejected the same way.
1543+
1544+
The KMS crypto endpoint — `encrypt`, `decrypt`, `sign`, `verify`, `exportKey`
1545+
and `generateDataEncryptionKey` — is claimed only to answer `501` naming the
1546+
gap rather than leaving a caller with a bare `404`. CloudEmu records master
1547+
encryption keys but stores no key material, so there is no ciphertext to invent.
1548+
14201549
---
14211550

14221551
## 13. Logging

‎providers/oci/vault/key.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func (m *Mock) newKeyLocked(spec *KeySpec, mode string) *keyData {
139139
DisplayName: spec.DisplayName,
140140
Shape: spec.Shape,
141141
ProtectionMode: mode,
142-
LifecycleState: StateActive,
142+
LifecycleState: StateEnabled,
143143
TimeCreated: m.now(),
144144
Scope: scope.Scope{Compartment: m.compartmentOr(spec.CompartmentID)},
145145
FreeformTags: copyTags(spec.FreeformTags),
@@ -266,7 +266,7 @@ func (m *Mock) CancelKeyDeletion(id string) (*KeyInfo, error) {
266266
return nil, cerrors.Newf(cerrors.FailedPrecondition, "key %s is not scheduled for deletion", id)
267267
}
268268

269-
k.LifecycleState = StateActive
269+
k.LifecycleState = StateEnabled
270270
k.TimeOfDeletion = ""
271271

272272
info := toKeyInfo(k)
@@ -318,7 +318,7 @@ func (m *Mock) CreateKeyVersion(keyID string) (*KeyVersionInfo, error) {
318318
return nil, err
319319
}
320320

321-
if k.LifecycleState != StateActive {
321+
if k.LifecycleState != StateEnabled {
322322
return nil, cerrors.Newf(cerrors.FailedPrecondition, "key %s is %s", keyID, k.LifecycleState)
323323
}
324324

@@ -442,7 +442,7 @@ func toKeyVersionInfo(kv *keyVersionData) KeyVersionInfo {
442442
KeyID: kv.KeyID,
443443
VaultID: kv.VaultID,
444444
CompartmentID: kv.Scope.Compartment,
445-
LifecycleState: StateActive,
445+
LifecycleState: StateEnabled,
446446
TimeCreated: kv.TimeCreated,
447447
}
448448
}

‎providers/oci/vault/key_test.go‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func TestCreateKey(t *testing.T) {
8585

8686
require.NoError(t, err)
8787
assert.Equal(t, tc.expectMode, info.ProtectionMode)
88-
assert.Equal(t, StateActive, info.LifecycleState)
88+
assert.Equal(t, StateEnabled, info.LifecycleState)
8989
assert.True(t, strings.HasPrefix(info.ID, "ocid1.key.oc1.iad."), "got %q", info.ID)
9090
assert.True(t, strings.HasPrefix(info.CurrentKeyVersion, "ocid1.keyversion.oc1.iad."),
9191
"got %q", info.CurrentKeyVersion)
@@ -155,7 +155,7 @@ func TestKeyScheduledDeletionAndCancellation(t *testing.T) {
155155

156156
restored, err := m.CancelKeyDeletion(keyID)
157157
require.NoError(t, err)
158-
assert.Equal(t, StateActive, restored.LifecycleState)
158+
assert.Equal(t, StateEnabled, restored.LifecycleState)
159159

160160
_, err = m.CancelKeyDeletion(keyID)
161161
assert.Equal(t, cerrors.FailedPrecondition, cerrors.GetCode(err))
@@ -217,3 +217,41 @@ func TestUpdateAndMoveKey(t *testing.T) {
217217
_, err = m.GetKey("ocid1.key.oc1.iad.x")
218218
assert.Equal(t, cerrors.NotFound, cerrors.GetCode(err))
219219
}
220+
221+
// The KMS surface and the secret surface report different live states. Per the
222+
// SDK's KeyLifecycleStateEnum a key and a key version are ENABLED and never
223+
// ACTIVE, while a vault and a secret are ACTIVE and never ENABLED —
224+
// terraform-provider-oci's oci_kms_key waits on ENABLED, so a key reporting
225+
// ACTIVE hangs the apply.
226+
func TestKeyStatesAreEnabledAndResourceStatesAreActive(t *testing.T) {
227+
m := newTestMock()
228+
229+
v, err := m.CreateVault(&VaultSpec{CompartmentID: testCompartment, DisplayName: "v"})
230+
require.NoError(t, err)
231+
assert.Equal(t, StateActive, v.LifecycleState, "a vault is ACTIVE")
232+
233+
k, err := m.CreateKey(&KeySpec{
234+
CompartmentID: testCompartment, VaultID: v.ID, DisplayName: "k",
235+
Shape: KeyShape{Algorithm: AlgorithmAES, Length: 32},
236+
})
237+
require.NoError(t, err)
238+
assert.Equal(t, StateEnabled, k.LifecycleState, "a key is ENABLED, never ACTIVE")
239+
240+
kv, err := m.GetKeyVersion(k.ID, k.CurrentKeyVersion)
241+
require.NoError(t, err)
242+
assert.Equal(t, StateEnabled, kv.LifecycleState, "a key version is ENABLED, never ACTIVE")
243+
244+
rotated, err := m.CreateKeyVersion(k.ID)
245+
require.NoError(t, err)
246+
assert.Equal(t, StateEnabled, rotated.LifecycleState, "a rotated key version is ENABLED")
247+
248+
s, err := m.CreateOCISecret(&SecretSpec{
249+
CompartmentID: testCompartment, VaultID: v.ID, KeyID: k.ID,
250+
Name: "s", Content: []byte("v"),
251+
})
252+
require.NoError(t, err)
253+
assert.Equal(t, StateActive, s.LifecycleState, "a secret is ACTIVE")
254+
255+
// The two vocabularies stay distinct.
256+
assert.NotEqual(t, StateActive, StateEnabled)
257+
}

0 commit comments

Comments
 (0)