Skip to content

Commit 83d440e

Browse files
authored
Merge pull request #33 from solinode/console-ec2-cw-events-sfn
Console: EC2 key pairs, snapshots, VPC routing, Logs Insights, alarms, EventBridge buses and Scheduler, Step Functions
2 parents 40699d0 + d3e8909 commit 83d440e

56 files changed

Lines changed: 3634 additions & 178 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎cli/internal/httpx/httpx.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ func (c *Ctx) Authorize(action, resource string) error {
219219
return core.Errf(http.StatusForbidden, "AccessDenied", "%s is not authorized to perform %s on %s", c.P.ARN, action, resource)
220220
}
221221

222+
// Check is Authorize; it exists so a Ctx can stand in for an AWS request where handlers share logic.
223+
func (c *Ctx) Check(action, resource string) error { return c.Authorize(action, resource) }
224+
222225
// MarkWritten tells the router the handler wrote its own response.
223226
func (c *Ctx) MarkWritten() { c.written = true }
224227

‎cli/internal/svc/cloudwatch/logs_aws.go‎

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ func (s *Service) groupARN(name string) string { return s.env.ARN("logs", "log-g
7373

7474
// authGroup authorizes an action on a log group. Policies usually name groups
7575
// as "log-group:NAME:*", so that form is tried first.
76-
func (s *Service) authGroup(q *awsapi.Req, action, group string) error {
76+
// authorizer is satisfied by *awsapi.Req and *httpx.Ctx, so the AWS and native
77+
// handlers share their logic.
78+
type authorizer interface {
79+
Authorize(action, resource string) error
80+
Check(action, resource string) error
81+
}
82+
83+
func (s *Service) authGroup(q authorizer, action, group string) error {
7784
arn := s.groupARN(group)
78-
if q.P != nil && q.P.Can(action, arn+":*") {
85+
if q.Check(action, arn+":*") == nil {
7986
return q.Authorize(action, arn+":*")
8087
}
8188
return q.Authorize(action, arn)
@@ -969,20 +976,26 @@ func (r *queryRegistry) get(id string) *insightsRun {
969976
return r.runs[id]
970977
}
971978

979+
type startQueryIn struct {
980+
QueryLanguage string `json:"queryLanguage"`
981+
LogGroupName string `json:"logGroupName"`
982+
LogGroupNames []string `json:"logGroupNames"`
983+
LogGroupIdentifiers []string `json:"logGroupIdentifiers"`
984+
StartTime int64 `json:"startTime"`
985+
EndTime int64 `json:"endTime"`
986+
QueryString string `json:"queryString"`
987+
Limit int `json:"limit"`
988+
}
989+
972990
func (s *Service) awsStartQuery(q *awsapi.Req) (any, error) {
973-
var in struct {
974-
QueryLanguage string `json:"queryLanguage"`
975-
LogGroupName string `json:"logGroupName"`
976-
LogGroupNames []string `json:"logGroupNames"`
977-
LogGroupIdentifiers []string `json:"logGroupIdentifiers"`
978-
StartTime int64 `json:"startTime"`
979-
EndTime int64 `json:"endTime"`
980-
QueryString string `json:"queryString"`
981-
Limit int `json:"limit"`
982-
}
991+
var in startQueryIn
983992
if err := q.Bind(&in); err != nil {
984993
return nil, err
985994
}
995+
return s.startQuery(q, in)
996+
}
997+
998+
func (s *Service) startQuery(q authorizer, in startQueryIn) (any, error) {
986999
var names []string
9871000
if in.LogGroupName != "" {
9881001
names = append(names, in.LogGroupName)
@@ -1057,9 +1070,13 @@ func (s *Service) awsGetQueryResults(q *awsapi.Req) (any, error) {
10571070
if err := q.Bind(&in); err != nil {
10581071
return nil, err
10591072
}
1060-
run := queries.get(in.QueryID)
1073+
return s.queryResults(q, in.QueryID)
1074+
}
1075+
1076+
func (s *Service) queryResults(q authorizer, id string) (any, error) {
1077+
run := queries.get(id)
10611078
if run == nil {
1062-
return nil, logsErr("ResourceNotFoundException", "query %s does not exist", in.QueryID)
1079+
return nil, logsErr("ResourceNotFoundException", "query %s does not exist", id)
10631080
}
10641081
if err := s.authGroup(q, "logs:GetQueryResults", run.Group); err != nil {
10651082
return nil, err

‎cli/internal/svc/cloudwatch/logs_filters.go‎

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,22 @@ func (s *Service) updateFilters(group string, fn func(f *groupFilters) error) er
202202

203203
// ---- AWS operations ----
204204

205+
type putMetricFilterIn struct {
206+
LogGroupName string `json:"logGroupName"`
207+
FilterName string `json:"filterName"`
208+
FilterPattern string `json:"filterPattern"`
209+
MetricTransformations []MetricTransformation `json:"metricTransformations"`
210+
}
211+
205212
func (s *Service) awsPutMetricFilter(q *awsapi.Req) (any, error) {
206-
var in struct {
207-
LogGroupName string `json:"logGroupName"`
208-
FilterName string `json:"filterName"`
209-
FilterPattern string `json:"filterPattern"`
210-
MetricTransformations []MetricTransformation `json:"metricTransformations"`
211-
}
213+
var in putMetricFilterIn
212214
if err := q.Bind(&in); err != nil {
213215
return nil, err
214216
}
217+
return s.putMetricFilter(q, in)
218+
}
219+
220+
func (s *Service) putMetricFilter(q authorizer, in putMetricFilterIn) (any, error) {
215221
if err := s.authGroup(q, "logs:PutMetricFilter", in.LogGroupName); err != nil {
216222
return nil, err
217223
}
@@ -309,14 +315,20 @@ func (s *Service) awsDescribeMetricFilters(q *awsapi.Req) (any, error) {
309315
return map[string]any{"metricFilters": out}, nil
310316
}
311317

318+
type deleteFilterIn struct {
319+
LogGroupName string `json:"logGroupName"`
320+
FilterName string `json:"filterName"`
321+
}
322+
312323
func (s *Service) awsDeleteMetricFilter(q *awsapi.Req) (any, error) {
313-
var in struct {
314-
LogGroupName string `json:"logGroupName"`
315-
FilterName string `json:"filterName"`
316-
}
324+
var in deleteFilterIn
317325
if err := q.Bind(&in); err != nil {
318326
return nil, err
319327
}
328+
return s.deleteMetricFilter(q, in)
329+
}
330+
331+
func (s *Service) deleteMetricFilter(q authorizer, in deleteFilterIn) (any, error) {
320332
if err := s.authGroup(q, "logs:DeleteMetricFilter", in.LogGroupName); err != nil {
321333
return nil, err
322334
}
@@ -358,18 +370,24 @@ func (s *Service) awsTestMetricFilter(q *awsapi.Req) (any, error) {
358370
return map[string]any{"matches": matches}, nil
359371
}
360372

373+
type putSubscriptionFilterIn struct {
374+
LogGroupName string `json:"logGroupName"`
375+
FilterName string `json:"filterName"`
376+
FilterPattern string `json:"filterPattern"`
377+
DestinationArn string `json:"destinationArn"`
378+
RoleArn string `json:"roleArn"`
379+
Distribution string `json:"distribution"`
380+
}
381+
361382
func (s *Service) awsPutSubscriptionFilter(q *awsapi.Req) (any, error) {
362-
var in struct {
363-
LogGroupName string `json:"logGroupName"`
364-
FilterName string `json:"filterName"`
365-
FilterPattern string `json:"filterPattern"`
366-
DestinationArn string `json:"destinationArn"`
367-
RoleArn string `json:"roleArn"`
368-
Distribution string `json:"distribution"`
369-
}
383+
var in putSubscriptionFilterIn
370384
if err := q.Bind(&in); err != nil {
371385
return nil, err
372386
}
387+
return s.putSubscriptionFilter(q, in)
388+
}
389+
390+
func (s *Service) putSubscriptionFilter(q authorizer, in putSubscriptionFilterIn) (any, error) {
373391
if !strings.HasPrefix(core.CanonicalARN(in.DestinationArn), "arn:"+core.Partition+":lambda:") || !strings.Contains(in.DestinationArn, ":function:") {
374392
return nil, logsErr("InvalidParameterException", "HomeCloud subscription filters deliver to Lambda functions (destinationArn must be a function ARN)")
375393
}
@@ -431,13 +449,14 @@ func (s *Service) awsDescribeSubscriptionFilters(q *awsapi.Req) (any, error) {
431449
}
432450

433451
func (s *Service) awsDeleteSubscriptionFilter(q *awsapi.Req) (any, error) {
434-
var in struct {
435-
LogGroupName string `json:"logGroupName"`
436-
FilterName string `json:"filterName"`
437-
}
452+
var in deleteFilterIn
438453
if err := q.Bind(&in); err != nil {
439454
return nil, err
440455
}
456+
return s.deleteSubscriptionFilter(q, in)
457+
}
458+
459+
func (s *Service) deleteSubscriptionFilter(q authorizer, in deleteFilterIn) (any, error) {
441460
if err := s.authGroup(q, "logs:DeleteSubscriptionFilter", in.LogGroupName); err != nil {
442461
return nil, err
443462
}

‎cli/internal/svc/cloudwatch/metrics.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ func (s *Service) Routes(r *httpx.Router) {
470470
r.Handle("POST /api/v1/cloudwatch/metrics/query", "cloudwatch:GetMetricStatistics", s.query)
471471
s.alarmRoutes(r)
472472
s.logRoutes(r)
473+
s.nativeLogRoutes(r)
473474
}
474475

475476
func (s *Service) listMetrics(c *httpx.Ctx) (any, error) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package cloudwatch
2+
3+
import (
4+
"errors"
5+
"github.com/homecloudhq/homecloud/cli/internal/store"
6+
"slices"
7+
8+
"github.com/homecloudhq/homecloud/cli/internal/awsapi"
9+
"github.com/homecloudhq/homecloud/cli/internal/core"
10+
"github.com/homecloudhq/homecloud/cli/internal/httpx"
11+
)
12+
13+
// Native (console) routes for Logs Insights, metric and subscription filters
14+
// and alarm history. They share their logic with the AWS handlers.
15+
16+
// nat converts an AWS API error to a native one.
17+
func nat[T any](v T, err error) (T, error) {
18+
var ae *awsapi.Error
19+
if errors.As(err, &ae) {
20+
st := ae.Status
21+
if ae.Code == "ResourceNotFoundException" {
22+
st = 404
23+
}
24+
return v, core.Errf(st, ae.Code, "%s", ae.Message)
25+
}
26+
return v, err
27+
}
28+
29+
func (s *Service) nativeLogRoutes(r *httpx.Router) {
30+
res := httpx.Res("arn:aws:logs:{region}:{account}:log-group:{group}")
31+
r.Handle("POST /api/v1/logs/insights/queries", "logs:StartQuery", s.nStartQuery, httpx.Deferred())
32+
r.Handle("GET /api/v1/logs/insights/queries/{id}", "logs:GetQueryResults", s.nQueryResults, httpx.Deferred())
33+
r.Handle("GET /api/v1/logs/groups/{group}/metric-filters", "logs:DescribeMetricFilters", s.nListMetricFilters, res)
34+
r.Handle("PUT /api/v1/logs/groups/{group}/metric-filters/{name}", "logs:PutMetricFilter", s.nPutMetricFilter, res)
35+
r.Handle("DELETE /api/v1/logs/groups/{group}/metric-filters/{name}", "logs:DeleteMetricFilter", s.nDeleteMetricFilter, res)
36+
r.Handle("GET /api/v1/logs/groups/{group}/subscription-filters", "logs:DescribeSubscriptionFilters", s.nListSubFilters, res)
37+
r.Handle("PUT /api/v1/logs/groups/{group}/subscription-filters/{name}", "logs:PutSubscriptionFilter", s.nPutSubFilter, res)
38+
r.Handle("DELETE /api/v1/logs/groups/{group}/subscription-filters/{name}", "logs:DeleteSubscriptionFilter", s.nDeleteSubFilter, res)
39+
r.Handle("GET /api/v1/cloudwatch/alarms/{name}/history", "cloudwatch:DescribeAlarmHistory", s.nAlarmHistory,
40+
httpx.Res("arn:aws:cloudwatch:{region}:{account}:alarm:{name}"))
41+
}
42+
43+
func (s *Service) nStartQuery(c *httpx.Ctx) (any, error) {
44+
var in startQueryIn
45+
if err := c.Bind(&in); err != nil {
46+
return nil, err
47+
}
48+
return nat(s.startQuery(c, in))
49+
}
50+
51+
func (s *Service) nQueryResults(c *httpx.Ctx) (any, error) {
52+
return nat(s.queryResults(c, c.Param("id")))
53+
}
54+
55+
func (s *Service) nListMetricFilters(c *httpx.Ctx) (any, error) {
56+
if _, err := nat(s.storedGroup(c.Param("group"))); err != nil {
57+
return nil, err
58+
}
59+
out := slices.Clone(s.filters(c.Param("group")).Metric)
60+
if out == nil {
61+
out = []MetricFilter{}
62+
}
63+
return out, nil
64+
}
65+
66+
func (s *Service) nPutMetricFilter(c *httpx.Ctx) (any, error) {
67+
var in putMetricFilterIn
68+
if err := c.Bind(&in); err != nil {
69+
return nil, err
70+
}
71+
in.LogGroupName, in.FilterName = c.Param("group"), c.Param("name")
72+
return nat(s.putMetricFilter(c, in))
73+
}
74+
75+
func (s *Service) nDeleteMetricFilter(c *httpx.Ctx) (any, error) {
76+
return nat(s.deleteMetricFilter(c, deleteFilterIn{LogGroupName: c.Param("group"), FilterName: c.Param("name")}))
77+
}
78+
79+
func (s *Service) nListSubFilters(c *httpx.Ctx) (any, error) {
80+
if _, err := nat(s.storedGroup(c.Param("group"))); err != nil {
81+
return nil, err
82+
}
83+
out := slices.Clone(s.filters(c.Param("group")).Subscription)
84+
if out == nil {
85+
out = []SubscriptionFilter{}
86+
}
87+
return out, nil
88+
}
89+
90+
func (s *Service) nPutSubFilter(c *httpx.Ctx) (any, error) {
91+
var in putSubscriptionFilterIn
92+
if err := c.Bind(&in); err != nil {
93+
return nil, err
94+
}
95+
in.LogGroupName, in.FilterName = c.Param("group"), c.Param("name")
96+
return nat(s.putSubscriptionFilter(c, in))
97+
}
98+
99+
func (s *Service) nDeleteSubFilter(c *httpx.Ctx) (any, error) {
100+
return nat(s.deleteSubscriptionFilter(c, deleteFilterIn{LogGroupName: c.Param("group"), FilterName: c.Param("name")}))
101+
}
102+
103+
func (s *Service) nAlarmHistory(c *httpx.Ctx) (any, error) {
104+
if !store.Has(s.env.Store, cAlarms, c.Param("name")) {
105+
return nil, core.NotFound("alarm", c.Param("name"))
106+
}
107+
out := slices.Clone(s.history(c.Param("name")))
108+
slices.SortStableFunc(out, func(a, b AlarmHistoryItem) int { return b.Timestamp.Compare(a.Timestamp) })
109+
if out == nil {
110+
out = []AlarmHistoryItem{}
111+
}
112+
return out, nil
113+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package cloudwatch
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestNativeLogs(t *testing.T) {
10+
h, _ := newAWS(t)
11+
h.AWS(t, "logs", "create-log-group", "--log-group-name", "web")
12+
h.AWS(t, "logs", "create-log-stream", "--log-group-name", "web", "--log-stream-name", "s")
13+
now := time.Now().UnixMilli()
14+
batch, _ := json.Marshal([]map[string]any{{"timestamp": now, "message": "hello"}, {"timestamp": now, "message": "world"}})
15+
h.AWS(t, "logs", "put-log-events", "--log-group-name", "web", "--log-stream-name", "s", "--log-events", string(batch))
16+
17+
h.Native(t, "PUT", "/api/v1/logs/groups/web/metric-filters/f1", map[string]any{"filterPattern": "hello",
18+
"metricTransformations": []map[string]any{{"metricName": "Hello", "metricNamespace": "Qa", "metricValue": "1"}}})
19+
var mf []map[string]any
20+
_ = json.Unmarshal(h.Native(t, "GET", "/api/v1/logs/groups/web/metric-filters", nil), &mf)
21+
if len(mf) != 1 {
22+
t.Fatalf("metric filters %v", mf)
23+
}
24+
h.Native(t, "DELETE", "/api/v1/logs/groups/web/metric-filters/f1", nil)
25+
26+
var st map[string]string
27+
_ = json.Unmarshal(h.Native(t, "POST", "/api/v1/logs/insights/queries", map[string]any{"logGroupNames": []string{"web"},
28+
"startTime": time.Now().Add(-time.Hour).Unix(), "endTime": time.Now().Add(time.Hour).Unix(), "queryString": "fields @message | limit 5"}), &st)
29+
var res struct {
30+
Status string
31+
Results [][]map[string]string
32+
}
33+
_ = json.Unmarshal(h.Native(t, "GET", "/api/v1/logs/insights/queries/"+st["queryId"], nil), &res)
34+
if res.Status != "Complete" || len(res.Results) != 2 {
35+
t.Fatalf("query %+v", res)
36+
}
37+
var hist []map[string]any
38+
h.AWS(t, "cloudwatch", "put-metric-alarm", "--alarm-name", "a1", "--metric-name", "M", "--namespace", "Qa", "--statistic", "Sum",
39+
"--period", "60", "--evaluation-periods", "1", "--threshold", "1", "--comparison-operator", "GreaterThanThreshold")
40+
_ = json.Unmarshal(h.Native(t, "GET", "/api/v1/cloudwatch/alarms/a1/history", nil), &hist)
41+
if len(hist) == 0 {
42+
t.Fatal("no alarm history")
43+
}
44+
}

0 commit comments

Comments
 (0)