Skip to content

Commit

Permalink
init: initial simple version of the janitor
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Case <richard.case@suse.com>
  • Loading branch information
richardcase committed Nov 3, 2023
1 parent b6f206e commit 1ca737e
Show file tree
Hide file tree
Showing 17 changed files with 689 additions and 1 deletion.
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Run CI checks

on: [pull_request, workflow_dispatch]

jobs:
ci:
name: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.21'
check-latest: true
cache: true
- name: Build
run: make build
- name: Test
run: make test
55 changes: 55 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: release

on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

env:
TAG: ${{ github.ref_name }}
REGISTRY: ghcr.io

permissions:
contents: write # Allow to create a release.

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: setupGo
uses: actions/setup-go@v4
with:
go-version: '=1.21.3'
- name: Docker login
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker image
run: make docker-build-all TAG=${{ env.TAG }}
- name: Push docker image
run: make docker-push-all TAG=${{ env.TAG }}
release:
name: Create draft release
needs: build
runs-on: ubuntu-latest
steps:
- name: Set env
run: echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
- name: checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Create draft GH release
uses: softprops/action-gh-release@v1
with:
draft: true
generate_release_notes: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@
# Dependency directories (remove the comment below to include it)
# vendor/

bin/

# Go workspace file
go.work

.vscode/
41 changes: 41 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Build the manager binary
ARG builder_image

# Build architecture
ARG ARCH

FROM ${builder_image} as builder
WORKDIR /workspace

# Run this with docker build --build-arg goproxy=$(go env GOPROXY) to override the goproxy
ARG goproxy=https://proxy.golang.org
# Run this with docker build --build-arg package=./controlplane or --build-arg package=./bootstrap
ENV GOPROXY=$goproxy

# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum

# Cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download

COPY ./ ./

ARG package=.
ARG ARCH
ARG ldflags

RUN --mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} \
go build -trimpath -ldflags "${ldflags} -extldflags '-static'" \
-o aws-janitor .

FROM gcr.io/distroless/static:nonroot-${ARCH}
LABEL org.opencontainers.image.source=https://github.com/rancher-sandbox/aws-janitor
WORKDIR /
COPY --from=builder /workspace/aws-janitor .
# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
USER 65532
ENTRYPOINT ["/aws-janitor"]
77 changes: 77 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

GO_VERSION ?= 1.21.3
GO_CONTAINER_IMAGE ?= docker.io/library/golang:$(GO_VERSION)

# Use GOPROXY environment variable if set
GOPROXY := $(shell go env GOPROXY)
ifeq ($(GOPROXY),)
GOPROXY := https://proxy.golang.org
endif
export GOPROXY

# Active module mode, as we use go modules to manage dependencies
export GO111MODULE=on

# This option is for running docker manifest command
export DOCKER_CLI_EXPERIMENTAL := enabled

TAG ?= dev
ARCH ?= $(shell go env GOARCH)
ALL_ARCH = amd64 arm64 s390x
REGISTRY ?= ghcr.io
ORG ?= rancher-sandbox
ACTION_IMAGE_NAME ?= aws-janitor
ACTION_IMG ?= $(REGISTRY)/$(ORG)/$(ACTION_IMAGE_NAME)
MANIFEST_IMG ?= $(ACTION_IMG)-$(ARCH)

.PHONY: test
test:
go test ./...

.PHONY: build
build:
go build -o bin/aws-janitor main.go

## --------------------------------------
## Docker
## --------------------------------------

.PHONY: docker-push
docker-push: ## Push the docker images
docker push $(MANIFEST_IMG):$(TAG)

.PHONY: docker-push-all
docker-push-all: $(addprefix docker-push-,$(ALL_ARCH)) ## Push all the architecture docker images
$(MAKE) docker-push-manifest-action

docker-push-%:
$(MAKE) ARCH=$* docker-push

.PHONY: docker-push-manifest-action
docker-push-manifest-action: ## Push the multiarch manifest for the actions docker images
## Minimum docker version 18.06.0 is required for creating and pushing manifest images.
docker manifest create --amend $(ACTION_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(ACTION_IMG)\-&:$(TAG)~g")
@for arch in $(ALL_ARCH); do docker manifest annotate --arch $${arch} ${ACTION_IMG}:${TAG} ${ACTION_IMG}-$${arch}:${TAG}; done
docker manifest push --purge $(ACTION_IMG):$(TAG)

.PHONY: docker-pull-prerequisites
docker-pull-prerequisites:
docker pull docker.io/docker/dockerfile:1.4
docker pull $(GO_CONTAINER_IMAGE)
docker pull gcr.io/distroless/static:latest

.PHONY: docker-build-all
docker-build-all: $(addprefix docker-build-,$(ALL_ARCH)) ## Build docker images for all architectures

docker-build-%:
$(MAKE) ARCH=$* docker-build

.PHONY: docker-build
docker-build: docker-pull-prerequisites ## Run docker-build-* targets for all providers
DOCKER_BUILDKIT=1 docker build --build-arg builder_image=$(GO_CONTAINER_IMAGE) --build-arg goproxy=$(GOPROXY) --build-arg ARCH=$(ARCH) --build-arg package=. --build-arg ldflags="$(LDFLAGS)" . -t $(MANIFEST_IMG):$(TAG)

docker-list-all:
@echo $(CONTROLLER_IMG):${TAG}
@for arch in $(ALL_ARCH); do echo $(ACTION_IMG)-$${arch}:${TAG}; done
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# aws-janitor
# AWS Janitor

A GitHub Action to cleanup AWS resources that have exceeded a TTL.

> By default the action will not perform the delete (i.e. it will be a dry-run). You need to explicitly set commit to `true`.
It supports cleaning up the following services:

- EKS Clusters
- Auto Scaling Groups

## Inputs

| Name | Required | Description |
| ----------------- | -------- | -------------------------------------------------------------------------------------- |
| regions | Y | A comma seperated list of regions to clean resources in. You can use * for all regions |
| allow-all-regions | N | Set to true if use * from regions. |
| ttl | Y | The duration that a resource can live for. For example, use 24h for 1 day. |
| commit | N | Whether to perform the delete. Defaults to `false` which is a dry run |

## Example Usage

```yaml
jobs:
cleanup:
runs-on: ubuntu-latest
name: Cleanup resource groups
steps:
- name: Cleanup
uses: rancher-sandbox/aws-janitor@v0.1.0
with:
regions: eu-west-1
ttl: 168h
env:
AWS_ACCESS_KEY_ID: {{secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: {{secrets.AWS_SECRET_ACCESS_KEY}}
```
## Implementation Notes
It currently assumes that an instance of a service will have some form of creation date. This means that the implementation can be simpler as it doesn't need to adopt a "mark & sweep" pattern that requires saving state between runs of the action.
24 changes: 24 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: 'AWS Janitor'
author: 'Rancher Sandbox'
description: 'Clean-up AWS resources based on a TTL.'
inputs:
regions:
description: 'A comma separated list of regions to clean resources in. You can use * for all regions.'
required: true
allow-all-regions:
description: 'Set to true if you want to allow cleaning resources in all regions. If true then * must be used for regions.'
required: false
default: 'false'
ttl:
description: 'The duration that a resource can live for. For example, use 24h for 1 day.'
required: true
commit:
description: 'Should the action just report or do the actual delete.'
required: false
default: 'false'
runs:
using: 'docker'
image: 'docker://ghcr.io/rancher-sandbox/aws-janitor:v0.1.0'
branding:
icon: 'delete'
color: 'blue'
85 changes: 85 additions & 0 deletions action/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package action

import (
"context"
"fmt"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/eks"
)

type AwsJanitorAction interface {
Cleanup(ctx context.Context, input *Input) error
}

func New(commit bool) AwsJanitorAction {
return &action{
commit: commit,
}
}

type action struct {
commit bool
}

func (a *action) Cleanup(ctx context.Context, input *Input) error {

//NOTE: ordering matters here!
cleanupFuncs := map[string]CleanupFunc{
eks.ServiceName: a.cleanEKSClusters,
autoscaling.ServiceName: a.cleanASGs,
}
inputRegions := strings.Split(input.Regions, ",")

for service, cleanupFunc := range cleanupFuncs {
regions := getServiceRegions(service, inputRegions)

for _, region := range regions {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region)},
)
if err != nil {
return fmt.Errorf("failed to create aws session for region %s: %w", region, err)
}

scope := &CleanupScope{
TTL: input.TTL,
Session: sess,
Commit: input.Commit,
}

Log("Cleaning up resources for service %s in region %s", service, region)
if err := cleanupFunc(ctx, scope); err != nil {
return fmt.Errorf("failed running cleanup for service %s: %w", service, err)
}
}
}

return nil
}

func getServiceRegions(service string, inputRegions []string) []string {
regions := []string{}
allRegions := inputRegions[0] == "*"

sr, exists := endpoints.RegionsForService(endpoints.DefaultPartitions(), endpoints.AwsPartitionID, service)
if exists {
for _, region := range sr {
if allRegions {
regions = append(regions, region.ID())
} else {
for _, r := range inputRegions {
if r == region.ID() {
regions = append(regions, region.ID())
}
}
}
}
}

return regions
}
16 changes: 16 additions & 0 deletions action/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package action

import (
"context"
"time"

"github.com/aws/aws-sdk-go/aws/session"
)

type CleanupScope struct {
Session *session.Session
TTL time.Duration
Commit bool
}

type CleanupFunc func(ctx context.Context, input *CleanupScope) error
Loading

0 comments on commit 1ca737e

Please sign in to comment.