Skip to content
Merged
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`OriginAccessControlAlreadyExists`/409 is published for a control "with the specified parameters"
without publishing which parameters, and a control carries no `CallerReference` to key a duplicate
on. `UpdateOriginAccessControl` is not implemented.
- **CloudWatch Logs tagging: `TagResource`, `UntagResource` and `ListTagsForResource`, and the ARN
rule that decides which ARN they take** (#1273). The issue reports a validation refusal on
`TagResource`; the operation was absent, so the request was refused as an unknown action rather
than accepted with the wrong ARN, and the trio arrives with the rule because a rule on an
unroutable operation is untestable. The rule is that the operations take the **unsuffixed**
log-group ARN and refuse the IAM-policy form with the trailing `:*`. Substrate hands a caller the
refused form itself — `DescribeLogGroups` reports both, and the suffixed one travels under the
shorter member name `arn` — so reaching for it is the natural mistake, and a fake that accepts any
ARN keeps a tagging path green offline and then fails on every log group of a live deploy. The rule
is applied to the read as well as to the two writes: a convergence path reads before it writes, and
a `ListTagsForResource` that accepted the suffixed form would report a group untagged rather than
naming the ARN to send. The refusal is `ValidationException` / `Invalid resourceArn`, which is
**observed** behavior rather than published — none of the three pages lists a `ValidationException`
at all — though what is published supports refusing: `resourceArn` carries Pattern `[\w+=/:,.@-]*`,
a character class with no `*` in it, so the suffixed form violates the request model before any
resource is resolved. Only the code and the message text come from observation. A `:log-stream:`
suffix is refused the same way; a `destination:` ARN — the
other type all three pages publish as taggable — answers `ResourceNotFoundException`, because
substrate models no destinations, so it is the resource that is absent rather than the ARN that is
wrong. `CreateLogGroup`'s inline `tags` are now persisted and read back, having been decoded and
discarded before, which is the half of the convergence path that looked like it worked. `tags` is a
string-to-string map on both the request and the response, not the `{key, value}` array most
services use, and an untagged group answers `{"tags":{}}` rather than omitting the member. The
50-tag ceiling is enforced at `TagResource` against the *merged* set, since a one-tag request
against a group holding fifty exceeds it while the request alone does not; `CreateLogGroup` is left
without it, because its page publishes no code for exceeding the `tags` maximum and
`TooManyTagsException` is published on `TagResource` alone (#671). The account and Region come from
the ARN and never from the caller's context — #826's rule, which is why the three handlers are the
only ones in the plugin that take no request context at all.

### Changed

Expand Down
64 changes: 63 additions & 1 deletion docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -14478,6 +14478,9 @@ KMS API requests: $0.03 per 10,000 requests.
| PutLogEvents | Accepts up to 10,000 events per call |
| GetLogEvents | Issues both `nextForwardToken` and `nextBackwardToken`; reads `startFromHead`; refuses a `nextToken` it did not issue |
| FilterLogEvents | Substring match on `filterPattern`; reports `searchedLogStreams`; refuses a `nextToken` it did not issue |
| TagResource | Takes the **unsuffixed** log-group ARN — see below; refuses the 51st tag |
| UntagResource | Same ARN rule; an absent key and an empty `tagKeys` are both no-ops |
| ListTagsForResource | Same ARN rule; an untagged group answers `{"tags":{}}` |

Lambda auto-creates `/aws/lambda/{name}` log groups.

Expand All @@ -14494,7 +14497,9 @@ field read raised `KeyError`. All four now emit the API's member names.
`DescribeLogGroups` reports **both** ARN forms the reference documents as distinct
members: `logGroupArn` without a trailing `:*`, which is what a
`logGroupIdentifier` input or a tagging API wants, and `arn` with it, which is
what an IAM policy wants for most actions. They differ only in that suffix.
what an IAM policy wants for most actions. They differ only in that suffix, and
which of the two the tagging operations accept is [a rule of its
own](#tagging-a-log-group-takes-the-unsuffixed-arn).

A group with no retention policy omits `retentionInDays` entirely rather than
reporting `0`, because the API has no value meaning "never" — the member's absence
Expand All @@ -14520,6 +14525,63 @@ for the provenance, the one divergence the change deliberately leaves in place (
24-hour token expiry, which is declined rather than deferred), and the argument that the token
is validated before any listing is read.

### Tagging a log group takes the unsuffixed ARN

The three tagging operations accept the **unsuffixed** log-group ARN and refuse the
IAM-policy form:

```
arn:aws:logs:us-west-2:123456789012:log-group:/aws/lambda/foray-gateway accepted
arn:aws:logs:us-west-2:123456789012:log-group:/aws/lambda/foray-gateway:* ValidationException: Invalid resourceArn
```

The trap is that substrate hands a caller the refused form itself: as the section
above records, `DescribeLogGroups` reports both, and the suffixed one travels under
the shorter member name `arn`. Reaching for it is the natural mistake, since it is
also the ARN an IAM policy wants, and a hand-written fake that accepts any ARN keeps
a tagging path green offline and then fails on every group of a live deploy — which
is the report [#1273](https://github.com/scttfrdmn/substrate/issues/1273) was filed
from.

The same rule applies to all three operations, not to the write alone: a convergence
path reads before it writes, and a `ListTagsForResource` that accepted the suffixed
form would report a group as untagged rather than telling the caller which ARN to
send.

**Provenance.** `ValidationException` / `Invalid resourceArn` is **observed** real-AWS
behaviour (us-west-2), not published: the pages for all three operations list
`InvalidParameterException`/400, `ResourceNotFoundException`/400 and
`ServiceUnavailableException`/500, plus `TooManyTagsException`/400 on `TagResource`,
and none of them lists a `ValidationException` at all. What *is* published supports
refusing — `resourceArn` carries Pattern `[\w+=/:,.@-]*`, a character class with no
`*` in it, so the suffixed form violates the request model before any resource is
resolved. Only the code and the message text come from observation.

A `:log-stream:{name}` suffix is refused the same way, since a stream is not one of
the two types these operations accept. The other type that *is* accepted —
`destination:{name}` — answers `ResourceNotFoundException`, because substrate models
no destinations at all: the ARN names a taggable type and a resource that cannot
exist here, so it is the resource that is missing rather than the ARN that is wrong.

Three further readings worth knowing:

- `tags` is a **string-to-string map** on both the `TagResource` request and the
`ListTagsForResource` response, not the array of `{key, value}` objects most
services use. An untagged group answers `{"tags":{}}` rather than omitting the
member, so a caller comparing maps never has to tell nil from empty.
- `CreateLogGroup`'s inline `tags` are persisted and read back by
`ListTagsForResource`. They were decoded and discarded before #1273, so a group
created with tags inline reported none.
- The 50-tag ceiling is enforced at `TagResource` only, against the **merged** set —
a request of one tag against a group already holding fifty is refused with
`TooManyTagsException`. `CreateLogGroup` accepts more, because its page publishes
no error code for exceeding the `tags` map maximum and inventing one would assert a
refusal AWS documents nowhere ([#671](https://github.com/scttfrdmn/substrate/issues/671)).

Note also what tagging does **not** reach: a log group is not resolvable through the
Resource Groups Tagging API, which has no `logs` arm in its ARN resolver, so
`GetResources` does not report one.

### GetLogEvents pages by a pair of tokens

`GetLogEvents` is the one Logs paginator whose response carries **two** tokens, and whose termination
Expand Down
13 changes: 12 additions & 1 deletion emulator/cloudwatchlogs_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (
// CloudWatchLogsPlugin emulates the Amazon CloudWatch Logs JSON-protocol API.
// It handles CreateLogGroup, DeleteLogGroup, DescribeLogGroups,
// PutRetentionPolicy, DeleteRetentionPolicy, CreateLogStream, DeleteLogStream,
// DescribeLogStreams, PutLogEvents, GetLogEvents, and FilterLogEvents.
// DescribeLogStreams, PutLogEvents, GetLogEvents, FilterLogEvents, TagResource,
// UntagResource, and ListTagsForResource.
type CloudWatchLogsPlugin struct {
state StateManager
logger Logger
Expand Down Expand Up @@ -65,6 +66,12 @@ func (p *CloudWatchLogsPlugin) HandleRequest(ctx *RequestContext, req *AWSReques
return p.getLogEvents(ctx, req)
case "FilterLogEvents":
return p.filterLogEvents(ctx, req)
case "TagResource":
return p.tagResource(req)
case "UntagResource":
return p.untagResource(req)
case "ListTagsForResource":
return p.listTagsForResource(req)
default:
return nil, unknownActionError(p.Name(), req.Operation)
}
Expand Down Expand Up @@ -95,11 +102,15 @@ func (p *CloudWatchLogsPlugin) createLogGroup(ctx *RequestContext, req *AWSReque
return nil, &AWSError{Code: "ResourceAlreadyExistsException", Message: "Log group already exists: " + body.LogGroupName, HTTPStatus: http.StatusConflict}
}

// The tags the request carries are persisted, so ListTagsForResource reports them. They were
// decoded and dropped before #1273 added the tagging trio: a group created with tags inline
// read back as untagged, which is the half of the convergence path that looked like it worked.
lg := CWLogGroup{
LogGroupName: body.LogGroupName,
ARN: cwLogGroupARN(ctx.Region, ctx.AccountID, body.LogGroupName),
CreationTime: p.tc.Now().UnixMilli(),
RetentionInDays: body.RetentionInDays,
Tags: body.Tags,
}
data, err := json.Marshal(lg)
if err != nil {
Expand Down
Loading
Loading