-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from cheelim1/feat/add-cloudwatch-alarm-manager
Feat: Add first implementation Cloudwatch Alarm Manager
- Loading branch information
Showing
8 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* @cheelim1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / Build and Push to GitHub PackagesThe 'as' keyword should match the case of the 'from' keyword
|
||
|
||
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |