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  }