cloudamatic/mu

View on GitHub
modules/mu/providers/aws/loadbalancer.rb

Summary

Maintainability
F
4 days
Test Coverage
# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#     http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module MU
  class Cloud
    class AWS
      # A load balancer as configured in {MU::Config::BasketofKittens::loadbalancers}
      class LoadBalancer < MU::Cloud::LoadBalancer

        @lb = nil
        attr_reader :targetgroups

        # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
        # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
        def initialize(**args)
          super

          if args[:from_cloud_desc]
            if args[:from_cloud_desc].class.name == "Aws::ElasticLoadBalancing::Types::LoadBalancerDescription"
              @config['classic'] = true
            else
              @config['classic'] = false
            end
          end

          @mu_name ||= @deploy.getResourceName(@config["name"], max_length: 32, need_unique_string: true)
          @mu_name.gsub!(/[^\-a-z0-9]/i, "-") # AWS ELB naming rules
        end

        # Called automatically by {MU::Deploy#createResources}
        def create
          if @config["zones"] == nil
            @config["zones"] = MU::Cloud::AWS.listAZs(region: @region)
            MU.log "Using zones from #{@region}", MU::DEBUG, details: @config['zones']
          end

          lb_options = {
            tags: []
          }
          if @config['classic']
            lb_options[:load_balancer_name] = @mu_name
          else
            lb_options[:name] = @mu_name
          end

          MU::MommaCat.listStandardTags.each_pair { |name, value|
            lb_options[:tags] << {key: name, value: value}
          }

          if @config['optional_tags']
            MU::MommaCat.listOptionalTags.each_pair { |name, value|
              lb_options[:tags] << {key: name, value: value}
            }
          end

          if !@config['tags'].nil?
            @config['tags'].each { |tag|
              lb_options[:tags] << {key: tag['key'], value: tag['value']}
            }
          end

          sgs = []
          if @dependencies.has_key?("firewall_rule")
            @dependencies['firewall_rule'].values.each { |sg|
              sgs << sg.cloud_id
            }
          end
          if sgs.size > 0 and !@config['vpc'].nil?
            lb_options[:security_groups] = sgs
            @config['sgs'] = sgs
          end

          if @config["vpc"] != nil
            if @vpc.nil?
              raise MuError, "LoadBalancer #{@config['name']} is configured to use a VPC, but no VPC found"
            end
            lb_options[:subnets] = []
            @config["vpc"]["subnets"].each { |subnet|
              subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"], name: subnet["subnet_name"])
              if subnet_obj.nil?
                raise MuError, "Failed to locate subnet from #{subnet} in LoadBalancer #{@config['name']}"
              end
              lb_options[:subnets] << subnet_obj.cloud_id
            }
            if @config["private"]
              lb_options[:scheme] = "internal"
            end
          else
            lb_options[:availability_zones] = @config["zones"]
          end

          listeners = Array.new
          if @config['classic']
            @config["listeners"].each { |listener|
              listen_struct = {
                :load_balancer_port => listener["lb_port"],
                :protocol => listener["lb_protocol"],
                :instance_port => listener["instance_port"],
                :instance_protocol => listener["instance_protocol"]
              }
              listen_struct[:ssl_certificate_id] = listener["ssl_certificate_id"] if !listener["ssl_certificate_id"].nil?

              listeners << listen_struct
            }
            lb_options[:listeners] = listeners
          end

          zones_to_try = @config["zones"]
          retries = 0
          lb = nil
          begin
            if @config['classic']
              MU.log "Creating Elastic Load Balancer #{@mu_name}", details: lb_options
              lb = MU::Cloud::AWS.elb(region: @region, credentials: @credentials).create_load_balancer(lb_options)
            else
              MU.log "Creating Application Load Balancer #{@mu_name}", details: lb_options
              lb = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).create_load_balancer(lb_options).load_balancers.first
              begin
                if lb.state.code != "active"
                  MU.log "Waiting for ALB #{@mu_name} to enter 'active' state", MU::NOTICE
                  sleep 20
                  lb = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).describe_load_balancers(
                    names: [@mu_name]
                  ).load_balancers.first
                end
              end while lb.state.code != "active"
            end
          rescue Aws::ElasticLoadBalancing::Errors::ValidationError, Aws::ElasticLoadBalancing::Errors::SubnetNotFound, Aws::ElasticLoadBalancing::Errors::InvalidConfigurationRequest => e
            if zones_to_try.size > 0 and lb_options.has_key?(:availability_zones)
              MU.log "Got #{e.inspect} when creating #{@mu_name} retrying with individual AZs in case that's the problem", MU::WARN
              lb_options[:availability_zones] = [zones_to_try.pop]
              retry
            else
              raise MuError, "#{e.inspect} when creating #{@mu_name}", e.backtrace
            end
          rescue Aws::ElasticLoadBalancing::Errors::InvalidSecurityGroup => e
            if retries < 5
              MU.log "#{e.inspect}, waiting then retrying", MU::WARN
              sleep 10
              retries = retries + 1
              retry
            else
              raise MuError, "#{e.inspect} when creating #{@mu_name}", e.backtrace
            end
          end
          @cloud_id = @mu_name
          MU.log "LoadBalancer #{@config['name']} is at #{lb.dns_name}"
          MU.log "LoadBalancer #{@config['name']} is at #{lb.dns_name}", MU::SUMMARY

          parent_thread_id = Thread.current.object_id
          generic_mu_dns = nil
          dnsthread = Thread.new {
            if !MU::Cloud::AWS.isGovCloud?
              MU.dupGlobals(parent_thread_id)
              generic_mu_dns = MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: @mu_name, target: "#{lb.dns_name}.", cloudclass: MU::Cloud::LoadBalancer, sync_wait: @config['dns_sync_wait'])
            end
          }

          if zones_to_try.size < @config["zones"].size
            zones_to_try.each { |zone|
              begin
                MU::Cloud::AWS.elb(region: @region, credentials: @credentials).enable_availability_zones_for_load_balancer(
                  load_balancer_name: @mu_name,
                  availability_zones: [zone]
                )
              rescue Aws::ElasticLoadBalancing::Errors::ValidationError => e
                MU.log "Couldn't enable Availability Zone #{zone} for Load Balancer #{@mu_name} (#{e.message})", MU::WARN
              end
            }
          end

          @targetgroups = {}
          if !@config['healthcheck'].nil? and @config['classic']
            MU.log "Configuring custom health check for ELB #{@mu_name}", details: @config['healthcheck']
            MU::Cloud::AWS.elb(region: @region, credentials: @credentials).configure_health_check(
                load_balancer_name: @mu_name,
                health_check: {
                    target: @config['healthcheck']['target'],
                    interval: @config['healthcheck']['interval'],
                    timeout: @config['healthcheck']['timeout'],
                    unhealthy_threshold: @config['healthcheck']['unhealthy_threshold'],
                    healthy_threshold: @config['healthcheck']['healthy_threshold']
                }
            )
          elsif !@config['classic']
            if @config['targetgroups']
              MU.log "Configuring target groups and health checks check for ELB #{@mu_name}", details: @config['healthcheck']
              @config['targetgroups'].each { |tg|
                tg_name = @deploy.getResourceName(tg["name"], max_length: 32, disallowed_chars: /[^A-Za-z0-9-]/)
                tg_descriptor = {
                  :name => tg_name,
                  :protocol => tg['proto'],
                  :vpc_id => @vpc.cloud_id,
                  :port => tg['port'],
                  :target_type  => 'instance'
                }
                if tg['target_type'] && tg['target_type'] != 'instance'
                  tg_descriptor[:target_type] = tg['target_type']
                end
                if tg['httpcode']
                  tg_descriptor[:matcher] = {
                    :http_code => tg['httpcode']
                  }
                end
                if tg['healthcheck']
                  hc_target = tg['healthcheck']['target'].match(/^([^:]+):(\d+)(.*)/)
                  tg_descriptor[:health_check_protocol] = hc_target[1]
                  tg_descriptor[:health_check_port] = hc_target[2]
                  tg_descriptor[:health_check_path] = hc_target[3]
                  tg_descriptor[:health_check_interval_seconds] = tg['healthcheck']['interval']
                  tg_descriptor[:health_check_timeout_seconds] = tg['healthcheck']['timeout']
                  tg_descriptor[:healthy_threshold_count] = tg['healthcheck']['healthy_threshold']
                  tg_descriptor[:unhealthy_threshold_count] = tg['healthcheck']['unhealthy_threshold']
                  if tg['healthcheck']['httpcode'] and !tg_descriptor.has_key?(:matcher)
                    tg_descriptor[:matcher] = {
                      :http_code => tg['healthcheck']['httpcode']
                    }
                  end
                end

                tg_resp = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).create_target_group(tg_descriptor)
                @targetgroups[tg['name']] = tg_resp.target_groups.first
                MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).add_tags(
                  resource_arns: [tg_resp.target_groups.first.target_group_arn],
                  tags: lb_options[:tags]
                )
              }
            end
          end

          redirect_block = Proc.new { |r|
            {
              :protocol => r['protocol'],
              :port => r['port'].to_s,
              :host => r['host'],
              :path => r['path'],
              :query => r['query'],
              :status_code => "HTTP_"+r['status_code'].to_s
            }
          }

          if !@config['classic']
            @config["listeners"].each { |l|
              action = if l['redirect']
                {
                  :type => "redirect",
                  :redirect_config => redirect_block.call(l['redirect'])
                }
              else
                if !@targetgroups.has_key?(l['targetgroup'])
                  raise MuError, "Listener in #{@mu_name} configured for target group #{l['targetgroup']}, but I don't have data on a targetgroup by that name"
                end
                {
                  :target_group_arn => @targetgroups[l['targetgroup']].target_group_arn,
                  :type => "forward"
                }
              end
              listen_descriptor = {
                :default_actions => [ action ],
                :load_balancer_arn => lb.load_balancer_arn,
                :port => l['lb_port'], 
                :protocol => l['lb_protocol']
              }
              if l['ssl_certificate_id']
                listen_descriptor[:certificates] = [{
                  :certificate_arn => l['ssl_certificate_id']
                }]
                listen_descriptor[:ssl_policy] = case l['tls_policy']
                when "tls1.0"
                  "ELBSecurityPolicy-TLS-1-0-2015-04"
                when "tls1.1"
                  "ELBSecurityPolicy-TLS-1-1-2017-01"
                when "tls1.2"
                  "ELBSecurityPolicy-TLS-1-2-2017-01"
                end
              end
              listen_resp = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).create_listener(listen_descriptor).listeners.first
              if !l['rules'].nil?
                l['rules'].each { |rule|
                  rule_descriptor = {
                    :listener_arn => listen_resp.listener_arn,
                    :priority => rule['order'],
                    :conditions => rule['conditions'],
                    :actions => []
                  }
                  rule['actions'].each { |a|
                    rule_descriptor[:actions] << if a['action'] == "forward"
                      {
                        :target_group_arn => @targetgroups[a['targetgroup']].target_group_arn,
                        :type => a['action']
                      }
                    elsif a['action'] == "redirect"
                      {
                        :redirect_config => redirect_block.call(rule['redirect']),
                        :type => a['action']
                      }
                    end
                  }
                  MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).create_rule(rule_descriptor)
                }
              end
            }
          else
            @config["listeners"].each { |l|
              if l['ssl_certificate_id']
                MU::Cloud::AWS.elb(region: @region, credentials: @credentials).set_load_balancer_policies_of_listener(
                  load_balancer_name: @cloud_id, 
                  load_balancer_port: l['lb_port'], 
                  policy_names: [
                    case l['tls_policy']
                    when "tls1.0"
                      "ELBSecurityPolicy-2016-08"
                    when "tls1.1"
                      # XXX This policy shows up in the console, but doesn't
                      # work there either. I think it's Amazon's bug, though we
                      # could get around it by creating a custom policy with all
                      # the bits we want. Ugh. Just use an ALB, man.
                      # "ELBSecurityPolicy-TLS-1-1-2017-01" 
                      MU.log "Correct TLS1.1 cipher policy for classic Load Balancers is currently not supported, falling back to ELBSecurityPolicy-2016-08", MU::WARN
                      "ELBSecurityPolicy-2016-08"
                    when "tls1.2"
                      # XXX This policy shows up in the console, but doesn't
                      # work there either. I think it's Amazon's bug, though we
                      # could get around it by creating a custom policy with all
                      # the bits we want. Ugh. Just use an ALB, man.
                      # "ELBSecurityPolicy-TLS-1-2-2017-01" 
                      MU.log "Correct TLS1.2 cipher policy for classic Load Balancers is currently not supported, falling back to ELBSecurityPolicy-2016-08", MU::WARN
                      "ELBSecurityPolicy-2016-08"
                    end
                  ]
                )
              end
            }
          end

          if @config['cross_zone_unstickiness'] 
            MU.log "Enabling cross-zone un-stickiness on #{lb.dns_name}"
            if @config['classic']
              MU::Cloud::AWS.elb(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                load_balancer_name: @mu_name,
                load_balancer_attributes: {
                  cross_zone_load_balancing: {
                    enabled: true
                  }
                }
              )
            else
              @targetgroups.values.each { |tg|
                MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).modify_target_group_attributes(
                  target_group_arn: tg.target_group_arn,
                  attributes: [
                    {
                      key: "stickiness.enabled",
                      value: "true"
                    }
                  ]
                )
              }
            end
          end

          if !@config['idle_timeout'].nil?
            MU.log "Setting idle timeout to #{@config['idle_timeout']} #{lb.dns_name}"
            if @config['classic']
              MU::Cloud::AWS.elb(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                load_balancer_name: @mu_name,
                load_balancer_attributes: {
                  connection_settings: {
                    idle_timeout: @config['idle_timeout']
                  }
                }
              )
            else
              MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                load_balancer_arn: lb.load_balancer_arn,
                attributes: [
                  {
                    key: "idle_timeout.timeout_seconds",
                    value: @config['idle_timeout'].to_s
                  }
                ]
              )
            end
          end

          if !@config['connection_draining_timeout'].nil?
            if @config['classic']
              if @config['connection_draining_timeout'] >= 0
                MU.log "Setting connection draining timeout to #{@config['connection_draining_timeout']} on #{lb.dns_name}"
                MU::Cloud::AWS.elb(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                    load_balancer_name: @mu_name,
                    load_balancer_attributes: {
                        connection_draining: {
                            enabled: true,
                            timeout: @config['connection_draining_timeout']
                        }
                    }
                )
              else
                MU.log "Disabling connection draining on #{lb.dns_name}"
                MU::Cloud::AWS.elb(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                    load_balancer_name: @mu_name,
                    load_balancer_attributes: {
                        connection_draining: {
                            enabled: false
                        }
                    }
                )
              end
            else
              timeout = @config['connection_draining_timeout'].to_s
              if @config['connection_draining_timeout'] >= 0
                MU.log "Setting connection draining timeout to #{@config['connection_draining_timeout']} on #{lb.dns_name}"
              else
                timeout = 0
                MU.log "Disabling connection draining on #{lb.dns_name}"
              end
              @targetgroups.values.each { |tg|
                MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).modify_target_group_attributes(
                  target_group_arn: tg.target_group_arn,
                  attributes: [
                    {
                      key: "deregistration_delay.timeout_seconds",
                      value: timeout.to_s
                    }
                  ]
                )
              }
            end
          end

          if !@config['access_log'].nil?
            MU.log "Setting access log params for #{lb.dns_name}", details: @config['access_log']
            if @config['classic']
              MU::Cloud::AWS.elb(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                load_balancer_name: @mu_name,
                load_balancer_attributes: {
                  access_log: {
                    enabled: @config['access_log']['enabled'],
                    emit_interval: @config['access_log']['emit_interval'],
                    s3_bucket_name: @config['access_log']['s3_bucket_name'],
                    s3_bucket_prefix: @config['access_log']['s3_bucket_prefix']
                  }
                }
              )
            else
              MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).modify_load_balancer_attributes(
                load_balancer_arn: lb.load_balancer_arn,
                attributes: [
                  {
                    key: "access_logs.s3.enabled",
                    value: "true"
                  },
                  {
                    key: "access_logs.s3.bucket",
                    value: @config['access_log']['s3_bucket_name']
                  },
                  {
                    key: "access_logs.s3.prefix",
                    value: @config['access_log']['s3_bucket_prefix']
                  }
                ]
              )
            end
          end

          if !@config['lb_cookie_stickiness_policy'].nil?
            MU.log "Setting ELB cookie stickiness policy for #{lb.dns_name}", details: @config['lb_cookie_stickiness_policy']
            if @config['classic']
              cookie_policy = {
                load_balancer_name: @mu_name,
                policy_name: @config['lb_cookie_stickiness_policy']['name']
              }
              if !@config['lb_cookie_stickiness_policy']['timeout'].nil?
                cookie_policy[:cookie_expiration_period] = @config['lb_cookie_stickiness_policy']['timeout']
              end
              MU::Cloud::AWS.elb(region: @region, credentials: @credentials).create_lb_cookie_stickiness_policy(cookie_policy)
              lb_policy_names = Array.new
              lb_policy_names << @config['lb_cookie_stickiness_policy']['name']
              listener_policy = {
                load_balancer_name: @mu_name,
                policy_names: lb_policy_names
              }
              lb_options[:listeners].each do |listener|
                if listener[:protocol].upcase == 'HTTP' or listener[:protocol].upcase == 'HTTPS'
                  listener_policy[:load_balancer_port] = listener[:load_balancer_port]
                  MU::Cloud::AWS.elb(region: @region, credentials: @credentials).set_load_balancer_policies_of_listener(listener_policy)
                end
              end
            else
              @targetgroups.values.each { |tg|
                MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).modify_target_group_attributes(
                  target_group_arn: tg.target_group_arn,
                  attributes: [
                    {
                      key: "stickiness.type",
                      value: "lb_cookie"
                    },
                    {
                      key: "stickiness.enabled",
                      value: "true"
                    },
                    {
                      key: "stickiness.lb_cookie.duration_seconds",
                      value: @config['lb_cookie_stickiness_policy']['timeout'].to_s
                    }
                  ]
                )
              }
            end
          end

          if !@config['app_cookie_stickiness_policy'].nil? 
            if @config['classic']
              MU.log "Setting application cookie stickiness policy for #{lb.dns_name}", details: @config['app_cookie_stickiness_policy']
              cookie_policy = {
                load_balancer_name: @mu_name,
                policy_name: @config['app_cookie_stickiness_policy']['name'],
                cookie_name: @config['app_cookie_stickiness_policy']['cookie']
              }
              MU::Cloud::AWS.elb(region: @region, credentials: @credentials).create_app_cookie_stickiness_policy(cookie_policy)
              lb_policy_names = Array.new
              lb_policy_names << @config['app_cookie_stickiness_policy']['name']
              listener_policy = {
                load_balancer_name: @mu_name,
                policy_names: lb_policy_names
              }
              lb_options[:listeners].each do |listener|
                if listener[:protocol].upcase == 'HTTP' or listener[:protocol].upcase == 'HTTPS'
                  listener_policy[:load_balancer_port] = listener[:load_balancer_port]
                  MU::Cloud::AWS.elb(region: @region, credentials: @credentials).set_load_balancer_policies_of_listener(listener_policy)
                end
              end
            else
              MU.log "App cookie stickiness not supported in ALBs. Redeploy with 'classic' set to true if you need this functionality.", MU::WARN
            end
          end

          dnsthread.join # from genericMuDNS

          if !@config['dns_records'].nil?
            # XXX this should be a call to @deploy.nameKitten
            @config['dns_records'].each { |dnsrec|
              dnsrec['name'] = @mu_name.downcase if !dnsrec.has_key?('name')
              dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/)
            }
            if !@config['classic']
              # XXX should be R53ALIAS, but we get "the alias target name does not lie within the target zone"
              @config['dns_records'].each { |r|
                r['type'] = "CNAME"
              }
            end
            if !MU::Cloud::AWS.isGovCloud?
              MU::Cloud.resourceClass("AWS", "DNSZone").createRecordsFromConfig(@config['dns_records'], target: cloud_desc.dns_name)
            end
          end

          notify
        end

        # Canonical Amazon Resource Number for this resource
        # @return [String]
        def arn
          if @config['classic']
            "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":elasticloadbalancing:"+@region+":"+MU::Cloud::AWS.credToAcct(@credentials)+":loadbalancer/"+@cloud_id
          else
            cloud_desc.load_balancer_arn
          end
        end

        @cloud_desc_cache = nil
        # Wrapper for cloud_desc method that deals with elb vs. elb2 resources.
        def cloud_desc(use_cache: true)
          return @cloud_desc_cache if @cloud_desc_cache and use_cache
          return nil if !@cloud_id
          if @config['classic']
            @cloud_desc_cache = MU::Cloud::AWS.elb(region: @region, credentials: @credentials).describe_load_balancers(
              load_balancer_names: [@cloud_id]
            ).load_balancer_descriptions.first
            return @cloud_desc_cache
          else
            @cloud_desc_cache = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).describe_load_balancers(
              names: [@cloud_id]
            ).load_balancers.first
            if @targetgroups.nil? 
              @targetgroups = {}
              if !@deploy.nil? and
                 @deploy.deployment['loadbalancers'] and
                 @deploy.deployment['loadbalancers'][@config['name']] and
                 @deploy.deployment['loadbalancers'][@config['name']]["targetgroups"]
                @deploy.deployment['loadbalancers'][@config['name']]["targetgroups"].each_pair { |tg_name, tg_arn|
                  @targetgroups[tg_name] = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).describe_target_groups(target_group_arns: [tg_arn]).target_groups.first
                }
              else
                pp @config['targetgroups']
                MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).describe_target_groups(load_balancer_arn: @cloud_desc_cache.load_balancer_arn).target_groups.each { |tg|
                  tg_name = tg.target_group_name
                  if @config['targetgroups']
                    @config['targetgroups'].each { |tg_cfg|
                      if tg_name = @deploy.getResourceName(tg_cfg["name"], max_length: 32, disallowed_chars: /[^A-Za-z0-9-]/)
                        tg_name = tg_cfg['name']
                        break
                      end
                    }
                  end
                  @targetgroups[tg_name] = tg
                }
#                @config['targetgroups'].each { |tg|
#                  tg_name = @deploy.getResourceName(tg["name"], max_length: 32, disallowed_chars: /[^A-Za-z0-9-]/)
#                  @targetgroups[tg_name] = MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).describe_target_groups(target_group_arns: [tg_arn]).target_groups.first
#                }
              end
            end

            return @cloud_desc_cache
          end
        end

        # Return the metadata for this LoadBalancer
        # @return [Hash]
        def notify
          deploy_struct = {
            "awsname" => @mu_name,
            "dns" => cloud_desc.dns_name,
            "targetgroups" => {}
          }
          deploy_struct["arn"] = cloud_desc.load_balancer_arn if !@config['classic']
          @targetgroups.each { |tgname, tg|
            deploy_struct["targetgroups"][tgname] = tg.target_group_arn
          }
          return deploy_struct
        end

        # Register a Server node with an existing LoadBalancer.
        #
        # @param instance_id [String] A node to register.
        # @param targetgroups [Array<String>] The target group(s) of which this node should be made a member. Not applicable to classic LoadBalancers. If not supplied, the node will be registered to all available target groups on this LoadBalancer.
        def registerNode(instance_id, targetgroups: nil)
          if @config['classic'] or !@config.has_key?("classic")
            MU.log "Registering #{instance_id} to ELB #{@cloud_id}"
            MU::Cloud::AWS.elb(region: @region, credentials: @credentials).register_instances_with_load_balancer(
              load_balancer_name: @cloud_id,
              instances: [
                {instance_id: instance_id}
              ]
            )
          else
            if targetgroups.nil? or !targetgroups.is_a?(Array) or targetgroups.size == 0
              if @targetgroups.nil?
                cloud_desc
                return if @targetgroups.nil?
              end
              targetgroups = @targetgroups.keys
            end
            targetgroups.each { |tg|
              MU.log "Registering #{instance_id} to Target Group #{tg}"
              MU::Cloud::AWS.elb2(region: @region, credentials: @credentials).register_targets(
                target_group_arn: @targetgroups[tg].target_group_arn,
                targets: [
                  {id: instance_id}
                ]
              )
            }
          end
        end

        # Does this resource type exist as a global (cloud-wide) artifact, or
        # is it localized to a region/zone?
        # @return [Boolean]
        def self.isGlobal?
          false
        end

        # Denote whether this resource implementation is experiment, ready for
        # testing, or ready for production use.
        def self.quality
          MU::Cloud::RELEASE
        end

        # Remove all load balancers associated with the currently loaded deployment.
        # @param noop [Boolean]: If true, will only print what would be done
        # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
        # @param region [String]: The cloud provider region
        # @return [void]
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
          if (deploy_id.nil? or deploy_id.empty?) and (!flags or !flags["vpc_id"])
            raise MuError, "Can't touch ELBs without MU-ID or vpc_id flag"
          end

          # Check for tags matching the current deploy identifier on an elb or 
          # elb2 resource.
          # @param arn [String]: The ARN of the resource to check
          # @param region [String]: The cloud provider region
          # @param ignoremaster [Boolean]: Whether to ignore the MU-MASTER-IP tag
          # @param classic [Boolean]: Whether to look for a classic ELB instead of an ALB (ELB2)
          def self.checkForTagMatch(arn, region, ignoremaster, credentials, classic = false, deploy_id: MU.deploy_id)
            tags = []
            if classic
              tags = MU::Cloud::AWS.elb(credentials: credentials, region: region).describe_tags(
                load_balancer_names: [arn]
              ).tag_descriptions.first.tags
            else
              tags = MU::Cloud::AWS.elb2(credentials: credentials, region: region).describe_tags(
                resource_arns: [arn]
              ).tag_descriptions.first.tags
            end
            muid_match = false
            mumaster_match = false
            saw_tags = []
            if !tags.nil?
              tags.each { |tag|
                saw_tags << tag.key
                muid_match = true if tag.key == "MU-ID" and tag.value == deploy_id
                mumaster_match = true if tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
              }
            end
            if saw_tags.include?("MU-ID") and (saw_tags.include?("MU-MASTER-IP") or ignoremaster)
              if muid_match and (mumaster_match or ignoremaster)
                return true
              end
            end
            return false
          end


          resp = MU::Cloud::AWS.elb(credentials: credentials, region: region).describe_load_balancers
          resp2 = MU::Cloud::AWS.elb2(credentials: credentials, region: region).describe_load_balancers
          (resp.load_balancer_descriptions + resp2.load_balancers).each { |lb|
            classic = true
            if lb.class.name != "Aws::ElasticLoadBalancing::Types::LoadBalancerDescription" and !lb.type.nil? and lb.type == "application"
              classic = false
            end
            begin
              matched = false
              if flags and flags['vpc_id']
                matched = true if lb.vpc_id == flags['vpc_id']
              else
                if classic
                  matched = self.checkForTagMatch(lb.load_balancer_name, region, ignoremaster, credentials, classic, deploy_id: deploy_id)
                else
                  matched = self.checkForTagMatch(lb.load_balancer_arn, region, ignoremaster, credentials, classic, deploy_id: deploy_id)
                end
              end
              if matched
                if !MU::Cloud::AWS.isGovCloud?
                  MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: lb.load_balancer_name, target: lb.dns_name, cloudclass: MU::Cloud::LoadBalancer, delete: true) if !noop
                end
                if classic
                  MU.log "Removing Elastic Load Balancer #{lb.load_balancer_name}"
                  if !noop
                    MU::Cloud::AWS.elb(credentials: credentials, region: region).delete_load_balancer(load_balancer_name: lb.load_balancer_name)
                    stillhere = true
                    begin
                      ext_check = MU::Cloud::AWS.elb(credentials: credentials, region: region).describe_load_balancers(load_balancer_names: [lb.load_balancer_name])
                      if !ext_check or
                         !ext_check.load_balancer_descriptions or
                         !ext_check.load_balancer_descriptions[0]
                        sleep 3
                      else stillhere = false
                      end
                    end while stillhere
                  end
                else
                  MU.log "Removing Application Load Balancer #{lb.load_balancer_name}"
                  MU::Cloud::AWS.elb2(credentials: credentials, region: region).describe_listeners(
                    load_balancer_arn: lb.load_balancer_arn
                  ).listeners.each { |l|
                    MU.log "Removing ALB Listener #{l.listener_arn}"
                    MU::Cloud::AWS.elb2(credentials: credentials, region: region).delete_listener(
                      listener_arn: l.listener_arn
                    ) if !noop
                  }
                  tgs = MU::Cloud::AWS.elb2(credentials: credentials, region: region).describe_target_groups.target_groups
                  begin
                    if lb.state.code == "provisioning"
                      MU.log "Waiting for ALB #{lb.load_balancer_name} to leave 'provisioning' state", MU::NOTICE
                      sleep 45
                      lb = MU::Cloud::AWS.elb2(credentials: credentials, region: region).describe_load_balancers(
                        load_balancer_arns: [lb.load_balancer_arn]
                      ).load_balancers.first
                    end
                  end while lb.state.code == "provisioning"
                  MU::Cloud::AWS.elb2(credentials: credentials, region: region).delete_load_balancer(load_balancer_arn: lb.load_balancer_arn) if !noop


                  tgs.each { |tg|
                    if self.checkForTagMatch(tg.target_group_arn, region, ignoremaster, credentials, deploy_id: deploy_id)
                      MU.log "Removing Load Balancer Target Group #{tg.target_group_name}"
                      retries = 0
                      begin
                        MU::Cloud::AWS.elb2(credentials: credentials, region: region).delete_target_group(target_group_arn: tg.target_group_arn) if !noop
                      rescue Aws::ElasticLoadBalancingV2::Errors::ResourceInUse => e
                        if retries < 6
                          retries = retries + 1
                          sleep 10
                          retry
                        else
                          MU.log "Failed to delete ALB targetgroup #{tg.target_group_arn}: #{e.message}", MU::WARN
                        end
                      end
                    end
                  }
                end
                next
              end
            rescue Aws::ElasticLoadBalancing::Errors::LoadBalancerNotFound, Aws::ElasticLoadBalancingV2::Errors::LoadBalancerNotFound
              MU.log "ELB #{lb.load_balancer_name} already deleted", MU::WARN
            end
          }
          return nil
        end

        # Cloud-specific configuration properties.
        # @param _config [MU::Config]: The calling MU::Config object
        # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
        def self.schema(_config)
          toplevel_required = []
          schema = {
            "targetgroups" => {
              "items" => {
                "properties" => {
                  "proto" => {
                    "type" => "string",
                    "enum" => ["HTTP", "HTTPS", "TCP", "SSL"],
                  },
                  "target_type " => {
                    "type" => "string",
                    "enum" => ["instance", "ip", "lambda"],
                  }
                }
              }
            },
            "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema
          }
          [toplevel_required, schema]
        end

        # Cloud-specific pre-processing of {MU::Config::BasketofKittens::loadbalancers}, bare and unvalidated.
        # @param lb [Hash]: The resource to process and validate
        # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
        # @return [Boolean]: True if validation succeeded, False otherwise
        def self.validateConfig(lb, _configurator)
          ok = true

          # XXX what about raw targetgroup ssl declarations?
          lb['listeners'].each { |listener|
            if (!listener["ssl_certificate_name"].nil? and !listener["ssl_certificate_name"].empty?) or
               (!listener["ssl_certificate_id"].nil? and !listener["ssl_certificate_id"].empty?)
              if lb['cloud'] != "CloudFormation" # XXX or maybe do this anyway?
                begin
                  listener["ssl_certificate_id"] = MU::Cloud::AWS.findSSLCertificate(name: listener["ssl_certificate_name"].to_s, id: listener["ssl_certificate_id"].to_s, region: lb['region']).first
                rescue MuError
                  ok = false
                  next
                end
                MU.log "Using SSL cert #{listener["ssl_certificate_id"]} on port #{listener['lb_port']} in ELB #{lb['name']}"
              end
            end
          }

#          if lb["alarms"] && !lb["alarms"].empty?
#            lb["alarms"].each { |alarm|
#              alarm["name"] = "lb-#{lb["name"]}-#{alarm["name"]}"
#              alarm['dimensions'] = [] if !alarm['dimensions']
#              alarm['dimensions'] << { "name" => lb["name"], "cloud_class" => "LoadBalancerName" }
#              alarm["namespace"] = "AWS/ELB" if alarm["namespace"].nil?
#              alarm['cloud'] = lb['cloud']
#              alarms << alarm.dup
#            }
#          end

          if !lb["classic"]
            if lb["vpc"].nil?
              MU.log "LoadBalancer #{lb['name']} has no VPC configured. Either set 'classic' to true or configure a VPC.", MU::ERR
              ok = false
            end
          else
            lb.delete("targetgroups")
          end

          ok
        end

        # Locate an existing LoadBalancer or LoadBalancers and return an array containing matching AWS resource descriptors for those that match.
        # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching LoadBalancers
        def self.find(**args)
          args[:flags] ||= {}
          classic = args[:flags]['classic'] ? true : false

          matches = {}
          list = {}
          arn2name = {}
          resp = nil
          if args[:flags].has_key?('classic') 
            if args[:flags]['classic']
              resp = MU::Cloud::AWS.elb(region: args[:region], credentials: args[:credentials]).describe_load_balancers().load_balancer_descriptions
            else
              resp = MU::Cloud::AWS.elb2(region: args[:region], credentials: args[:credentials]).describe_load_balancers().load_balancers
            end
          elsif args[:cloud_id].nil? and args[:tag_value].nil?
            matches = find(region: args[:region], credentials: args[:credentials], flags: { "classic" => true } )
            matches.merge!(find(region: args[:region], credentials: args[:credentials], flags: { "classic" => false } ))
            return matches
          end

          resp.each { |lb|
            list[lb.load_balancer_name] = lb
            arn2name[lb.load_balancer_arn] = lb.load_balancer_name if !classic
            if !args[:cloud_id].nil? and lb.load_balancer_name == args[:cloud_id]
              matches[args[:cloud_id]] = lb
            end
          }

          return list if args[:tag_value].nil? and args[:cloud_id].nil?

          return matches if matches.size > 0

          if !args[:tag_key].nil? and !args[:tag_value].nil? and !args[:tag_key].empty? and list.size > 0
            tag_descriptions = nil
            if classic
              tag_descriptions = MU::Cloud::AWS.elb(region: args[:region], credentials: args[:credentials]).describe_tags(
                load_balancer_names: list.keys
              ).tag_descriptions
            else
              tag_descriptions = MU::Cloud::AWS.elb2(region: args[:region], credentials: args[:credentials]).describe_tags(
                resource_arns: list.values.map { |l| l.load_balancer_arn }
              ).tag_descriptions
            end
            if !resp.nil?
              tag_descriptions.each { |lb|
                lb_name = classic ? lb.load_balancer_name : arn2name[lb.resource_arn]
                lb.tags.each { |tag|
                  if tag.key == args[:tag_key] and tag.value == args[:tag_value]
                    matches[lb_name] = list[lb_name]
                  end
                }
              }
            end
          end

          return matches

        end

      end
    end
  end
end