diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9ebe8c..85727a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: version=$(git tag | sort --version-sort | tail -1) version_without_v=$(echo $version | sed -r 's/v(.+)/\1/') file="cmd/version.go" - if [ "v$(grep '// ci-version-check' $file | sed -r 's/.+return\s"(.+)".+/\1/')" != "$version" ] ; then + if [ "v$(grep '// ci-version-check' $file | sed -r 's/.+return "(.+)" .+/\1/')" != "$version" ] ; then echo "Tag version do not match application version in $file" exit 1 fi @@ -58,13 +58,13 @@ jobs: version: latest args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get smart tag id: prepare uses: Surgo/docker-smart-tag-action@v1 with: - docker_image: qoveryrd/pleco + docker_image: swtrasformer/pleco - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 diff --git a/charts/pleco/Chart.yaml b/charts/pleco/Chart.yaml index d03bd35..6bcbf5e 100644 --- a/charts/pleco/Chart.yaml +++ b/charts/pleco/Chart.yaml @@ -3,6 +3,6 @@ name: pleco description: Automatically removes Cloud managed services and Kubernetes resources based on tags with TTL type: application home: https://github.com/Qovery/pleco -version: 0.9.36 -appVersion: 0.9.36 +version: 0.12.0 +appVersion: 0.12.0 icon: https://github.com/Qovery/pleco/raw/main/assets/pleco_logo.png diff --git a/charts/pleco/templates/deployment.yaml b/charts/pleco/templates/deployment.yaml index af036ea..7c4c819 100644 --- a/charts/pleco/templates/deployment.yaml +++ b/charts/pleco/templates/deployment.yaml @@ -46,7 +46,7 @@ spec: {{ end }} {{ if .Values.enabledFeatures.kubernetes }} - --kube-conn - - {{ .Values.enabledFeatures.kubernetes }} + - {{ .Values.enabledFeatures.kubernetes | quote }} {{ end }} {{ if eq .Values.enabledFeatures.s3 true }} - --enable-s3 @@ -94,6 +94,18 @@ spec: {{ if or (eq .Values.awsFeatures.ecr true)}} - --enable-ecr {{ end }} + {{ if or (eq .Values.awsFeatures.sfn true)}} + - --enable-sfn + {{ end }} + {{ if or (eq .Values.awsFeatures.sqs true)}} + - --enable-sqs + {{ end }} + {{ if or (eq .Values.awsFeatures.lambda true)}} + - --enable-lambda + {{ end }} + {{ if or (eq .Values.awsFeatures.cloudformation true)}} + - --enable-cloudformation + {{ end }} {{- end }} # Scaleway features diff --git a/charts/pleco/values-aws.yaml b/charts/pleco/values-aws.yaml index 089e5ee..a8c8f42 100644 --- a/charts/pleco/values-aws.yaml +++ b/charts/pleco/values-aws.yaml @@ -44,6 +44,7 @@ awsFeatures: iam: true sshKeys: true ecr: true + cloudformation: true resources: limits: diff --git a/charts/pleco/values.yaml b/charts/pleco/values.yaml index c247e6e..fe72d4a 100644 --- a/charts/pleco/values.yaml +++ b/charts/pleco/values.yaml @@ -3,7 +3,7 @@ replicaCount: 1 image: repository: qoveryrd/pleco pullPolicy: IfNotPresent - plecoImageTag: "0.9.36" + plecoImageTag: "0.12.0" cloudProvider: "" diff --git a/cmd/version.go b/cmd/version.go index 021d0da..1e87c10 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -19,5 +19,5 @@ func init() { } func GetCurrentVersion() string { - return "0.9.36" // ci-version-check + return "0.12.0" // ci-version-check } diff --git a/pkg/aws/cloudformation_stack.go b/pkg/aws/cloudformation_stack.go new file mode 100644 index 0000000..7289636 --- /dev/null +++ b/pkg/aws/cloudformation_stack.go @@ -0,0 +1,114 @@ +package aws + +import ( + "time" + + "github.com/Qovery/pleco/pkg/common" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" + log "github.com/sirupsen/logrus" +) + +type CloudformationStack struct { + StackName string + CreateTime time.Time + TTL int64 + IsProtected bool +} + +func CloudformationSession(sess session.Session, region string) *cloudformation.CloudFormation { + return cloudformation.New(&sess, &aws.Config{Region: aws.String(region)}) +} + +func listTaggedStacks(svc cloudformation.CloudFormation, tagName string) ([]CloudformationStack, error) { + var taggedStacks []CloudformationStack + + result, err := svc.ListStacks(nil) + if err != nil { + return nil, err + } + + if len(result.StackSummaries) == 0 { + return nil, nil + } + + for _, stack := range result.StackSummaries { + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(*stack.StackName), + } + + stackDescriptionList, err := svc.DescribeStacks(describeStacksInput) + + if err != nil { + continue + } + stackDescription := stackDescriptionList.Stacks[0] + + essentialTags := common.GetEssentialTags(stackDescription.Tags, tagName) + + taggedStacks = append(taggedStacks, CloudformationStack{ + StackName: *stack.StackName, + CreateTime: *stack.CreationTime, + TTL: essentialTags.TTL, + IsProtected: essentialTags.IsProtected, + }) + + } + + return taggedStacks, nil +} + +func deleteStack(svc cloudformation.CloudFormation, stack CloudformationStack) error { + + log.Infof("Deleting CloudFormation Stack %s in %s, expired after %d seconds", + stack.StackName, *svc.Config.Region, stack.TTL) + + _, err := svc.DeleteStack(&cloudformation.DeleteStackInput{ + StackName: &stack.StackName, + }, + ) + if err != nil { + return err + } + + return nil +} + +func getExpiredStacks(ECsession *cloudformation.CloudFormation, tagName string) ([]CloudformationStack, string) { + stacks, err := listTaggedStacks(*ECsession, tagName) + region := *ECsession.Config.Region + if err != nil { + log.Errorf("can't list CloudFormation Stacks in region %s: %s", region, err.Error()) + } + + var expiredStacks []CloudformationStack + for _, stack := range stacks { + if common.CheckIfExpired(stack.CreateTime, stack.TTL, "cloudformation: "+stack.StackName) && !stack.IsProtected { + expiredStacks = append(expiredStacks, stack) + } + } + + return expiredStacks, region +} + +func DeleteExpiredStacks(sessions AWSSessions, options AwsOptions) { + expiredStacks, region := getExpiredStacks(sessions.CloudFormation, options.TagName) + + count, start := common.ElemToDeleteFormattedInfos("expired CloudFormation Stacks", len(expiredStacks), region) + + log.Debug(count) + + if options.DryRun || len(expiredStacks) == 0 { + return + } + + log.Debug(start) + + for _, stack := range expiredStacks { + deletionErr := deleteStack(*sessions.CloudFormation, stack) + if deletionErr != nil { + log.Errorf("Deletion CloudFormation Stack error %s/%s: %s", stack.StackName, region, deletionErr.Error()) + } + } +} diff --git a/pkg/aws/run.go b/pkg/aws/run.go index 748f534..c5224a7 100644 --- a/pkg/aws/run.go +++ b/pkg/aws/run.go @@ -15,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/sfn" "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -41,6 +42,7 @@ type AwsOptions struct { EnableSQS bool EnableLambda bool EnableSFN bool + EnableCloudFormation bool } type AWSSessions struct { @@ -57,6 +59,7 @@ type AWSSessions struct { SQS *sqs.SQS LambdaFunction *lambda.Lambda SFN *sfn.SFN + CloudFormation *cloudformation.CloudFormation } type funcDeleteExpired func(sessions AWSSessions, options AwsOptions) @@ -184,6 +187,12 @@ func runPlecoInRegion(region string, interval int64, wg *sync.WaitGroup, options listServiceToCheckStatus = append(listServiceToCheckStatus, DeleteExpiredStateMachines) } + // Cloudformation Stacks + if options.EnableCloudFormation { + sessions.CloudFormation = cloudformation.New(currentSession) + listServiceToCheckStatus = append(listServiceToCheckStatus, DeleteExpiredStacks) + } + for { for _, check := range listServiceToCheckStatus { check(sessions, options) diff --git a/pkg/aws/statemachine_root.go b/pkg/aws/statemachine_root.go index 169517e..679809b 100644 --- a/pkg/aws/statemachine_root.go +++ b/pkg/aws/statemachine_root.go @@ -94,7 +94,7 @@ func getExpiredMachines(ECsession *sfn.SFN, tagName string) ([]stateMachine, str func DeleteExpiredStateMachines(sessions AWSSessions, options AwsOptions) { expiredMachines, region := getExpiredMachines(sessions.SFN, options.TagName) - count, start := common.ElemToDeleteFormattedInfos("expired Lambda Function", len(expiredMachines), region) + count, start := common.ElemToDeleteFormattedInfos("expired Step Function", len(expiredMachines), region) log.Debug(count) diff --git a/pkg/common/flags.go b/pkg/common/flags.go index 93ce31b..2fbab73 100644 --- a/pkg/common/flags.go +++ b/pkg/common/flags.go @@ -31,6 +31,7 @@ func initAWSFlags(startCmd *cobra.Command) { startCmd.Flags().BoolP("enable-sqs", "q", false, "Enable SQS watch") startCmd.Flags().BoolP("enable-lambda", "f", false, "Enable Lambda Function watch") startCmd.Flags().BoolP("enable-sfn", "x", false, "Enable Lambda Function watch") + startCmd.Flags().BoolP("enable-cloudformation", "d", false, "Enable Cloudformation watch") } func initScalewayFlags(startCmd *cobra.Command) { diff --git a/pkg/common/utils.go b/pkg/common/utils.go index 5f9dc7a..0dc828f 100644 --- a/pkg/common/utils.go +++ b/pkg/common/utils.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/sfn" + "github.com/aws/aws-sdk-go/service/cloudformation" log "github.com/sirupsen/logrus" "strconv" "strings" @@ -73,6 +74,10 @@ func GetEssentialTags(tagsInput interface{}, tagName string) EssentialTags { for _, elem := range typedTags { tags = append(tags, MyTag{Key: *elem.Key, Value: *elem.Value}) } + case []*cloudformation.Tag: + for _, elem := range typedTags { + tags = append(tags, MyTag{Key: *elem.Key, Value: *elem.Value}) + } case []*Tag: for _, elem := range typedTags { tags = append(tags, MyTag{Key: *elem.Key, Value: *elem.Value}) diff --git a/pkg/daemon.go b/pkg/daemon.go index f29e992..e6908a0 100644 --- a/pkg/daemon.go +++ b/pkg/daemon.go @@ -67,6 +67,7 @@ func startAWS(cmd *cobra.Command, interval int64, dryRun bool, wg *sync.WaitGrou EnableSQS: getCmdBool(cmd, "enable-sqs"), EnableLambda: getCmdBool(cmd, "enable-lambda"), EnableSFN: getCmdBool(cmd, "enable-sfn"), + EnableCloudFormation: getCmdBool(cmd, "enable-cloudformation"), } aws.RunPlecoAWS(cmd, regions, interval, wg, awsOptions) wg.Done()