Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `networkconnectivity` | — | — | [NetworkConnectivity](./gcp/networkconnectivity.md) | — | 11 |
| `networkfirewall` | [NetworkFirewall](./aws/networkfirewall.md) | — | — | — | 23 |
| `networking` | [VPC](./aws/vpc.md) | [VNet](./azure/vnet.md) | [VPC](./gcp/vpc.md) | [VCN](./oci/vcn.md) | 57 |
| `notification` | [SNS](./aws/sns.md) | [NotificationHubs](./azure/notificationhubs.md) | [FCM](./gcp/fcm.md) | | 9 |
| `notification` | [SNS](./aws/sns.md) | [NotificationHubs](./azure/notificationhubs.md) | [FCM](./gcp/fcm.md) | [Notifications](./oci/notifications.md) | 9 |
| `opensearch` | [OpenSearch](./aws/opensearch.md) | — | — | — | 96 |
| `parameterstore` | [SSM](./aws/ssm.md) | — | — | — | 9 |
| `postgresflex` | — | [PostgresFlex](./azure/postgresflex.md) | — | — | 21 |
Expand Down
3 changes: 2 additions & 1 deletion docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -10973,7 +10973,8 @@
"providers": {
"aws": "SNS",
"azure": "NotificationHubs",
"gcp": "FCM"
"gcp": "FCM",
"oci": "Notifications"
}
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/coverage/oci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Services cloudemu emulates for OCI, by native name. Back to the [cross-provider
| --- | --- | --- |
| [Identity](./identity.md) | `iam` | 40 |
| [Monitoring](./monitoring.md) | `monitoring` | 12 |
| [Notifications](./notifications.md) | `notification` | 9 |
| [VCN](./vcn.md) | `networking` | 57 |
| [Workrequest](./workrequest.md) | — (provider-native) | 4 |
22 changes: 22 additions & 0 deletions docs/coverage/oci/notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Generated by `go generate ./...` (internal/coveragegen). Do not edit by hand. -->
# Notifications

OCI's `notification` service · portable interface `driver.Notification` · [OCI index](./README.md)

## Operations (9)

| Operation | Description |
| --- | --- |
| `CreateTopic` | |
| `DeleteTopic` | |
| `GetTopic` | |
| `ListSubscriptions` | |
| `ListTopics` | |
| `Publish` | |
| `Subscribe` | |
| `Unsubscribe` | |
| `UpdateTopic` | UpdateTopic replaces the mutable fields (display name, tags) of an |

## Not in scope

_Not documented yet. See the [emulator boundary](../../../README.md) for cloudemu-wide non-goals._
72 changes: 71 additions & 1 deletion docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -1464,7 +1464,7 @@ a source cluster and detach on promote; clone-on-read on every path.
## 14. Notification

**Driver interface:** `services/notification/driver/driver.go`
**AWS:** SNS | **Azure:** Notification Hubs | **GCP:** FCM
**AWS:** SNS | **Azure:** Notification Hubs | **GCP:** FCM | **OCI:** Notifications (ONS)

### Topic Operations

Expand All @@ -1491,6 +1491,76 @@ a source cluster and detach on promote; clone-on-read on every path.

**Total: 8 operations**

### OCI Notifications (ONS)

**Optional capability:** `server/oci/notifications.Extras` — ONS scopes topics
and subscriptions to a compartment, addresses both by OCID, and gates delivery
behind a confirmation handshake, none of which the portable model carries. Its
value types live in `providers/oci/notifications`; a driver that does not
implement `Extras` is served `501` for every path the handler claims.
**Provider:** `providers/oci/notifications` | **Wire:** `server/oci/notifications`

| Operation | Route |
|-----------|-------|
| `CreateTopic` | `POST /20181201/topics` |
| `ListTopics` | `GET /20181201/topics` |
| `GetTopic` | `GET /20181201/topics/{topicId}` |
| `UpdateTopic` | `PUT /20181201/topics/{topicId}` |
| `DeleteTopic` | `DELETE /20181201/topics/{topicId}` |
| `ChangeTopicCompartment` | `POST /20181201/topics/{topicId}/actions/changeCompartment` |
| `PublishMessage` | `POST /20181201/topics/{topicId}/messages` |
| `CreateSubscription` | `POST /20181201/subscriptions` |
| `ListSubscriptions` | `GET /20181201/subscriptions` |
| `GetSubscription` | `GET /20181201/subscriptions/{id}` |
| `UpdateSubscription` | `PUT /20181201/subscriptions/{id}` |
| `DeleteSubscription` | `DELETE /20181201/subscriptions/{id}` |
| `GetConfirmSubscription` | `GET /20181201/subscriptions/{id}/confirmation` |
| `GetUnsubscription` | `GET /20181201/subscriptions/{id}/unsubscription` |
| `ChangeSubscriptionCompartment` | `POST /20181201/subscriptions/{id}/actions/changeCompartment` |
| `ResendSubscriptionConfirmation` | `POST /20181201/subscriptions/{id}/actions/resendConfirmation` |

Both list routes require `compartmentId` and paginate with `limit` / `page`,
returning the cursor as `opc-next-page`. `ListTopics` also honours `sortBy`
(`TIMECREATED`, `LIFECYCLESTATE`) with `sortOrder` `ASC` / `DESC`, plus `id`,
`name` and `lifecycleState` filters; an unknown sort key is rejected rather
than answered in an arbitrary order.
`definedTags` are rejected rather than echoed back empty;
`freeformTags` round-trip.

Both creates answer `201 Created`. Updating or deleting a topic or a
subscription honours an `if-match` precondition against the stored etag, which
rotates on every mutation: a stale etag is a `412` with code `NoEtagMatch` and
the resource is left alone. An absent `if-match` is unconditional.

Real ONS splits the control plane from the data plane **by host, not by API
prefix**: `PublishMessage` goes to the topic's own `apiEndpoint` rather than to
a differently-prefixed path. CloudEmu serves both on one listener, so every
topic reports the origin the caller reached as its `apiEndpoint` and a publish
posted there lands back on the same handler. A client that follows
`apiEndpoint` — as the real SDKs do — needs no special casing.

A subscription is created `PENDING` and receives nothing until it is confirmed.
Real ONS mails the confirmation token to the endpoint; the emulator has no
channel to mail it on, so a `PENDING` subscription carries its token in the
create response, and `GET .../confirmation?token=…&protocol=…` flips it to
`ACTIVE`. Publishing to a topic whose subscriptions are all `PENDING`
succeeds and delivers to nobody. `.../unsubscription` takes the same token pair
and removes the subscription. Protocols are `EMAIL`, `SMS`, `CUSTOM_HTTPS`,
`SLACK`, `PAGERDUTY` and `ORACLE_FUNCTIONS` (`HTTP` / `HTTPS` alias onto
`CUSTOM_HTTPS`); anything else is rejected rather than stored unused. The
endpoint is checked against the protocol at create rather than at first
delivery: an `EMAIL` endpoint must hold an `@`, and a `CUSTOM_HTTPS`, `SLACK`
or `PAGERDUTY` endpoint must be an `https` URL. Message bodies are `RAW_TEXT`
or `JSON` and are capped at ONS's 64 KB.

`DeleteTopic` is the one asynchronous mutation: it answers **`204` with an
`opc-work-request-id`**, not the `202` the rest of OCI uses for async work, and
the work request is resolvable through `server/oci/workrequest`.
`ChangeTopicCompartment` records a work request too and answers `202`. Every
subscription mutation is synchronous. Delivery is recorded in-memory and
readable through `Deliveries`; nothing is sent over a real transport, so
delivery policies (`backoffRetryPolicy`) round-trip but never retry.

---

## 15. Container Registry
Expand Down
16 changes: 13 additions & 3 deletions providers/oci/monitoring/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ type metricSeries struct {

// PostMetricData records metric data points against a compartment.
func (m *Mock) PostMetricData(_ context.Context, compartmentID, resourceGroup string, data []driver.MetricDatum) error {
return m.postMetricData(compartmentID, resourceGroup, data, false)
}

// postMetricData records metric data points. allowReserved admits the `oci_`
// namespaces Oracle keeps for its own service metrics, which a sibling mock
// emitting its service's metrics is the producer of and the public
// PostMetricData is not.
func (m *Mock) postMetricData(
compartmentID, resourceGroup string, data []driver.MetricDatum, allowReserved bool,
) error {
if compartmentID == "" {
return cerrors.New(cerrors.InvalidArgument, "compartmentId is required")
}
Expand All @@ -89,7 +99,7 @@ func (m *Mock) PostMetricData(_ context.Context, compartmentID, resourceGroup st
}

for i := range data {
if err := validateDatum(&data[i]); err != nil {
if err := validateDatum(&data[i], allowReserved); err != nil {
return err
}
}
Expand Down Expand Up @@ -320,7 +330,7 @@ func resolutionOf(interval time.Duration, resolution string) (time.Duration, err
// validateDatum rejects a data point real OCI would reject. Namespace and
// dimension shapes are checked; the metadata, per-request datapoint cap and
// ingestion time window are not.
func validateDatum(d *driver.MetricDatum) error {
func validateDatum(d *driver.MetricDatum, allowReserved bool) error {
switch {
case d.Namespace == "":
return cerrors.New(cerrors.InvalidArgument, "namespace is required")
Expand All @@ -329,7 +339,7 @@ func validateDatum(d *driver.MetricDatum) error {
case !validNamespace(d.Namespace):
return cerrors.Newf(cerrors.InvalidArgument,
"namespace %q must start with a letter and hold only letters, digits and underscores", d.Namespace)
case reservedNamespace(d.Namespace):
case !allowReserved && reservedNamespace(d.Namespace):
return cerrors.Newf(cerrors.InvalidArgument, "namespace %q uses a prefix Oracle reserves", d.Namespace)
case d.MetricName == "":
return cerrors.New(cerrors.InvalidArgument, "metric name is required")
Expand Down
8 changes: 5 additions & 3 deletions providers/oci/monitoring/monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ func New(opts *config.Options) *Mock {
}
}

// PutMetricData stores metric data points in the default compartment.
func (m *Mock) PutMetricData(ctx context.Context, data []driver.MetricDatum) error {
return m.PostMetricData(ctx, m.opts.CompartmentID, "", data)
// PutMetricData stores metric data points in the default compartment. It is
// the in-process path a sibling mock emits its own service metrics on, so it
// admits the `oci_` namespaces those metrics live under.
func (m *Mock) PutMetricData(_ context.Context, data []driver.MetricDatum) error {
return m.postMetricData(m.opts.CompartmentID, "", data, true)
}

// GetMetricData aggregates the matching series in the default compartment into
Expand Down
Loading
Loading