github.com/in4it/ecs-deploy@v0.0.42-0.20240508120354-ed77ff16df25/api/export.go (about) 1 package api 2 3 import ( 4 "encoding/base64" 5 "errors" 6 "io/ioutil" 7 "sort" 8 "strconv" 9 "strings" 10 11 "github.com/in4it/ecs-deploy/provider/ecs" 12 "github.com/in4it/ecs-deploy/service" 13 "github.com/in4it/ecs-deploy/util" 14 "github.com/juju/loggo" 15 ) 16 17 // logging 18 var exportLogger = loggo.GetLogger("export") 19 20 type ExportedApps map[string]string 21 22 type Export struct { 23 templateMap map[string]string 24 deployData *service.Deploy 25 alb map[string]*ecs.ALB 26 p ecs.Paramstore 27 } 28 29 type RulePriority []int64 30 31 func (a RulePriority) Len() int { return len(a) } 32 func (a RulePriority) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 33 func (a RulePriority) Less(i, j int) bool { return a[i] < a[j] } 34 35 type ListenerRuleExport struct { 36 RuleKeys RulePriority `json:"ruleKeys" binding:"dive"` 37 Rules map[int64]ListenerRule `json:"rules" binding:"dive"` 38 } 39 40 type ListenerRule struct { 41 ListenerRuleArn string `json:"listenerRuleArn"` 42 TargetGroupArn string `json:"targetGroupArn" binding:"dive"` 43 Conditions []ListenerRuleCondition `json:"conditions" binding:"dive"` 44 } 45 type ListenerRuleCondition struct { 46 Field string `json:"field" binding:"dive"` 47 Values string `json:"values" binding:"dive"` 48 } 49 50 func (e *Export) getTemplateMap(serviceName, clusterName string) error { 51 // retrieve data 52 iam := ecs.IAM{} 53 err := iam.GetAccountId() 54 if err != nil { 55 return err 56 } 57 // get deployment obj 58 s := service.NewService() 59 s.ServiceName = serviceName 60 s.ClusterName = clusterName 61 62 dd, err := s.GetLastDeploy() 63 if err != nil { 64 return err 65 } 66 if dd.DeployData == nil { 67 return errors.New("DeployData is empty") 68 } 69 e.deployData = dd.DeployData 70 exportLogger.Debugf("got: %+v", e.deployData) 71 72 // retrieve alb data 73 var loadBalancer string 74 if e.deployData.LoadBalancer == "" { 75 loadBalancer = clusterName 76 } else { 77 loadBalancer = e.deployData.LoadBalancer 78 } 79 if _, ok := e.alb[loadBalancer]; !ok { 80 e.alb[loadBalancer], err = ecs.NewALB(loadBalancer) 81 if err != nil { 82 return err 83 } 84 // get rules for all listener 85 err = e.alb[loadBalancer].GetRulesForAllListeners() 86 if err != nil { 87 return err 88 } 89 } 90 91 // get target group (if service has loadbalancer) 92 var targetGroup *string 93 if strings.ToLower(e.deployData.ServiceProtocol) != "none" { 94 targetGroup, err = e.alb[loadBalancer].GetTargetGroupArn(serviceName) 95 if err != nil { 96 return err 97 } 98 if targetGroup == nil { 99 return errors.New("No target group found for " + serviceName) 100 } 101 } 102 103 // init map 104 e.templateMap = make(map[string]string) 105 e.templateMap["${SERVICE}"] = serviceName 106 e.templateMap["${CLUSTERNAME}"] = clusterName 107 e.templateMap["${LOADBALANCER}"] = loadBalancer 108 if targetGroup != nil { 109 e.templateMap["${TARGET_GROUP_ARN}"] = *targetGroup 110 } 111 e.templateMap["${SERVICE_DESIREDCOUNT}"] = strconv.FormatInt(e.deployData.DesiredCount, 10) 112 if e.deployData.MinimumHealthyPercent == 0 { 113 e.templateMap["${SERVICE_MINIMUMHEALTHYPERCENT}"] = "// no minimum healthy percent set" 114 } else { 115 e.templateMap["${SERVICE_MINIMUMHEALTHYPERCENT}"] = `deployment_minimum_healthy_percent = "` + strconv.FormatInt(e.deployData.MinimumHealthyPercent, 10) + `"` 116 } 117 if e.deployData.MaximumPercent == 0 { 118 e.templateMap["${SERVICE_MAXIMUMPERCENT}"] = "// no maximum percent set" 119 } else { 120 e.templateMap["${SERVICE_MAXIMUMPERCENT}"] = `deployment_maximum_percent = "` + strconv.FormatInt(e.deployData.MaximumPercent, 10) + `"` 121 } 122 e.templateMap["${SERVICE_PORT}"] = strconv.FormatInt(e.deployData.ServicePort, 10) 123 e.templateMap["${SERVICE_PROTOCOL}"] = e.deployData.ServiceProtocol 124 e.templateMap["${AWS_REGION}"] = util.GetEnv("AWS_REGION", "") 125 e.templateMap["${ACCOUNT_ID}"] = iam.AccountId 126 e.templateMap["${PARAMSTORE_PREFIX}"] = util.GetEnv("PARAMSTORE_PREFIX", "") 127 if dd.DeployData.EnvNamespace == "" { 128 e.templateMap["${NAMESPACE}"] = serviceName 129 } else { 130 e.templateMap["${NAMESPACE}"] = dd.DeployData.EnvNamespace 131 } 132 e.templateMap["${AWS_ACCOUNT_ENV}"] = util.GetEnv("AWS_ACCOUNT_ENV", "") 133 e.templateMap["${PARAMSTORE_KMS_ARN}"] = util.GetEnv("PARAMSTORE_KMS_ARN", "") 134 e.templateMap["${VPC_ID}"] = e.alb[loadBalancer].VpcId 135 if e.deployData.HealthCheck.HealthyThreshold != 0 { 136 b, err := ioutil.ReadFile("templates/export/alb_targetgroup_healthcheck.tf") 137 if err != nil { 138 exportLogger.Errorf("Can't read template templates/export/alb_targetgroup_healthcheck.tf") 139 return err 140 } 141 str := string(b) 142 if e.deployData.HealthCheck.HealthyThreshold != 0 { 143 str = strings.Replace(str, "${HEALTHCHECK_HEALTHYTHRESHOLD}", strconv.FormatInt(e.deployData.HealthCheck.HealthyThreshold, 10), -1) 144 } else { 145 str = strings.Replace(str, "${HEALTHCHECK_HEALTHYTHRESHOLD}", "3", -1) 146 } 147 if e.deployData.HealthCheck.UnhealthyThreshold != 0 { 148 str = strings.Replace(str, "${HEALTHCHECK_UNHEALTHYTHRESHOLD}", strconv.FormatInt(e.deployData.HealthCheck.UnhealthyThreshold, 10), -1) 149 } else { 150 str = strings.Replace(str, "${HEALTHCHECK_UNHEALTHYTHRESHOLD}", "2", -1) 151 } 152 if e.deployData.HealthCheck.Protocol != "" { 153 str = strings.Replace(str, "${HEALTHCHECK_PROTOCOL}", e.deployData.HealthCheck.Protocol, -1) 154 } else { 155 str = strings.Replace(str, "${HEALTHCHECK_PROTOCOL}", "HTTP", -1) 156 } 157 if e.deployData.HealthCheck.Path != "" { 158 str = strings.Replace(str, "${HEALTHCHECK_PATH}", e.deployData.HealthCheck.Path, -1) 159 } else { 160 str = strings.Replace(str, "${HEALTHCHECK_PATH}", "/", -1) 161 } 162 if e.deployData.HealthCheck.Interval != 0 { 163 str = strings.Replace(str, "${HEALTHCHECK_INTERVAL}", strconv.FormatInt(e.deployData.HealthCheck.Interval, 10), -1) 164 } else { 165 str = strings.Replace(str, "${HEALTHCHECK_INTERVAL}", "30", -1) 166 } 167 if e.deployData.HealthCheck.Matcher != "" { 168 str = strings.Replace(str, "${HEALTHCHECK_MATCHER}", e.deployData.HealthCheck.Matcher, -1) 169 } else { 170 str = strings.Replace(str, "${HEALTHCHECK_MATCHER}", "200", -1) 171 } 172 if e.deployData.HealthCheck.Timeout > 0 { 173 str = strings.Replace(str, "${HEALTHCHECK_TIMEOUT}", strconv.FormatInt(e.deployData.HealthCheck.Timeout, 10), -1) 174 } else { 175 str = strings.Replace(str, "${HEALTHCHECK_TIMEOUT}", "5", -1) 176 } 177 e.templateMap["${HEALTHCHECK}"] = str 178 } 179 return nil 180 } 181 182 // check first whether the template is in the parameter store 183 // if not, use the default template from the template path 184 func (e *Export) getTemplate(template string) (*string, error) { 185 parameter, ok := e.p.Parameters["TEMPLATES_EXPORT_"+strings.Replace(strings.ToUpper(template), ".", "_", -1)] 186 str := parameter.Value 187 if !ok { 188 b, err := ioutil.ReadFile("templates/export/" + template) 189 if err != nil { 190 exportLogger.Errorf("Can't read template templates/export/" + template) 191 return nil, err 192 } 193 str = string(b) 194 } 195 // replace 196 for k, v := range e.templateMap { 197 str = strings.Replace(str, k, v, -1) 198 } 199 return &str, nil 200 } 201 202 func (e *Export) terraform() (*map[string]ExportedApps, error) { 203 // get all services 204 export := make(map[string]ExportedApps) 205 export["apps"] = make(ExportedApps) 206 e.alb = make(map[string]*ecs.ALB) 207 208 var ds service.DynamoServices 209 // get possible parameters 210 e.p = ecs.Paramstore{} 211 e.p.GetParameters(e.p.GetPrefix(), true) 212 // ecr obj 213 ecr := ecs.ECR{} 214 // get services 215 s := service.NewService() 216 err := s.GetServices(&ds) 217 if err != nil { 218 return nil, err 219 } 220 for _, service := range ds.Services { 221 var ret string 222 err := e.getTemplateMap(service.S, service.C) 223 if err != nil { 224 return nil, err 225 } 226 exportLogger.Debugf("Retrieved template map: %+v", e.templateMap) 227 228 // check if we have targetGroup 229 var processTargetGroup bool 230 if _, ok := e.templateMap["${TARGET_GROUP_ARN}"]; ok { 231 processTargetGroup = true 232 } 233 // check whether to process ecr 234 processEcr, err := ecr.RepositoryExists(service.S) 235 if err != nil { 236 return nil, err 237 } 238 239 var toProcess []string 240 241 if processEcr { 242 toProcess = append(toProcess, "ecr") 243 } 244 if processTargetGroup { 245 toProcess = append(toProcess, []string{"ecs", "iam", "alb_targetgroup"}...) 246 } else { 247 toProcess = append(toProcess, []string{"ecs", "iam"}...) 248 } 249 if e.p.IsEnabled() { 250 toProcess = append(toProcess, "iam_paramstore") 251 } 252 for _, v := range toProcess { 253 t, err := e.getTemplate(v + ".tf") 254 if err != nil { 255 return nil, err 256 } 257 ret += *t 258 } 259 260 // get listener rules 261 if processTargetGroup { 262 t, err := e.getListenerRules(service.S, service.C, service.Listeners, e.templateMap["${LOADBALANCER}"]) 263 if err != nil { 264 return nil, err 265 } 266 ret += *t 267 } 268 export["apps"][service.S] = base64.StdEncoding.EncodeToString([]byte(ret)) 269 } 270 return &export, nil 271 } 272 273 func (e *Export) getListenerRules(serviceName string, clusterName string, listeners []string, loadBalancer string) (*string, error) { 274 var ret string 275 // listeners 276 albListenerRule, err := e.getTemplate("alb_listenerrule.tf") 277 if err != nil { 278 return nil, err 279 } 280 condition, err := e.getTemplate("alb_listenerrule_condition.tf") 281 if err != nil { 282 return nil, err 283 } 284 if len(e.deployData.RuleConditions) == 0 { 285 exportLogger.Debugf("No rule conditions, going with default rules") 286 for _, l := range listeners { 287 a := strings.Replace(*albListenerRule, "${LISTENER_ARN}", l, -1) 288 for _, v := range []string{"/" + serviceName, "/" + serviceName + "/*"} { 289 // get priority 290 ruleArn, priority, err := e.alb[loadBalancer].FindRule(l, e.templateMap["${TARGET_GROUP_ARN}"], []string{"path-pattern"}, []string{v}) 291 if err != nil { 292 return nil, err 293 } 294 // replace listeners and return template 295 a = strings.Replace(a, "${LISTENER_PRIORITY}", *priority, -1) 296 a = strings.Replace(a, "${LISTENER_RULE_ARN}", *ruleArn, -1) 297 c := strings.Replace(*condition, "${LISTENER_CONDITION_FIELD}", "path-pattern", -1) 298 c = strings.Replace(c, "${LISTENER_CONDITION_VALUE}", v, -1) 299 ret += strings.Replace(a, "${LISTENER_CONDITION_RULE}", c, -1) 300 } 301 } 302 } else { 303 exportLogger.Debugf("Found rule conditions in deploy, examining conditions") 304 for _, y := range e.deployData.RuleConditions { 305 for _, l := range e.alb[loadBalancer].Listeners { 306 for _, l2 := range y.Listeners { 307 if l.Protocol != nil && strings.ToLower(*l.Protocol) == strings.ToLower(l2) { 308 a := strings.Replace(*albListenerRule, "${LISTENER_ARN}", *l.ListenerArn, -1) 309 var c, cc string 310 var f []string 311 var v []string 312 if y.PathPattern != "" { 313 f = append(f, "path-pattern") 314 v = append(v, y.PathPattern) 315 c = strings.Replace(*condition, "${LISTENER_CONDITION_FIELD}", "path-pattern", -1) 316 c = strings.Replace(c, "${LISTENER_CONDITION_VALUE}", y.PathPattern, -1) 317 } 318 if y.Hostname != "" { 319 f = append(f, "host-header") 320 v = append(v, y.Hostname+"."+e.alb[loadBalancer].GetDomain()) 321 cc = strings.Replace(*condition, "${LISTENER_CONDITION_FIELD}", "host-header", -1) 322 cc = strings.Replace(cc, "${LISTENER_CONDITION_VALUE}", y.Hostname+"."+e.alb[loadBalancer].GetDomain(), -1) 323 } 324 // get priority 325 ruleArn, priority, err := e.alb[loadBalancer].FindRule(*l.ListenerArn, e.templateMap["${TARGET_GROUP_ARN}"], f, v) 326 if err != nil { 327 return nil, err 328 } 329 a = strings.Replace(a, "${LISTENER_PRIORITY}", *priority, -1) 330 a = strings.Replace(a, "${LISTENER_RULE_ARN}", *ruleArn, -1) 331 // get everything together and return template 332 ret += strings.Replace(a, "${LISTENER_CONDITION_RULE}", c+cc, -1) 333 } 334 } 335 } 336 } 337 } 338 return &ret, nil 339 } 340 341 func (e *Export) getTargetGroupArn(serviceName string) (*string, error) { 342 a := ecs.ALB{} 343 return a.GetTargetGroupArn(serviceName) 344 } 345 func (e *Export) getListenerRuleArn(serviceName string, rulePriority string) (*string, error) { 346 var clusterName string 347 var listenerRuleArn string 348 var ds service.DynamoServices 349 s := service.NewService() 350 s.GetServices(&ds) 351 for _, service := range ds.Services { 352 if service.S == serviceName { 353 clusterName = service.C 354 } 355 } 356 if clusterName == "" { 357 return nil, errors.New("Service not found: " + serviceName) 358 } 359 a, err := ecs.NewALB(clusterName) 360 if err != nil { 361 return nil, err 362 } 363 targetGroupArn, err := a.GetTargetGroupArn(serviceName) 364 if err != nil { 365 return nil, err 366 } 367 a.GetRulesForAllListeners() 368 for _, rules := range a.Rules { 369 for _, rule := range rules { 370 if *rule.Priority == rulePriority { 371 if listenerRuleArn != "" { 372 return nil, errors.New("Duplicate listener rule found, can't determine listener (rule = " + rulePriority + ", Conflict between " + listenerRuleArn + " and " + *rule.RuleArn + ")") 373 } else { 374 if len(rule.Actions) > 0 && *rule.Actions[0].TargetGroupArn == *targetGroupArn { 375 listenerRuleArn = *rule.RuleArn 376 } 377 } 378 } 379 } 380 } 381 if listenerRuleArn == "" { 382 return nil, errors.New("No rule with priority " + rulePriority + " found") 383 } 384 return &listenerRuleArn, nil 385 } 386 func (e *Export) getListenerRuleArns(serviceName string) (*ListenerRuleExport, error) { 387 var clusterName string 388 var ds service.DynamoServices 389 var result *ListenerRuleExport 390 var exportRuleKeys RulePriority 391 exportRules := make(map[int64]ListenerRule) 392 s := service.NewService() 393 s.GetServices(&ds) 394 for _, service := range ds.Services { 395 if service.S == serviceName { 396 clusterName = service.C 397 } 398 } 399 if clusterName == "" { 400 return nil, errors.New("Service not found: " + serviceName) 401 } 402 a, err := ecs.NewALB(clusterName) 403 if err != nil { 404 return nil, err 405 } 406 targetGroupArn, err := a.GetTargetGroupArn(serviceName) 407 if err != nil { 408 return nil, err 409 } 410 a.GetRulesForAllListeners() 411 for _, rules := range a.Rules { 412 for _, rule := range rules { 413 if len(rule.Actions) > 0 && *rule.Actions[0].TargetGroupArn == *targetGroupArn { 414 priority, err := strconv.ParseInt(*rule.Priority, 10, 64) 415 if err != nil { 416 return nil, err 417 } 418 var conditions []ListenerRuleCondition 419 for _, condition := range rule.Conditions { 420 if len(condition.Values) > 0 { 421 conditions = append(conditions, ListenerRuleCondition{Field: *condition.Field, Values: *condition.Values[0]}) 422 } 423 } 424 exportRuleKeys = append(exportRuleKeys, priority) 425 exportRules[priority] = ListenerRule{ 426 ListenerRuleArn: *rule.RuleArn, 427 TargetGroupArn: *rule.Actions[0].TargetGroupArn, 428 Conditions: conditions, 429 } 430 } 431 } 432 } 433 if len(exportRuleKeys) == 0 { 434 return nil, errors.New("No rules found for service: " + serviceName) 435 } 436 sort.Sort(exportRuleKeys) 437 result = &ListenerRuleExport{RuleKeys: exportRuleKeys, Rules: exportRules} 438 return result, nil 439 }