Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
db0936d
fix(cloudwatch): serve the query protocol so the AWS CLI works (#319)
thzgajendra Aug 4, 2026
ab8c259
fix(sns): deliver published messages to SQS subscriptions (#319)
thzgajendra Aug 4, 2026
fcf2aea
fix(eventbridge): deliver matched-rule events to SQS targets (#319)
thzgajendra Aug 4, 2026
2eb4dc5
fix(ec2): return *.NotFound for Describe by nonexistent ID (#319)
thzgajendra Aug 4, 2026
070116b
feat(sqs): serve GetQueueAttributes/SetQueueAttributes/PurgeQueue (#319)
thzgajendra Aug 4, 2026
6496d94
feat(lambda): route configuration, version, and alias sub-paths (#319)
thzgajendra Aug 4, 2026
85a08dd
feat(lambda): support AddPermission/GetPolicy/RemovePermission (#319)
thzgajendra Aug 4, 2026
6e53fd2
feat(ecr): implement GetAuthorizationToken (#319)
thzgajendra Aug 4, 2026
d4769e3
feat(secretsmanager): support UpdateSecret, TagResource, UntagResourc…
thzgajendra Aug 4, 2026
7b42b43
feat(ec2): implement CreateTags/DeleteTags (#319)
thzgajendra Aug 4, 2026
0a886a8
feat(sns): support TagResource and UntagResource (#319)
thzgajendra Aug 4, 2026
c8a5443
feat(dynamodb): serve TagResource/UntagResource/ListTagsOfResource (#…
thzgajendra Aug 4, 2026
9188143
fix(lambda): serve the tagging API (TagResource/UntagResource/ListTag…
thzgajendra Aug 4, 2026
8080944
feat(sqs): support TagQueue/UntagQueue/ListQueueTags (#319)
thzgajendra Aug 4, 2026
fa8f1f9
feat(ssm): support parameter tagging (Add/Remove/ListTagsForResource)…
thzgajendra Aug 4, 2026
d937b01
fix(s3): route HeadBucket and the bucket ?tagging sub-resource (#319)
thzgajendra Aug 4, 2026
54c86bd
test(ec2): update integration tests for Describe NotFound behavior (#…
thzgajendra Aug 4, 2026
cf5de0e
fix(resourceexplorer2): serve CreateIndex and GetDefaultView (#319)
thzgajendra Aug 4, 2026
b7983f4
feat(cloudwatchlogs): implement PutRetentionPolicy (#319)
thzgajendra Aug 4, 2026
2c74b80
feat(iam): support inline role policies (Put/Get/Delete/ListRolePolic…
thzgajendra Aug 4, 2026
e29e719
feat(elasticache): implement ModifyCacheCluster (#319)
thzgajendra Aug 4, 2026
ede9324
fix(dynamodb): apply FilterExpression in Query (#319)
thzgajendra Aug 4, 2026
3f725f4
fix(s3): honor max-keys and pagination in ListObjects (#319)
thzgajendra Aug 4, 2026
745d9fe
test(bedrock): stop ConverseStream flaking on connection teardown (#319)
thzgajendra Aug 4, 2026
acb7fe8
feat(ec2): implement DescribeRegions and DescribeInstanceTypes (#319)
thzgajendra Aug 4, 2026
7217888
feat(ec2): add CreateNetworkInterface, MonitorInstances, DescribeInst…
thzgajendra Aug 4, 2026
3fde28c
feat(redshift): implement CreateClusterParameterGroup and CreateClust…
thzgajendra Aug 4, 2026
3aab1bd
feat(dynamodb): serve DescribeTimeToLive and UpdateTimeToLive (#319)
thzgajendra Aug 4, 2026
dfc43ff
test(dynamodb): update unrouted-op assertion after TTL routing (#319)
thzgajendra Aug 4, 2026
4ea055c
feat(sns): implement SetTopicAttributes (#319)
thzgajendra Aug 4, 2026
45aaf11
feat(s3): bucket notifications with S3 -> SQS delivery (#319)
thzgajendra Aug 4, 2026
cb29dee
feat(lambda): stub invoke without a handler + event source mappings (…
thzgajendra Aug 4, 2026
14665d4
feat(cloudwatchlogs): support log-group tagging (#319)
thzgajendra Aug 4, 2026
b8ba473
feat(ecr): support repository tagging (Tag/Untag/ListTagsForResource)…
thzgajendra Aug 4, 2026
6796d69
feat(eventbridge): support TagResource/UntagResource/ListTagsForResou…
thzgajendra Aug 4, 2026
1af6358
feat(iam): support role tagging (TagRole/UntagRole/ListRoleTags) (#319)
thzgajendra Aug 4, 2026
dcb5fc6
feat(redshift): support CreateTags/DeleteTags/DescribeTags (#319)
thzgajendra Aug 4, 2026
8dacc6e
fix(ec2): move CreateNetworkInterface off the shared NetworkInterface…
thzgajendra Aug 4, 2026
513012e
fix(redshift): gate generic tag verbs by SigV4 scope to avoid collisi…
thzgajendra Aug 4, 2026
fcf2fc6
feat(elbv2): support AddTags and RemoveTags (#319)
thzgajendra Aug 4, 2026
37f2842
feat(eks): support cluster tagging (Tag/Untag/ListTagsForResource) (#…
thzgajendra Aug 4, 2026
697e709
feat(route53): support ChangeTagsForResource and ListTagsForResource …
thzgajendra Aug 4, 2026
8421354
fix(eventbridge): emit well-formed rule ARNs with region and account …
thzgajendra Aug 4, 2026
192b938
fix(eks): default cluster Kubernetes version instead of null (#319)
thzgajendra Aug 4, 2026
ead94bb
fix(aws): address #320 review — event/notification fidelity + parity …
thzgajendra Aug 4, 2026
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
11 changes: 4 additions & 7 deletions cloudemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7097,13 +7097,10 @@ func TestVolumeLifecycleAWS(t *testing.T) {
t.Fatal(err)
}

vols, err = p.EC2.DescribeVolumes(ctx, []string{vol.ID})
if err != nil {
t.Fatal(err)
}

if len(vols) != 0 {
t.Errorf("expected 0 volumes after delete, got %d", len(vols))
// Describing the deleted volume by ID now yields NotFound (issue #319,
// theme C: InvalidVolume.NotFound), not an empty success.
if _, err = p.EC2.DescribeVolumes(ctx, []string{vol.ID}); err == nil {
t.Error("expected NotFound describing a deleted volume, got nil")
}
}

Expand Down
6 changes: 6 additions & 0 deletions providers/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ func New(opts ...config.Option) *Provider {
p.Redshift.SetMonitoring(p.CloudWatch)
p.EKS.SetMonitoring(p.CloudWatch)
p.SageMaker.SetMonitoring(p.CloudWatch)
// SNS -> SQS fan-out: publishes deliver to SQS-protocol subscriptions.
p.SNS.SetSQSDeliverer(p.SQS)
// EventBridge -> SQS: matched rules deliver events to SQS targets.
p.EventBridge.SetSQSDeliverer(p.SQS)
// S3 -> SQS: object-create events deliver to bucket notification targets.
p.S3.SetSQSDeliverer(p.SQS)

p.ResourceDiscovery = resourcediscovery.New(
resourcediscovery.ProviderAWS, o.AccountID, o.Region,
Expand Down
1 change: 1 addition & 0 deletions providers/aws/awsiam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type roleData struct {
Path string
AssumeRolePolicyDoc string
Tags map[string]string
inlinePolicies map[string]string // policyName -> policy document JSON
}

type policyData struct {
Expand Down
87 changes: 87 additions & 0 deletions providers/aws/awsiam/rolepolicy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package awsiam

import (
"context"
"sort"

"github.com/stackshy/cloudemu/v2/errors"
)

// PutRolePolicy adds or replaces an inline policy on a role (IAM
// PutRolePolicy). Inline policies are embedded in the role, distinct from the
// managed policies attached via AttachRolePolicy.
func (m *Mock) PutRolePolicy(_ context.Context, roleName, policyName, policyDocument string) error {
m.mu.Lock()
defer m.mu.Unlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return errors.Newf(errors.NotFound, "role %q not found", roleName)
}

if rd.inlinePolicies == nil {
rd.inlinePolicies = make(map[string]string)
}

rd.inlinePolicies[policyName] = policyDocument

return nil
}

// GetRolePolicy returns an inline policy document by name (IAM GetRolePolicy).
func (m *Mock) GetRolePolicy(_ context.Context, roleName, policyName string) (string, error) {
m.mu.RLock()
defer m.mu.RUnlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return "", errors.Newf(errors.NotFound, "role %q not found", roleName)
}

doc, ok := rd.inlinePolicies[policyName]
if !ok {
return "", errors.Newf(errors.NotFound, "policy %q not found on role %q", policyName, roleName)
}

return doc, nil
}

// DeleteRolePolicy removes an inline policy from a role (IAM DeleteRolePolicy).
func (m *Mock) DeleteRolePolicy(_ context.Context, roleName, policyName string) error {
m.mu.Lock()
defer m.mu.Unlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return errors.Newf(errors.NotFound, "role %q not found", roleName)
}

if _, ok := rd.inlinePolicies[policyName]; !ok {
return errors.Newf(errors.NotFound, "policy %q not found on role %q", policyName, roleName)
}

delete(rd.inlinePolicies, policyName)

return nil
}

// ListRolePolicies returns the names of a role's inline policies, sorted (IAM
// ListRolePolicies).
func (m *Mock) ListRolePolicies(_ context.Context, roleName string) ([]string, error) {
m.mu.RLock()
defer m.mu.RUnlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return nil, errors.Newf(errors.NotFound, "role %q not found", roleName)
}

names := make([]string, 0, len(rd.inlinePolicies))
for name := range rd.inlinePolicies {
names = append(names, name)
}

sort.Strings(names)

return names, nil
}
63 changes: 63 additions & 0 deletions providers/aws/awsiam/roletags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package awsiam

import (
"context"

"github.com/stackshy/cloudemu/v2/errors"
)

// TagRole adds or overwrites tags on a role (IAM TagRole).
func (m *Mock) TagRole(_ context.Context, roleName string, tags map[string]string) error {
m.mu.Lock()
defer m.mu.Unlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return errors.Newf(errors.NotFound, "role %q not found", roleName)
}

if rd.Tags == nil {
rd.Tags = make(map[string]string, len(tags))
}

for k, v := range tags {
rd.Tags[k] = v
}

return nil
}

// UntagRole removes tags by key from a role (IAM UntagRole).
func (m *Mock) UntagRole(_ context.Context, roleName string, keys []string) error {
m.mu.Lock()
defer m.mu.Unlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return errors.Newf(errors.NotFound, "role %q not found", roleName)
}

for _, k := range keys {
delete(rd.Tags, k)
}

return nil
}

// ListRoleTags returns a role's tags (IAM ListRoleTags).
func (m *Mock) ListRoleTags(_ context.Context, roleName string) (map[string]string, error) {
m.mu.RLock()
defer m.mu.RUnlock()

rd, ok := m.roles.Get(roleName)
if !ok {
return nil, errors.Newf(errors.NotFound, "role %q not found", roleName)
}

out := make(map[string]string, len(rd.Tags))
for k, v := range rd.Tags {
out[k] = v
}

return out, nil
}
27 changes: 27 additions & 0 deletions providers/aws/cloudwatchlogs/cloudwatchlogs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,30 @@ func TestDescribeMetricFilters(t *testing.T) {
require.Error(t, err)
})
}

// TestLogGroupTagging is a regression guard for issue #319: CloudWatch Logs
// TagResource/UntagResource/ListTagsForResource were unimplemented.
func TestLogGroupTagging(t *testing.T) {
m := newTestMock()
ctx := context.Background()

_, err := m.CreateLogGroup(ctx, driver.LogGroupConfig{Name: "g"})
require.NoError(t, err)

require.NoError(t, m.TagLogGroup(ctx, "g", map[string]string{"env": "prod", "team": "ops"}))

tags, err := m.ListLogGroupTags(ctx, "g")
require.NoError(t, err)
assert.Equal(t, "prod", tags["env"])
assert.Equal(t, "ops", tags["team"])

require.NoError(t, m.UntagLogGroup(ctx, "g", []string{"env"}))

tags, err = m.ListLogGroupTags(ctx, "g")
require.NoError(t, err)
_, has := tags["env"]
assert.False(t, has)
assert.Equal(t, "ops", tags["team"])

assert.Error(t, m.TagLogGroup(ctx, "missing", map[string]string{"a": "b"}))
}
55 changes: 55 additions & 0 deletions providers/aws/cloudwatchlogs/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cloudwatchlogs

import (
"context"

"github.com/stackshy/cloudemu/v2/errors"
)

// TagLogGroup adds or overwrites tags on a log group (CloudWatch Logs
// TagResource / TagLogGroup).
func (m *Mock) TagLogGroup(_ context.Context, name string, tags map[string]string) error {
g, ok := m.groups.Get(name)
if !ok {
return errors.Newf(errors.NotFound, "log group %q not found", name)
}

if g.info.Tags == nil {
g.info.Tags = make(map[string]string, len(tags))
}

for k, v := range tags {
g.info.Tags[k] = v
}

return nil
}

// UntagLogGroup removes tags by key from a log group.
func (m *Mock) UntagLogGroup(_ context.Context, name string, keys []string) error {
g, ok := m.groups.Get(name)
if !ok {
return errors.Newf(errors.NotFound, "log group %q not found", name)
}

for _, k := range keys {
delete(g.info.Tags, k)
}

return nil
}

// ListLogGroupTags returns a log group's tags.
func (m *Mock) ListLogGroupTags(_ context.Context, name string) (map[string]string, error) {
g, ok := m.groups.Get(name)
if !ok {
return nil, errors.Newf(errors.NotFound, "log group %q not found", name)
}

out := make(map[string]string, len(g.info.Tags))
for k, v := range g.info.Tags {
out[k] = v
}

return out, nil
}
6 changes: 6 additions & 0 deletions providers/aws/dynamodb/dynamodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ func (m *Mock) matchQueryItems(
}
}

// Apply the FilterExpression (post key-condition), matching real
// DynamoDB: Query filters the key-matched set the same way Scan does.
if !matchesFilters(item, input.Filters) {
continue
}

matched = append(matched, item)
}

Expand Down
12 changes: 10 additions & 2 deletions providers/aws/ec2/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ func (m *Mock) describeCandidates(instanceIDs []string, hidden, includeManaged b
for _, id := range instanceIDs {
inst, ok := m.instances.Get(id)
if !ok {
continue
return nil, cerrors.Newf(cerrors.NotFound, "instance %q not found", id)
}

if hiddenManaged(inst, hidden, includeManaged) {
Expand Down Expand Up @@ -607,8 +607,16 @@ func (m *Mock) DeleteVolume(_ context.Context, id string) error {
return nil
}

// DescribeVolumes returns volumes matching the given IDs.
// DescribeVolumes returns volumes matching the given IDs. An explicit ID that
// does not exist yields InvalidVolume.NotFound, matching real EC2 (an empty
// success would break existence checks and Terraform drift detection).
func (m *Mock) DescribeVolumes(_ context.Context, ids []string) ([]driver.VolumeInfo, error) {
for _, id := range ids {
if !m.volumes.Has(id) {
return nil, cerrors.Newf(cerrors.NotFound, "volume %q not found", id)
}
}

return describeResources(m.volumes, ids), nil
}

Expand Down
8 changes: 4 additions & 4 deletions providers/aws/ec2/ec2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,10 @@ func TestDeleteVolume(t *testing.T) {
err = m.DeleteVolume(ctx, vol.ID)
requireNoError(t, err)

// Should be gone
vols, err := m.DescribeVolumes(ctx, []string{vol.ID})
requireNoError(t, err)
assertEqual(t, 0, len(vols))
// Should be gone: describing the deleted ID now yields NotFound,
// matching real EC2 (InvalidVolume.NotFound) rather than empty success.
_, err = m.DescribeVolumes(ctx, []string{vol.ID})
assertError(t, err, true)
})

t.Run("not found", func(t *testing.T) {
Expand Down
Loading
Loading