Skip to content

Commit

Permalink
Merge pull request #1 from cheelim1/feat/add-cloudwatch-alarm-manager
Browse files Browse the repository at this point in the history
Feat: Add first implementation Cloudwatch Alarm Manager
  • Loading branch information
cheelim1 authored Nov 28, 2024
2 parents 825dc4b + 9343011 commit 783f315
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @cheelim1
37 changes: 37 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Build

on:
push:
branches:
- main

jobs:
build:
name: Build and Push to GitHub Packages
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Build and push Docker image to GitHub Packages
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Output image details
run: |
echo "Docker image pushed to: ghcr.io/${{ github.repository }}:${{ github.sha }}"
21 changes: 21 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Test

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
container:
image: golang:1.21.4-alpine
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Run Tests
run: go test ./...
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM golang:1.21-alpine as builder

Check warning on line 1 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build and Push to GitHub Packages

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /bin/cw-alarm-manager main.go

FROM alpine:latest

RUN apk --no-cache add ca-certificates

RUN adduser -D appuser
USER appuser

WORKDIR /app

COPY --from=builder /bin/cw-alarm-manager /app/

CMD ["./cw-alarm-manager"]
28 changes: 28 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module github.com/cheelim1/cw-alarm-manager

go 1.21.4

require (
github.com/aws/aws-sdk-go-v2 v1.32.2
github.com/aws/aws-sdk-go-v2/config v1.27.43
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.42.2
github.com/stretchr/testify v1.9.0
)

require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
46 changes: 46 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/aws-sdk-go-v2/config v1.27.43 h1:p33fDDihFC390dhhuv8nOmX419wjOSDQRb+USt20RrU=
github.com/aws/aws-sdk-go-v2/config v1.27.43/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.42.2 h1:eMh+iBTF1CbpHMfiRvIaVm+rzrH1DOzuSFaR55O+bBo=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.42.2/go.mod h1:/A4zNqF1+RS5RV+NNLKIzUX1KtK5SoWgf/OpiqrwmBo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90 changes: 90 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"context"
"errors"
"log/slog"
"os"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
)

const (
maxRetries = 3
initialBackoff = time.Second // 1 second
)

// CloudWatchAPI defines the interface that will be implemented by both the real client and the mock client.
type CloudWatchAPI interface {
SetAlarmState(ctx context.Context, input *cloudwatch.SetAlarmStateInput, opts ...func(*cloudwatch.Options)) (*cloudwatch.SetAlarmStateOutput, error)
}

func main() {
// Setup structured logging with log/slog
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

alarmName := os.Getenv("CLOUDWATCH_ALARM_NAME")
alarmState := os.Getenv("ALARM_STATE")
alarmReason := os.Getenv("ALARM_REASON")

if alarmName == "" || alarmState == "" || alarmReason == "" {
logger.Error("Missing required environment variables",
slog.String("alarmName", alarmName),
slog.String("alarmState", alarmState),
slog.String("alarmReason", alarmReason))
os.Exit(1)
}

// Load AWS configuration
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
logger.Error("Failed to load AWS config", slog.String("error", err.Error()))
os.Exit(1)
}

client := cloudwatch.NewFromConfig(cfg)

// Set CloudWatch alarm state with retry logic
err = setAlarmStateWithRetry(client, alarmName, alarmState, alarmReason, logger)
if err != nil {
logger.Error("Failed to set CloudWatch alarm state after retries", slog.String("error", err.Error()))
os.Exit(1)
}

logger.Info("Successfully set CloudWatch alarm state", slog.String("AlarmName", alarmName), slog.String("AlarmState", alarmState))
}

func setAlarmStateWithRetry(client CloudWatchAPI, alarmName, alarmState, alarmReason string, logger *slog.Logger) error {
var attempt int
var backoff = initialBackoff

for attempt = 1; attempt <= maxRetries; attempt++ {
err := setAlarmState(client, alarmName, alarmState, alarmReason)
if err == nil {
return nil
}

logger.Error("Failed to set CloudWatch alarm state", slog.Int("attempt", attempt), slog.String("error", err.Error()))

// Exponential backoff before retrying
time.Sleep(backoff)
backoff *= 2
}

return errors.New("exceeded maximum retries")
}

func setAlarmState(client CloudWatchAPI, alarmName, alarmState, alarmReason string) error {
input := &cloudwatch.SetAlarmStateInput{
AlarmName: aws.String(alarmName),
StateValue: types.StateValue(alarmState),
StateReason: aws.String(alarmReason),
}

_, err := client.SetAlarmState(context.TODO(), input)
return err
}
39 changes: 39 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"context"
"errors"
"os"
"testing"

"log/slog"

"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/stretchr/testify/assert"
)

// MockCloudWatchClient implements the CloudWatchAPI interface to mock the SetAlarmState method.
type MockCloudWatchClient struct {
err error
}

func (m *MockCloudWatchClient) SetAlarmState(ctx context.Context, input *cloudwatch.SetAlarmStateInput, opts ...func(*cloudwatch.Options)) (*cloudwatch.SetAlarmStateOutput, error) {
return nil, m.err
}

func TestSetAlarmStateWithRetry_Success(t *testing.T) {
mockClient := &MockCloudWatchClient{err: nil}
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

err := setAlarmStateWithRetry(mockClient, "TestAlarm", "OK", "Test reason", logger)
assert.NoError(t, err)
}

func TestSetAlarmStateWithRetry_Failure(t *testing.T) {
mockClient := &MockCloudWatchClient{err: errors.New("API error")}
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

err := setAlarmStateWithRetry(mockClient, "TestAlarm", "OK", "Test reason", logger)
assert.Error(t, err)
assert.Equal(t, "exceeded maximum retries", err.Error())
}

0 comments on commit 783f315

Please sign in to comment.