Skip to content

Commit f16777a

Browse files
authored
Merge pull request #67 from mattermost/CLD-5986
CLD-5986
2 parents 379cd25 + 69e4600 commit f16777a

File tree

13 files changed

+339
-253
lines changed

13 files changed

+339
-253
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ check-style: govet lint tflint goformat
5555
.PHONY: lint
5656
lint: $(GOLANGCILINT)
5757
@echo Running golangci-lint
58-
$(GOLANGCILINT) run
58+
$(GOLANGCILINT) run --timeout=10m
5959

6060
## Runs lint against all packages for changes only
6161
.PHONY: lint-changes

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ or via a API call:
7171
"replicas": "1",
7272
"dbProxy": true,
7373
"EnableDevopsGuru": false,
74-
"AllowMajorVersionUpgrade": true
74+
"AllowMajorVersionUpgrade": true,
75+
"KMSKeyID": "",
76+
"DeletionProtection": false
7577
}
7678
```
7779

api/api.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ func initCluster(apiRouter *mux.Router, context *Context) {
2121
return newContextHandler(context, handler)
2222
}
2323

24-
clustersRouter := apiRouter.PathPrefix("/provision").Subrouter()
25-
clustersRouter.Handle("", addContext(handleProvisionDBCluster)).Methods("POST")
24+
clustersRouter := apiRouter.PathPrefix("").Subrouter()
25+
clustersRouter.Handle("/provision", addContext(handleProvisionDBCluster)).Methods("POST")
26+
clustersRouter.Handle("/delete", addContext(handleDeleteDBCluster)).Methods("POST")
2627
}
2728

2829
// handleProvisionDBCluster responds to POST /api/provision, beginning the process of creating a new RDS Aurora cluster.
@@ -42,6 +43,8 @@ func initCluster(apiRouter *mux.Router, context *Context) {
4243
// "creationSnapshotARN": "",
4344
// "enableDevopsGuru": false,
4445
// "allowMajorVersionUpgrade": false,
46+
// "KMSKeyID": "",
47+
// "deletionProtection": true
4548
// }
4649
func handleProvisionDBCluster(c *Context, w http.ResponseWriter, r *http.Request) {
4750
provisionClusterRequest, err := model.NewProvisionClusterRequestFromReader(r.Body)
@@ -66,6 +69,7 @@ func handleProvisionDBCluster(c *Context, w http.ResponseWriter, r *http.Request
6669
EnableDevopsGuru: provisionClusterRequest.EnableDevopsGuru,
6770
AllowMajorVersionUpgrade: provisionClusterRequest.AllowMajorVersionUpgrade,
6871
KMSKeyID: provisionClusterRequest.KMSKeyID,
72+
DeletionProtection: provisionClusterRequest.DeletionProtection,
6973
}
7074

7175
go dbfactory.InitProvisionCluster(&cluster)
@@ -74,3 +78,36 @@ func handleProvisionDBCluster(c *Context, w http.ResponseWriter, r *http.Request
7478
w.WriteHeader(http.StatusAccepted)
7579
outputJSON(c, w, cluster)
7680
}
81+
82+
// handleDeleteDBCluster responds to POST /api/delete, beginning the process of deleting an existing RDS Aurora cluster.
83+
func handleDeleteDBCluster(c *Context, w http.ResponseWriter, r *http.Request) {
84+
provisionClusterRequest, err := model.NewProvisionClusterRequestFromReader(r.Body)
85+
if err != nil {
86+
c.Logger.WithError(err).Error("failed to decode request")
87+
w.WriteHeader(http.StatusBadRequest)
88+
return
89+
}
90+
91+
cluster := model.Cluster{
92+
VPCID: provisionClusterRequest.VPCID,
93+
ClusterID: provisionClusterRequest.ClusterID,
94+
Environment: provisionClusterRequest.Environment,
95+
StateStore: provisionClusterRequest.StateStore,
96+
Apply: provisionClusterRequest.Apply,
97+
InstanceType: provisionClusterRequest.InstanceType,
98+
BackupRetentionPeriod: provisionClusterRequest.BackupRetentionPeriod,
99+
DBEngine: provisionClusterRequest.DBEngine,
100+
Replicas: provisionClusterRequest.Replicas,
101+
DBProxy: provisionClusterRequest.DBProxy,
102+
CreationSnapshotARN: provisionClusterRequest.CreationSnapshotARN,
103+
EnableDevopsGuru: provisionClusterRequest.EnableDevopsGuru,
104+
AllowMajorVersionUpgrade: provisionClusterRequest.AllowMajorVersionUpgrade,
105+
KMSKeyID: provisionClusterRequest.KMSKeyID,
106+
}
107+
108+
go dbfactory.InitDeleteCluster(&cluster)
109+
110+
w.Header().Set("Content-Type", "application/json")
111+
w.WriteHeader(http.StatusAccepted)
112+
outputJSON(c, w, cluster)
113+
}

cmd/dbfactory/cluster.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ import (
1919
func init() {
2020
clusterCmd.PersistentFlags().String("server", "http://localhost:8077", "The DB factory server whose API will be queried.")
2121

22+
clusterDeleteCmd.Flags().String("vpc-id", "", "The VPC id to provision a RDS Aurora Cluster")
23+
clusterDeleteCmd.Flags().String("cluster-id", "", "A random 8 character identifier of the Aurora cluster")
24+
clusterDeleteCmd.Flags().String("environment", "dev", "The environment used for the deployment. Can be dev, test, staging or prod")
25+
clusterDeleteCmd.Flags().String("state-store", "terraform-database-factory-state-bucket-dev", "The s3 bucket to store the terraform state")
26+
clusterDeleteCmd.Flags().Bool("apply", false, "If disabled, only a Terraform plan will run instead of Terraform apply")
27+
clusterDeleteCmd.Flags().String("instance-type", "db.r5.large", "The instance type used for Aurora cluster replicas")
28+
clusterDeleteCmd.Flags().String("backup-retention-period", "15", "The retention period for the DB instance backups")
29+
clusterDeleteCmd.Flags().String("db-engine", "postgres", "The database engine. Can be mysql or postgres")
30+
clusterDeleteCmd.Flags().String("replicas", "3", "The total number of write/read replicas.")
31+
clusterDeleteCmd.Flags().Bool("dbproxy", true, "If enabled the multitenant DB cluster will be used with a DB proxy.")
32+
clusterDeleteCmd.Flags().String("creation-snapshot-arn", "", "The ARN of the snapshot to use for the DB cluster (default \"\")")
33+
clusterDeleteCmd.Flags().Bool("devops-guru", false, "Enable the AWS service Devops Guru to all database instances within a cluster.")
34+
clusterDeleteCmd.Flags().Bool("allow-major-version-upgrade", false, "Enable to allow major engine version upgrades when changing engine versions.")
35+
clusterDeleteCmd.Flags().String("kms-key-id", "", "The ARN key to encrypt the storage and performance insights")
36+
2237
clusterProvisionCmd.Flags().String("vpc-id", "", "The VPC id to provision a RDS Aurora Cluster")
2338
clusterProvisionCmd.Flags().String("cluster-id", "", "A random 8 character identifier of the Aurora cluster")
2439
clusterProvisionCmd.Flags().String("environment", "dev", "The environment used for the deployment. Can be dev, test, staging or prod")
@@ -33,8 +48,10 @@ func init() {
3348
clusterProvisionCmd.Flags().Bool("devops-guru", false, "Enable the AWS service Devops Guru to all database instances within a cluster.")
3449
clusterProvisionCmd.Flags().Bool("allow-major-version-upgrade", false, "Enable to allow major engine version upgrades when changing engine versions.")
3550
clusterProvisionCmd.Flags().String("kms-key-id", "", "The ARN key to encrypt the storage and performance insights")
51+
clusterProvisionCmd.Flags().Bool("deletion-protection", true, "Disable to allow cluster deletion.")
3652

3753
clusterCmd.AddCommand(clusterProvisionCmd)
54+
clusterCmd.AddCommand(clusterDeleteCmd)
3855

3956
sess, err := session.NewSession()
4057
if err != nil {
@@ -71,6 +88,7 @@ var clusterProvisionCmd = &cobra.Command{
7188
enableDevopsGuru, _ := command.Flags().GetBool("devops-guru")
7289
allowMajorVersionUpgrade, _ := command.Flags().GetBool("allow-major-version-upgrade")
7390
kmsKeyID, _ := command.Flags().GetString("kms-key-id")
91+
deletionProtection, _ := command.Flags().GetBool("deletion-protection")
7492

7593
cluster, err := client.ProvisionCluster(&model.ProvisionClusterRequest{
7694
VPCID: vpcID,
@@ -87,6 +105,7 @@ var clusterProvisionCmd = &cobra.Command{
87105
EnableDevopsGuru: enableDevopsGuru,
88106
AllowMajorVersionUpgrade: allowMajorVersionUpgrade,
89107
KMSKeyID: kmsKeyID,
108+
DeletionProtection: deletionProtection,
90109
})
91110
if err != nil {
92111
return errors.Wrap(err, "failed to provision RDS cluster")
@@ -100,6 +119,57 @@ var clusterProvisionCmd = &cobra.Command{
100119
},
101120
}
102121

122+
var clusterDeleteCmd = &cobra.Command{
123+
Use: "delete",
124+
Short: "Delete a RDS Aurora cluster.",
125+
RunE: func(command *cobra.Command, args []string) error {
126+
command.SilenceUsage = true
127+
serverAddress, _ := command.Flags().GetString("server")
128+
client := model.NewClient(serverAddress)
129+
130+
vpcID, _ := command.Flags().GetString("vpc-id")
131+
clusterID, _ := command.Flags().GetString("cluster-id")
132+
environment, _ := command.Flags().GetString("environment")
133+
stateStore, _ := command.Flags().GetString("state-store")
134+
apply, _ := command.Flags().GetBool("apply")
135+
instanceType, _ := command.Flags().GetString("instance-type")
136+
backupRetentionPeriod, _ := command.Flags().GetString("backup-retention-period")
137+
dbEngine, _ := command.Flags().GetString("db-engine")
138+
replicas, _ := command.Flags().GetString("replicas")
139+
dbProxy, _ := command.Flags().GetBool("dbproxy")
140+
creationSnapshotARN, _ := command.Flags().GetString("creation-snapshot-arn")
141+
enableDevopsGuru, _ := command.Flags().GetBool("devops-guru")
142+
allowMajorVersionUpgrade, _ := command.Flags().GetBool("allow-major-version-upgrade")
143+
kmsKeyID, _ := command.Flags().GetString("kms-key-id")
144+
145+
cluster, err := client.DeleteCluster(&model.ProvisionClusterRequest{
146+
VPCID: vpcID,
147+
ClusterID: clusterID,
148+
Environment: environment,
149+
StateStore: stateStore,
150+
Apply: apply,
151+
InstanceType: instanceType,
152+
BackupRetentionPeriod: backupRetentionPeriod,
153+
DBEngine: dbEngine,
154+
Replicas: replicas,
155+
DBProxy: dbProxy,
156+
CreationSnapshotARN: creationSnapshotARN,
157+
EnableDevopsGuru: enableDevopsGuru,
158+
AllowMajorVersionUpgrade: allowMajorVersionUpgrade,
159+
KMSKeyID: kmsKeyID,
160+
})
161+
if err != nil {
162+
return errors.Wrap(err, "failed to delete RDS cluster")
163+
}
164+
err = printJSON(cluster)
165+
if err != nil {
166+
return err
167+
}
168+
169+
return nil
170+
},
171+
}
172+
103173
func printJSON(data interface{}) error {
104174
encoder := json.NewEncoder(os.Stdout)
105175
encoder.SetIndent("", " ")

database-factory/dbfactory.go

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,33 @@ var templateDirPostgreSQL = "terraform/aws/database-factory-postgresql"
1616
func InitProvisionCluster(cluster *model.Cluster) {
1717
err := ProvisionCluster(cluster)
1818
if err != nil {
19-
logger.WithError(err).Error("failed to deploy RDS Aurora cluster")
20-
err = sendMattermostErrorNotification(cluster, err, "The Database Factory failed to deploy RDS Aurora cluster")
21-
if err != nil {
22-
logger.WithError(err).Error("Failed to send Mattermost error notification")
19+
if isTerraformWarning(err) {
20+
// Handle Terraform warning (non-fatal error)
21+
logger.WithError(err).Warn("Terraform encountered a warning during deployment")
22+
} else {
23+
logger.WithError(err).Error("failed to deploy RDS Aurora cluster")
24+
err = sendMattermostErrorNotification(cluster, err, "The Database Factory failed to deploy RDS Aurora cluster")
25+
if err != nil {
26+
logger.WithError(err).Error("Failed to send Mattermost error notification")
27+
}
2328
}
29+
}
30+
}
2431

32+
func InitDeleteCluster(cluster *model.Cluster) {
33+
err := DeleteCluster(cluster)
34+
if err != nil {
35+
if isTerraformWarning(err) {
36+
// Handle Terraform warning (non-fatal error)
37+
logger.WithError(err).Warn("Terraform encountered a warning during deletion")
38+
} else {
39+
// Handle other types of errors (fatal errors)
40+
logger.WithError(err).Error("failed to delete RDS Aurora cluster")
41+
err = sendMattermostErrorNotification(cluster, err, "The Database Factory failed to delete RDS Aurora cluster")
42+
if err != nil {
43+
logger.WithError(err).Error("Failed to send Mattermost error notification")
44+
}
45+
}
2546
}
2647
}
2748

@@ -68,3 +89,71 @@ func ProvisionCluster(cluster *model.Cluster) error {
6889

6990
return nil
7091
}
92+
93+
// DeleteCluster is used to initiate Terraform and apply the deletion plan for the RDS Cluster.
94+
func DeleteCluster(cluster *model.Cluster) error {
95+
logger.Info("Initialising Terraform for cluster deletion")
96+
stateObject := fmt.Sprintf("rds-cluster-multitenant-%s-%s", strings.Split(cluster.VPCID, "-")[1], cluster.ClusterID)
97+
98+
var templateDir string
99+
if cluster.DBEngine == "mysql" {
100+
templateDir = templateDirMySQL
101+
} else {
102+
templateDir = templateDirPostgreSQL
103+
}
104+
105+
tf, err := terraform.New(templateDir, cluster.StateStore, logger)
106+
if err != nil {
107+
return errors.Wrap(err, "failed to initiate Terraform for cluster deletion")
108+
}
109+
110+
err = tf.Init(stateObject)
111+
if err != nil {
112+
return errors.Wrap(err, "failed to run Terraform init for cluster deletion")
113+
}
114+
115+
logger.Info("planning Terraform deletion")
116+
err = tf.PlanDeletion(cluster)
117+
if err != nil {
118+
return errors.Wrap(err, "failed to run Terraform plan for cluster deletion")
119+
}
120+
logger.Info("successfully planned Terraform deletion")
121+
122+
if cluster.Apply {
123+
logger.Info("applying Terraform deletion plan")
124+
err = tf.ApplyDeletion(cluster)
125+
if err != nil {
126+
return errors.Wrap(err, "failed to run Terraform apply for cluster deletion")
127+
}
128+
logger.Info("successfully applied Terraform deletion plan")
129+
err = sendMattermostNotification(cluster, "The Database Factory successfully deleted an RDS Aurora cluster")
130+
if err != nil {
131+
logger.WithError(err).Error("Failed to send Mattermost error notification")
132+
}
133+
return nil
134+
}
135+
136+
logger.Info("Terraform apply for cluster deletion not requested, no changes made")
137+
return nil
138+
}
139+
140+
func isTerraformWarning(err error) bool {
141+
// Check if the error message contains keywords indicating a Terraform warning
142+
errorMessage := err.Error()
143+
144+
// List of common Terraform warning keywords
145+
warningKeywords := []string{
146+
"Warning:",
147+
"Potential data loss:",
148+
"this backend configuration block will have no effect.",
149+
}
150+
151+
// Check if any of the warning keywords are present in the error message
152+
for _, keyword := range warningKeywords {
153+
if strings.Contains(errorMessage, keyword) {
154+
return true // Error is a Terraform warning
155+
}
156+
}
157+
158+
return false // Error is not a Terraform warning
159+
}

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ module github.com/mattermost/mattermost-cloud-database-factory
33
go 1.17
44

55
require (
6-
github.com/aws/aws-sdk-go v1.38.67
6+
github.com/aws/aws-sdk-go v1.44.320
77
github.com/golang/mock v1.6.0
88
github.com/gorilla/mux v1.8.0
9-
github.com/mattermost/mattermost-server/v5 v5.39.0
9+
github.com/mattermost/mattermost-server/v5 v5.39.3
1010
github.com/olekukonko/tablewriter v0.0.5
1111
github.com/pborman/uuid v1.2.1
1212
github.com/pkg/errors v0.9.1
13-
github.com/sirupsen/logrus v1.8.1
14-
github.com/spf13/cobra v1.3.0
15-
github.com/stretchr/testify v1.7.0
13+
github.com/sirupsen/logrus v1.9.3
14+
github.com/spf13/cobra v1.7.0
15+
github.com/stretchr/testify v1.8.4
1616
)
1717

1818
require (
@@ -26,7 +26,7 @@ require (
2626
github.com/gorilla/websocket v1.4.2 // indirect
2727
github.com/hashicorp/errwrap v1.1.0 // indirect
2828
github.com/hashicorp/go-multierror v1.1.1 // indirect
29-
github.com/inconshreveable/mousetrap v1.0.0 // indirect
29+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3030
github.com/jmespath/go-jmespath v0.4.0 // indirect
3131
github.com/json-iterator/go v1.1.12 // indirect
3232
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
@@ -61,5 +61,5 @@ require (
6161
gopkg.in/ini.v1 v1.66.2 // indirect
6262
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
6363
gopkg.in/yaml.v2 v2.4.0 // indirect
64-
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
64+
gopkg.in/yaml.v3 v3.0.1 // indirect
6565
)

0 commit comments

Comments
 (0)