Merge pull request #95 from HoustonPutman/addressability

Adding custom addressability options to the SolrCloud spec.
diff --git a/README.md b/README.md
index 63e431b..e028494 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,13 @@
 
 ## Version Compatibility & Upgrade Notes
 
+#### v0.2.6
+- The solr-operator argument `--ingressBaseDomain` has been **DEPRECATED**.
+In order to set the external baseDomain of your clouds, please begin to use `SolrCloud.spec.solrAddressability.external.domainName` instead.
+You will also need to set `SolrCloud.spec.solrAddressability.external.method` to `Ingress`.
+The `--ingressBaseDomain` argument is backwards compatible, and all existing SolrCloud objects will be auto-updated once your operator is upgraded to `v0.2.6`.
+The argument will be removed in a future version (`v0.3.0`).
+
 #### v0.2.4
 - The default supported version of the Zookeeper Operator has been upgraded to `v0.2.6`.  
 If you are using the provided zookeeper option for your SolrClouds, then you will want to upgrade your zookeeper operator version as well as the version and image of the zookeeper that you are running.
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index 94490be..eae9f30 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -18,6 +18,7 @@
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	zk "github.com/pravega/zookeeper-operator/pkg/apis/zookeeper/v1beta1"
@@ -101,6 +102,10 @@
 	// +optional
 	CustomSolrKubeOptions CustomSolrKubeOptions `json:"customSolrKubeOptions,omitempty"`
 
+	// Customize how Solr is addressed both internally and externally in Kubernetes.
+	// +optional
+	SolrAddressability SolrAddressabilityOptions `json:"solrAddressability,omitempty"`
+
 	// +optional
 	BusyBoxImage *ContainerImage `json:"busyBoxImage,omitempty"`
 
@@ -121,7 +126,7 @@
 	SolrGCTune string `json:"solrGCTune,omitempty"`
 }
 
-func (spec *SolrCloudSpec) withDefaults() (changed bool) {
+func (spec *SolrCloudSpec) withDefaults(ingressBaseDomain string) (changed bool) {
 	if spec.Replicas == nil {
 		changed = true
 		r := DefaultSolrReplicas
@@ -148,6 +153,8 @@
 		spec.SolrGCTune = DefaultSolrGCTune
 	}
 
+	changed = spec.SolrAddressability.withDefaults(ingressBaseDomain) || changed
+
 	if spec.ZookeeperRef == nil {
 		spec.ZookeeperRef = &ZookeeperRef{}
 	}
@@ -214,6 +221,147 @@
 	IngressOptions *IngressOptions `json:"ingressOptions,omitempty"`
 }
 
+type SolrAddressabilityOptions struct {
+	// External defines the way in which this SolrCloud nodes should be made addressable externally, from outside the Kubernetes cluster.
+	// If none is provided, the Solr Cloud will not be made addressable externally.
+	// +optional
+	External *ExternalAddressability `json:"external,omitempty"`
+
+	// PodPort defines the port to have the Solr Pod listen on.
+	// Defaults to 8983
+	// +optional
+	PodPort int `json:"podPort,omitempty"`
+
+	// CommonServicePort defines the port to have the common Solr service listen on.
+	// Defaults to 80
+	// +optional
+	CommonServicePort int `json:"commonServicePort,omitempty"`
+
+	// KubeDomain allows for the specification of an override of the default "cluster.local" Kubernetes cluster domain.
+	// Only use this option if the Kubernetes cluster has been setup with a custom domain.
+	// +optional
+	KubeDomain string `json:"kubeDomain,omitempty"`
+}
+
+func (opts *SolrAddressabilityOptions) withDefaults(ingressBaseDomain string) (changed bool) {
+	// DEPRECATED: ingressBaseDomain will be removed in v0.3.0
+	if opts.External == nil && ingressBaseDomain != "" {
+		changed = true
+		opts.External = &ExternalAddressability{
+			Method:             Ingress,
+			DomainName:         ingressBaseDomain,
+			UseExternalAddress: true,
+			NodePortOverride:   80,
+		}
+	} else if opts.External != nil {
+		changed = opts.External.withDefaults()
+	}
+	if opts.PodPort == 0 {
+		changed = true
+		opts.PodPort = 8983
+	}
+	if opts.CommonServicePort == 0 {
+		changed = true
+		opts.CommonServicePort = 80
+	}
+	return changed
+}
+
+// ExternalAddressability defines the config for making Solr services available externally to kubernetes.
+// Be careful when using LoadBalanced and includeNodes, as many IP addresses could be created if you are running many large solrClouds.
+type ExternalAddressability struct {
+	// The way in which this SolrCloud's service(s) should be made addressable externally.
+	Method ExternalAddressabilityMethod `json:"method"`
+
+	// Use the external address to advertise the SolrNode, defaults to false.
+	//
+	// If false, the external address will be available, however Solr (and clients using the CloudSolrClient in SolrJ) will only be aware of the internal URLs.
+	// If true, Solr will startup with the hostname of the external address.
+	//
+	// NOTE: This option cannot be true when hideNodes is set to true. So it will be auto-set to false if that is the case.
+	//
+	// Deprecation warning: When an ingress-base-domain is passed in to the operator, this value defaults to true.
+	// +optional
+	UseExternalAddress bool `json:"useExternalAddress"`
+
+	// Do not expose the common Solr service externally. This affects a single service.
+	// Defaults to false.
+	// +optional
+	HideCommon bool `json:"hideCommon,omitempty"`
+
+	// Do not expose each of the Solr Node services externally.
+	// The number of services this affects could range from 1 (a headless service for ExternalDNS) to the number of Solr pods your cloud contains (individual node services for Ingress/LoadBalancer).
+	// Defaults to false.
+	// +optional
+	HideNodes bool `json:"hideNodes,omitempty"`
+
+	// Override the domainName provided as startup parameters to the operator, used by ingresses and externalDNS.
+	// The common and/or node services will be addressable by unique names under the given domain.
+	// e.g. default-example-solrcloud.given.domain.name.com
+	//
+	// This options will be required for the Ingress and ExternalDNS methods once the ingressBaseDomain startup parameter is removed.
+	//
+	// For the LoadBalancer method, this field is optional and will only be used when useExternalAddress=true.
+	// If used with the LoadBalancer method, you will need DNS routing to the LoadBalancer IP address through the url template given above.
+	// +optional
+	DomainName string `json:"domainName,omitempty"`
+
+	// Provide additional domainNames that the Ingress or ExternalDNS should listen on.
+	// This option is ignored with the LoadBalancer method.
+	// +optional
+	AdditionalDomainNames []string `json:"additionalDomains,omitempty"`
+
+	// NodePortOverride defines the port to have all Solr node service(s) listen on and advertise itself as if advertising through an Ingress or LoadBalancer.
+	// This overrides the default usage of the podPort.
+	//
+	// This is option is only used when HideNodes=false, otherwise the the port each Solr Node will advertise itself with the podPort.
+	// This option is also unavailable with the ExternalDNS method.
+	//
+	// If using method=Ingress, your ingress controller is required to listen on this port.
+	// If your ingress controller is not listening on the podPort, then this option is required for solr to be addressable via an Ingress.
+	//
+	// Defaults to 80 if HideNodes=false and method=Ingress, otherwise this is optional.
+	// +optional
+	NodePortOverride int `json:"nodePortOverride,omitempty"`
+}
+
+// ExternalAddressability is a string enumeration type that enumerates
+// all possible ways that a SolrCloud can be made addressable external to the kubernetes cluster.
+// +kubebuilder:validation:Enum=Ingress;ExternalDNS
+type ExternalAddressabilityMethod string
+
+const (
+	// Use an ingress to make the Solr service(s) externally addressable
+	Ingress ExternalAddressabilityMethod = "Ingress"
+
+	// Use ExternalDNS to make the Solr service(s) externally addressable
+	ExternalDNS ExternalAddressabilityMethod = "ExternalDNS"
+
+	// Make Solr service(s) type:LoadBalancer to make them externally addressable
+	// NOTE: This option is not currently supported.
+	LoadBalancer ExternalAddressabilityMethod = "LoadBalancer"
+)
+
+func (opts *ExternalAddressability) withDefaults() (changed bool) {
+	// You can't use an externalAddress for Solr Nodes if the Nodes are hidden externally
+	if opts.UseExternalAddress && opts.HideNodes {
+		changed = true
+		opts.UseExternalAddress = false
+	}
+	// If the Ingress method is used, default the nodePortOverride to 80, since that is the port that most ingress controllers listen on.
+	if !opts.HideNodes && opts.Method == Ingress && opts.NodePortOverride == 0 {
+		changed = true
+		opts.NodePortOverride = 80
+	}
+	// If a headless service is used, aka not using individual node services, then a nodePortOverride is not allowed.
+	if !opts.UsesIndividualNodeServices() && opts.NodePortOverride > 0 {
+		changed = true
+		opts.NodePortOverride = 0
+	}
+
+	return changed
+}
+
 // DEPRECATED: Please use the options provided in SolrCloud.Spec.customSolrKubeOptions.podOptions
 //
 // SolrPodPolicy defines the common pod configuration for Pods, including when used
@@ -620,12 +768,16 @@
 }
 
 // WithDefaults set default values when not defined in the spec.
-func (sc *SolrCloud) WithDefaults() bool {
-	return sc.Spec.withDefaults()
+func (sc *SolrCloud) WithDefaults(ingressBaseDomain string) bool {
+	return sc.Spec.withDefaults(ingressBaseDomain)
 }
 
 func (sc *SolrCloud) GetAllSolrNodeNames() []string {
-	nodeNames := make([]string, *sc.Spec.Replicas)
+	replicas := 1
+	if sc.Spec.Replicas != nil {
+		replicas = int(*sc.Spec.Replicas)
+	}
+	nodeNames := make([]string, replicas)
 	statefulSetName := sc.StatefulSetName()
 	for i := range nodeNames {
 		nodeNames[i] = fmt.Sprintf("%s-%d", statefulSetName, i)
@@ -695,47 +847,139 @@
 	return zkInfo.InternalConnectionString + zkInfo.ChRoot
 }
 
-func (sc *SolrCloud) CommonIngressPrefix() string {
+// UsesHeadlessService returns whether the given solrCloud requires a headless service to be created for it.
+// solrCloud: SolrCloud instance
+func (sc *SolrCloud) UsesHeadlessService() bool {
+	return !sc.Spec.SolrAddressability.External.UsesIndividualNodeServices()
+}
+
+// UsesIndividualNodeServices returns whether the given solrCloud requires a individual node services to be created for it.
+// solrCloud: SolrCloud instance
+func (sc *SolrCloud) UsesIndividualNodeServices() bool {
+	return sc.Spec.SolrAddressability.External.UsesIndividualNodeServices()
+}
+
+func (extOpts *ExternalAddressability) UsesIndividualNodeServices() bool {
+	// LoadBalancer and Ingress will not work with headless services if each pod needs to be exposed externally.
+	return extOpts != nil && !extOpts.HideNodes && (extOpts.Method == Ingress || extOpts.Method == LoadBalancer)
+}
+
+func (sc *SolrCloud) CommonExternalPrefix() string {
 	return fmt.Sprintf("%s-%s-solrcloud", sc.Namespace, sc.Name)
 }
 
-func (sc *SolrCloud) CommonIngressUrl(ingressBaseUrl string) string {
-	return fmt.Sprintf("%s.%s", sc.CommonIngressPrefix(), ingressBaseUrl)
+func (sc *SolrCloud) CommonExternalUrl(domainName string) string {
+	return fmt.Sprintf("%s.%s", sc.CommonExternalPrefix(), domainName)
 }
 
 func (sc *SolrCloud) NodeIngressPrefix(nodeName string) string {
 	return fmt.Sprintf("%s-%s", sc.Namespace, nodeName)
 }
 
-func (sc *SolrCloud) NodeIngressUrl(nodeName string, ingressBaseUrl string) string {
-	return fmt.Sprintf("%s.%s", sc.NodeIngressPrefix(nodeName), ingressBaseUrl)
+func (sc *SolrCloud) ExternalDnsDomain(domainName string) string {
+	return fmt.Sprintf("%s.%s", sc.Namespace, domainName)
 }
 
-func (sc *SolrCloud) NodeHeadlessUrl(nodeName string, withPort bool) string {
-	url := fmt.Sprintf("%s.%s.%s", nodeName, sc.HeadlessServiceName(), sc.Namespace)
+func (sc *SolrCloud) customKubeDomain() string {
+	if sc.Spec.SolrAddressability.KubeDomain != "" {
+		return ".svc." + sc.Spec.SolrAddressability.KubeDomain
+	} else {
+		return ""
+	}
+}
+
+func (sc *SolrCloud) NodeHeadlessUrl(nodeName string, withPort bool) (url string) {
+	url = fmt.Sprintf("%s.%s.%s", nodeName, sc.HeadlessServiceName(), sc.Namespace) + sc.customKubeDomain()
 	if withPort {
-		url += ":8983"
+		url += sc.NodePortSuffix()
 	}
 	return url
 }
 
-func (sc *SolrCloud) NodeServiceUrl(nodeName string) string {
-	return fmt.Sprintf("%s.%s", nodeName, sc.Namespace)
+func (sc *SolrCloud) NodeServiceUrl(nodeName string, withPort bool) (url string) {
+	url = fmt.Sprintf("%s.%s", nodeName, sc.Namespace) + sc.customKubeDomain()
+	if withPort {
+		url += sc.NodePortSuffix()
+	}
+	return url
 }
 
-func (sc *SolrCloud) InternalNodeUrl(nodeName string, useHeadlessService bool, withPort bool) string {
-	if useHeadlessService {
+func (sc *SolrCloud) CommonPortSuffix() string {
+	return PortToSuffix(sc.Spec.SolrAddressability.CommonServicePort)
+}
+
+func (sc *SolrCloud) NodePortSuffix() string {
+	return PortToSuffix(sc.NodePort())
+}
+
+func (sc *SolrCloud) NodePort() int {
+	port := sc.Spec.SolrAddressability.PodPort
+	external := sc.Spec.SolrAddressability.External
+	// The nodePort is different than the podPort ONLY if the nodes are exposed externally and a nodePortOverride has been set.
+	if external.UsesIndividualNodeServices() && external.NodePortOverride > 0 {
+		port = sc.Spec.SolrAddressability.External.NodePortOverride
+	}
+	return port
+}
+
+// PortToSuffix returns the url suffix for a port.
+// Port 80 does not require a suffix, as it is the default port for HTTP.
+func PortToSuffix(port int) string {
+	if port == 80 {
+		return ""
+	}
+	return ":" + strconv.Itoa(port)
+}
+
+func (sc *SolrCloud) InternalNodeUrl(nodeName string, withPort bool) string {
+	if sc.UsesHeadlessService() {
 		return sc.NodeHeadlessUrl(nodeName, withPort)
+	} else if sc.UsesIndividualNodeServices() {
+		return sc.NodeServiceUrl(nodeName, withPort)
 	} else {
-		return sc.NodeServiceUrl(nodeName)
+		return ""
 	}
 }
 
-func (sc *SolrCloud) ExternalNodeUrl(nodeName string, ingressBaseDomain string, withPort bool) string {
-	if ingressBaseDomain == "" {
-		return sc.NodeHeadlessUrl(nodeName, withPort)
+func (sc *SolrCloud) InternalCommonUrl(withPort bool) (url string) {
+	url = fmt.Sprintf("%s.%s", sc.CommonServiceName(), sc.Namespace) + sc.customKubeDomain()
+	if withPort {
+		url += sc.NodePortSuffix()
+	}
+	return url
+}
+
+func (sc *SolrCloud) ExternalNodeUrl(nodeName string, domainName string, withPort bool) (url string) {
+	if sc.Spec.SolrAddressability.External.Method == Ingress {
+		url = fmt.Sprintf("%s.%s", sc.NodeIngressPrefix(nodeName), domainName)
+	} else if sc.Spec.SolrAddressability.External.Method == ExternalDNS {
+		url = fmt.Sprintf("%s.%s", nodeName, sc.ExternalDnsDomain(domainName))
+	}
+	// TODO: Add LoadBalancer stuff here
+	if withPort {
+		url += sc.NodePortSuffix()
+	}
+	return url
+}
+
+func (sc *SolrCloud) ExternalCommonUrl(domainName string, withPort bool) (url string) {
+	if sc.Spec.SolrAddressability.External.Method == Ingress {
+		url = fmt.Sprintf("%s.%s", sc.CommonExternalPrefix(), domainName)
+	} else if sc.Spec.SolrAddressability.External.Method == ExternalDNS {
+		url = fmt.Sprintf("%s.%s", sc.CommonServiceName(), sc.ExternalDnsDomain(domainName))
+	}
+	if withPort {
+		url += sc.CommonPortSuffix()
+	}
+	return url
+}
+
+func (sc *SolrCloud) AdvertisedNodeHost(nodeName string) string {
+	external := sc.Spec.SolrAddressability.External
+	if external != nil && external.UseExternalAddress {
+		return sc.ExternalNodeUrl(nodeName, sc.Spec.SolrAddressability.External.DomainName, false)
 	} else {
-		return sc.NodeIngressUrl(nodeName, ingressBaseDomain)
+		return sc.InternalNodeUrl(nodeName, false)
 	}
 }
 
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index df79e33..2dfe0bd 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -310,6 +310,26 @@
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalAddressability) DeepCopyInto(out *ExternalAddressability) {
+	*out = *in
+	if in.AdditionalDomainNames != nil {
+		in, out := &in.AdditionalDomainNames, &out.AdditionalDomainNames
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAddressability.
+func (in *ExternalAddressability) DeepCopy() *ExternalAddressability {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalAddressability)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *FullZetcdSpec) DeepCopyInto(out *FullZetcdSpec) {
 	*out = *in
 	if in.EtcdSpec != nil {
@@ -548,6 +568,26 @@
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SolrAddressabilityOptions) DeepCopyInto(out *SolrAddressabilityOptions) {
+	*out = *in
+	if in.External != nil {
+		in, out := &in.External, &out.External
+		*out = new(ExternalAddressability)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SolrAddressabilityOptions.
+func (in *SolrAddressabilityOptions) DeepCopy() *SolrAddressabilityOptions {
+	if in == nil {
+		return nil
+	}
+	out := new(SolrAddressabilityOptions)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SolrBackup) DeepCopyInto(out *SolrBackup) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -768,6 +808,7 @@
 		(*in).DeepCopyInto(*out)
 	}
 	in.CustomSolrKubeOptions.DeepCopyInto(&out.CustomSolrKubeOptions)
+	in.SolrAddressability.DeepCopyInto(&out.SolrAddressability)
 	if in.BusyBoxImage != nil {
 		in, out := &in.BusyBoxImage, &out.BusyBoxImage
 		*out = new(ContainerImage)
diff --git a/config/crd/bases/solr.bloomberg.com_solrclouds.yaml b/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
index 743d234..8ae390e 100644
--- a/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
+++ b/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
@@ -3766,6 +3766,96 @@
               description: The number of solr nodes to run
               format: int32
               type: integer
+            solrAddressability:
+              description: Customize how Solr is addressed both internally and externally
+                in Kubernetes.
+              properties:
+                commonServicePort:
+                  description: CommonServicePort defines the port to have the common
+                    Solr service listen on. Defaults to 80
+                  type: integer
+                external:
+                  description: External defines the way in which this SolrCloud nodes
+                    should be made addressable externally, from outside the Kubernetes
+                    cluster. If none is provided, the Solr Cloud will not be made
+                    addressable externally.
+                  properties:
+                    additionalDomains:
+                      description: Provide additional domainNames that the Ingress
+                        or ExternalDNS should listen on. This option is ignored with
+                        the LoadBalancer method.
+                      items:
+                        type: string
+                      type: array
+                    domainName:
+                      description: "Override the domainName provided as startup parameters
+                        to the operator, used by ingresses and externalDNS. The common
+                        and/or node services will be addressable by unique names under
+                        the given domain. e.g. default-example-solrcloud.given.domain.name.com
+                        \n This options will be required for the Ingress and ExternalDNS
+                        methods once the ingressBaseDomain startup parameter is removed.
+                        \n For the LoadBalancer method, this field is optional and
+                        will only be used when useExternalAddress=true. If used with
+                        the LoadBalancer method, you will need DNS routing to the
+                        LoadBalancer IP address through the url template given above."
+                      type: string
+                    hideCommon:
+                      description: Do not expose the common Solr service externally.
+                        This affects a single service. Defaults to false.
+                      type: boolean
+                    hideNodes:
+                      description: Do not expose each of the Solr Node services externally.
+                        The number of services this affects could range from 1 (a
+                        headless service for ExternalDNS) to the number of Solr pods
+                        your cloud contains (individual node services for Ingress/LoadBalancer).
+                        Defaults to false.
+                      type: boolean
+                    method:
+                      description: The way in which this SolrCloud's service(s) should
+                        be made addressable externally.
+                      enum:
+                      - Ingress
+                      - ExternalDNS
+                      type: string
+                    nodePortOverride:
+                      description: "NodePortOverride defines the port to have all
+                        Solr node service(s) listen on and advertise itself as if
+                        advertising through an Ingress or LoadBalancer. This overrides
+                        the default usage of the podPort. \n This is option is only
+                        used when HideNodes=false, otherwise the the port each Solr
+                        Node will advertise itself with the podPort. This option is
+                        also unavailable with the ExternalDNS method. \n If using
+                        method=Ingress, your ingress controller is required to listen
+                        on this port. If your ingress controller is not listening
+                        on the podPort, then this option is required for solr to be
+                        addressable via an Ingress. \n Defaults to 80 if HideNodes=false
+                        and method=Ingress, otherwise this is optional."
+                      type: integer
+                    useExternalAddress:
+                      description: "Use the external address to advertise the SolrNode,
+                        defaults to false. \n If false, the external address will
+                        be available, however Solr (and clients using the CloudSolrClient
+                        in SolrJ) will only be aware of the internal URLs. If true,
+                        Solr will startup with the hostname of the external address.
+                        \n NOTE: This option cannot be true when hideNodes is set
+                        to true. So it will be auto-set to false if that is the case.
+                        \n Deprecation warning: When an ingress-base-domain is passed
+                        in to the operator, this value defaults to true."
+                      type: boolean
+                  required:
+                  - method
+                  type: object
+                kubeDomain:
+                  description: KubeDomain allows for the specification of an override
+                    of the default "cluster.local" Kubernetes cluster domain. Only
+                    use this option if the Kubernetes cluster has been setup with
+                    a custom domain.
+                  type: string
+                podPort:
+                  description: PodPort defines the port to have the Solr Pod listen
+                    on. Defaults to 8983
+                  type: integer
+              type: object
             solrGCTune:
               description: Set GC Tuning configuration through GC_TUNE environment
                 variable
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
index fb6f089..f1d0edf 100644
--- a/config/manager/manager.yaml
+++ b/config/manager/manager.yaml
@@ -20,7 +20,6 @@
       - args:
         - -zk-operator=true
         - -etcd-operator=false
-        - -ingress-base-domain=ing.local.domain
         image: bloomberg/solr-operator:latest
         imagePullPolicy: IfNotPresent
         name: solr-operator
diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go
index c41c7b8..9a16c1c 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -35,6 +35,12 @@
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
+func emptyRequests(requests chan reconcile.Request) {
+	for len(requests) > 0 {
+		<-requests
+	}
+}
+
 func expectStatefulSet(t *testing.T, g *gomega.GomegaWithT, requests chan reconcile.Request, expectedRequest reconcile.Request, statefulSetKey types.NamespacedName) *appsv1.StatefulSet {
 	stateful := &appsv1.StatefulSet{}
 	g.Eventually(func() error { return testClient.Get(context.TODO(), statefulSetKey, stateful) }, timeout).
@@ -83,6 +89,12 @@
 	return service
 }
 
+func expectNoService(g *gomega.GomegaWithT, serviceKey types.NamespacedName, message string) {
+	service := &corev1.Service{}
+	g.Eventually(func() error { return testClient.Get(context.TODO(), serviceKey, service) }, timeout).
+		Should(gomega.MatchError("Service \""+serviceKey.Name+"\" not found"), message)
+}
+
 func expectIngress(g *gomega.GomegaWithT, requests chan reconcile.Request, expectedRequest reconcile.Request, ingressKey types.NamespacedName) *extv1.Ingress {
 	ingress := &extv1.Ingress{}
 	g.Eventually(func() error { return testClient.Get(context.TODO(), ingressKey, ingress) }, timeout).
@@ -101,7 +113,7 @@
 	return ingress
 }
 
-func expectNoIngress(g *gomega.GomegaWithT, requests chan reconcile.Request, ingressKey types.NamespacedName) {
+func expectNoIngress(g *gomega.GomegaWithT, ingressKey types.NamespacedName) {
 	ingress := &extv1.Ingress{}
 	g.Eventually(func() error { return testClient.Get(context.TODO(), ingressKey, ingress) }, timeout).
 		Should(gomega.MatchError("Ingress.extensions \"" + ingressKey.Name + "\" not found"))
@@ -130,7 +142,7 @@
 	return configMap
 }
 
-func expectNoConfigMap(g *gomega.GomegaWithT, requests chan reconcile.Request, configMapKey types.NamespacedName) {
+func expectNoConfigMap(g *gomega.GomegaWithT, configMapKey types.NamespacedName) {
 	configMap := &corev1.ConfigMap{}
 	g.Eventually(func() error { return testClient.Get(context.TODO(), configMapKey, configMap) }, timeout).
 		Should(gomega.MatchError("ConfigMap \"" + configMapKey.Name + "\" not found"))
@@ -229,6 +241,12 @@
 }
 
 var (
+	testKubeDomain        = "kube.domain.com"
+	testDomain            = "test.domain.com"
+	testAdditionalDomains = []string{
+		"test1.domain.com",
+		"test2.domain.com",
+	}
 	testPodAnnotations = map[string]string{
 		"testP1": "valueP1",
 		"testP2": "valueP2",
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index 7d89691..c2ed761 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -100,7 +100,7 @@
 		return reconcile.Result{}, err
 	}
 
-	changed := instance.WithDefaults()
+	changed := instance.WithDefaults(IngressBaseUrl)
 	if changed {
 		r.Log.Info("Setting default settings for solr-cloud", "namespace", instance.Namespace, "name", instance.Name)
 		if err := r.Update(context.TODO(), instance); err != nil {
@@ -140,7 +140,6 @@
 			r.Log.Info("Updating Common Service", "namespace", commonService.Namespace, "name", commonService.Name)
 			err = r.Update(context.TODO(), foundCommonService)
 		}
-		newStatus.InternalCommonAddress = "http://" + foundCommonService.Name + "." + foundCommonService.Namespace
 	} else {
 		return requeueOrNot, err
 	}
@@ -149,40 +148,45 @@
 
 	hostNameIpMap := make(map[string]string)
 	// Generate a service for every Node
-	for _, nodeName := range solrNodeNames {
-		err, ip := reconcileNodeService(r, instance, nodeName)
-		if err != nil {
-			return requeueOrNot, err
-		}
-		if IngressBaseUrl != "" {
-			if ip == "" {
-				// If we are using this IP in the hostAliases of the statefulSet, it needs to be set for every service before trying to update the statefulSet
-				blockReconciliationOfStatefulSet = true
-			} else {
-				hostNameIpMap[instance.NodeIngressUrl(nodeName, IngressBaseUrl)] = ip
+	if instance.UsesIndividualNodeServices() {
+		for _, nodeName := range solrNodeNames {
+			err, ip := reconcileNodeService(r, instance, nodeName)
+			if err != nil {
+				return requeueOrNot, err
+			}
+			// This IP Address only needs to be used in the hostname map if the SolrCloud is advertising the external address.
+			if instance.Spec.SolrAddressability.External.UseExternalAddress {
+				if ip == "" {
+					// If we are using this IP in the hostAliases of the statefulSet, it needs to be set for every service before trying to update the statefulSet
+					blockReconciliationOfStatefulSet = true
+				} else {
+					hostNameIpMap[instance.AdvertisedNodeHost(nodeName)] = ip
+				}
 			}
 		}
 	}
 
 	// Generate HeadlessService
-	headless := util.GenerateHeadlessService(instance)
-	if err := controllerutil.SetControllerReference(instance, headless, r.scheme); err != nil {
-		return requeueOrNot, err
-	}
+	if instance.UsesHeadlessService() {
+		headless := util.GenerateHeadlessService(instance)
+		if err := controllerutil.SetControllerReference(instance, headless, r.scheme); err != nil {
+			return requeueOrNot, err
+		}
 
-	// Check if the HeadlessService already exists
-	foundHeadless := &corev1.Service{}
-	err = r.Get(context.TODO(), types.NamespacedName{Name: headless.Name, Namespace: headless.Namespace}, foundHeadless)
-	if err != nil && errors.IsNotFound(err) {
-		r.Log.Info("Creating HeadlessService", "namespace", headless.Namespace, "name", headless.Name)
-		err = r.Create(context.TODO(), headless)
-	} else if err == nil && util.CopyServiceFields(headless, foundHeadless) {
-		// Update the found HeadlessService and write the result back if there are any changes
-		r.Log.Info("Updating HeadlessService", "namespace", headless.Namespace, "name", headless.Name)
-		err = r.Update(context.TODO(), foundHeadless)
-	}
-	if err != nil {
-		return requeueOrNot, err
+		// Check if the HeadlessService already exists
+		foundHeadless := &corev1.Service{}
+		err = r.Get(context.TODO(), types.NamespacedName{Name: headless.Name, Namespace: headless.Namespace}, foundHeadless)
+		if err != nil && errors.IsNotFound(err) {
+			r.Log.Info("Creating HeadlessService", "namespace", headless.Namespace, "name", headless.Name)
+			err = r.Create(context.TODO(), headless)
+		} else if err == nil && util.CopyServiceFields(headless, foundHeadless) {
+			// Update the found HeadlessService and write the result back if there are any changes
+			r.Log.Info("Updating HeadlessService", "namespace", headless.Namespace, "name", headless.Name)
+			err = r.Update(context.TODO(), foundHeadless)
+		}
+		if err != nil {
+			return requeueOrNot, err
+		}
 	}
 
 	// Generate ConfigMap
@@ -213,7 +217,7 @@
 
 	if !blockReconciliationOfStatefulSet {
 		// Generate StatefulSet
-		statefulSet := util.GenerateStatefulSet(instance, &newStatus, IngressBaseUrl, hostNameIpMap)
+		statefulSet := util.GenerateStatefulSet(instance, &newStatus, hostNameIpMap)
 		if err := controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err != nil {
 			return requeueOrNot, err
 		}
@@ -260,9 +264,10 @@
 		return requeueOrNot, err
 	}
 
-	if IngressBaseUrl != "" {
+	extAddressabilityOpts := instance.Spec.SolrAddressability.External
+	if extAddressabilityOpts != nil && extAddressabilityOpts.Method == solr.Ingress {
 		// Generate Ingress
-		ingress := util.GenerateCommonIngress(instance, solrNodeNames, IngressBaseUrl)
+		ingress := util.GenerateIngress(instance, solrNodeNames, IngressBaseUrl)
 		if err := controllerutil.SetControllerReference(instance, ingress, r.scheme); err != nil {
 			return requeueOrNot, err
 		}
@@ -280,9 +285,6 @@
 		}
 		if err != nil {
 			return requeueOrNot, err
-		} else {
-			address := "http://" + instance.CommonIngressUrl(IngressBaseUrl)
-			newStatus.ExternalCommonAddress = &address
 		}
 	}
 
@@ -323,9 +325,9 @@
 		nodeStatus := solr.SolrNodeStatus{}
 		nodeStatus.Name = p.Name
 		nodeStatus.NodeName = p.Spec.NodeName
-		nodeStatus.InternalAddress = "http://" + solrCloud.InternalNodeUrl(nodeStatus.NodeName, IngressBaseUrl == "", true)
-		if IngressBaseUrl != "" {
-			nodeStatus.ExternalAddress = "http://" + solrCloud.NodeIngressUrl(nodeStatus.Name, IngressBaseUrl)
+		nodeStatus.InternalAddress = "http://" + solrCloud.InternalNodeUrl(nodeStatus.NodeName, true)
+		if solrCloud.Spec.SolrAddressability.External != nil {
+			nodeStatus.ExternalAddress = "http://" + solrCloud.ExternalNodeUrl(nodeStatus.Name, solrCloud.Spec.SolrAddressability.External.DomainName, true)
 		}
 		ready := false
 		if len(p.Status.ContainerStatuses) > 0 {
@@ -373,6 +375,12 @@
 		newStatus.Version = solrCloud.Spec.SolrImage.Tag
 	}
 
+	newStatus.InternalCommonAddress = "http://" + solrCloud.InternalCommonUrl(true)
+	if solrCloud.Spec.SolrAddressability.External != nil {
+		extAddress := "http://" + solrCloud.ExternalCommonUrl(solrCloud.Spec.SolrAddressability.External.DomainName, true)
+		newStatus.ExternalCommonAddress = &extAddress
+	}
+
 	return nil
 }
 
diff --git a/controllers/solrcloud_controller_externaldns_test.go b/controllers/solrcloud_controller_externaldns_test.go
new file mode 100644
index 0000000..68e4e83
--- /dev/null
+++ b/controllers/solrcloud_controller_externaldns_test.go
@@ -0,0 +1,697 @@
+/*
+Copyright 2019 Bloomberg Finance LP.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+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.
+*/
+
+package controllers
+
+import (
+	"k8s.io/apimachinery/pkg/types"
+	"testing"
+
+	"github.com/bloomberg/solr-operator/controllers/util"
+	"github.com/stretchr/testify/assert"
+
+	solr "github.com/bloomberg/solr-operator/api/v1beta1"
+	"github.com/onsi/gomega"
+	"golang.org/x/net/context"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+var _ reconcile.Reconciler = &SolrCloudReconciler{}
+
+func TestEDSCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.ExternalDNS,
+					UseExternalAddress: true,
+					DomainName:         testDomain,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + instance.Namespace + "." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=3000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	expectedCommonServiceAnnotations := util.MergeLabelsOrAnnotations(testCommonServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "common service annotations", expectedCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	expectedHeadlessServiceAnnotations := util.MergeLabelsOrAnnotations(testHeadlessServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "headless service annotations", expectedHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 3000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
+
+func TestEDSNoNodesCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.ExternalDNS,
+					UseExternalAddress: true,
+					HideNodes:          true,
+					DomainName:         testDomain,
+				},
+				PodPort:           2000,
+				CommonServicePort: 5000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + instance.Namespace,
+		"SOLR_PORT": "2000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=2000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	expectedCommonServiceAnnotations := util.MergeLabelsOrAnnotations(testCommonServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "common service annotations", expectedCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 5000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "headless service annotations", testHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 2000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
+
+func TestEDSNoCommonCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.ExternalDNS,
+					UseExternalAddress: true,
+					HideCommon:         true,
+					DomainName:         testDomain,
+				},
+				PodPort:           3000,
+				CommonServicePort: 2000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + instance.Namespace + "." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=3000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 2000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	expectedHeadlessServiceAnnotations := util.MergeLabelsOrAnnotations(testHeadlessServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "headless service annotations", expectedHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 3000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
+
+func TestEDSUseInternalAddressCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.ExternalDNS,
+					UseExternalAddress: false,
+					DomainName:         testDomain,
+					NodePortOverride:   454,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + instance.Namespace,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=3000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	expectedCommonServiceAnnotations := util.MergeLabelsOrAnnotations(testCommonServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "common service annotations", expectedCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	expectedHeadlessServiceAnnotations := util.MergeLabelsOrAnnotations(testHeadlessServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "headless service annotations", expectedHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 3000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
+
+func TestEDSExtraDomainsCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:                solr.ExternalDNS,
+					UseExternalAddress:    true,
+					DomainName:            testDomain,
+					AdditionalDomainNames: testAdditionalDomains,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + instance.Namespace + "." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=3000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	hostnameAnnotation := instance.Namespace + "." + testDomain
+	for _, domain := range testAdditionalDomains {
+		hostnameAnnotation += "," + instance.Namespace + "." + domain
+	}
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	expectedCommonServiceAnnotations := util.MergeLabelsOrAnnotations(testCommonServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": hostnameAnnotation,
+	})
+	testMapsEqual(t, "common service annotations", expectedCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	expectedHeadlessServiceAnnotations := util.MergeLabelsOrAnnotations(testHeadlessServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": hostnameAnnotation,
+	})
+	testMapsEqual(t, "headless service annotations", expectedHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 3000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
+
+func TestEDSKubeDomainCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.ExternalDNS,
+					UseExternalAddress: true,
+					HideNodes:          true,
+					DomainName:         testDomain,
+				},
+				PodPort:           2000,
+				CommonServicePort: 5000,
+				KubeDomain:        testKubeDomain,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + instance.Namespace + ".svc." + testKubeDomain,
+		"SOLR_PORT": "2000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=2000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	expectedCommonServiceAnnotations := util.MergeLabelsOrAnnotations(testCommonServiceAnnotations, map[string]string{
+		"external-dns.alpha.kubernetes.io/hostname": instance.Namespace + "." + testDomain,
+	})
+	testMapsEqual(t, "common service annotations", expectedCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 5000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "headless service annotations", testHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 2000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
diff --git a/controllers/solrcloud_controller_ingress_test.go b/controllers/solrcloud_controller_ingress_test.go
new file mode 100644
index 0000000..dacea92
--- /dev/null
+++ b/controllers/solrcloud_controller_ingress_test.go
@@ -0,0 +1,818 @@
+/*
+Copyright 2019 Bloomberg Finance LP.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+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.
+*/
+
+package controllers
+
+import (
+	extv1 "k8s.io/api/extensions/v1beta1"
+	"k8s.io/apimachinery/pkg/types"
+	"strconv"
+	"testing"
+
+	"github.com/bloomberg/solr-operator/controllers/util"
+	"github.com/stretchr/testify/assert"
+
+	solr "github.com/bloomberg/solr-operator/api/v1beta1"
+	"github.com/onsi/gomega"
+	"golang.org/x/net/context"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+var _ reconcile.Reconciler = &SolrCloudReconciler{}
+
+func TestIngressCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.Ingress,
+					UseExternalAddress: true,
+					DomainName:         testDomain,
+					NodePortOverride:   100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.EqualValues(t, replicas, len(statefulSet.Spec.Template.Spec.HostAliases), "Since external address is used for advertising, host aliases should be used for every node.")
+	for i, hostAlias := range statefulSet.Spec.Template.Spec.HostAliases {
+		assert.EqualValues(t, cloudSsKey.Namespace+"-"+statefulSet.Name+"-"+strconv.Itoa(i)+"."+testDomain, hostAlias.Hostnames[0], "Since external address is used for advertising, host aliases should be used for every node.")
+	}
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": instance.Namespace + "-$(POD_HOSTNAME)." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=100"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
+
+	// Check on individual node services
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		service := expectService(t, g, requests, expectedCloudRequest, nodeSKey, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}))
+		expectedNodeServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "external"})
+		testMapsEqual(t, "node '"+nodeName+"' service labels", util.MergeLabelsOrAnnotations(expectedNodeServiceLabels, testNodeServiceLabels), service.Labels)
+		testMapsEqual(t, "node '"+nodeName+"' service annotations", testNodeServiceAnnotations, service.Annotations)
+		assert.EqualValues(t, 100, service.Spec.Ports[0].Port, "Wrong port on node Service")
+		assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on node Service")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, true, int(replicas), []string{testDomain})
+}
+
+func TestIngressNoNodesCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.Ingress,
+					UseExternalAddress: true,
+					HideNodes:          true,
+					DomainName:         testDomain,
+					NodePortOverride:   100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "Since external address is not used, there is no need for host aliases.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + cloudHsKey.Namespace,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=3000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "headless service annotations", nil, service.Annotations)
+	assert.EqualValues(t, 3000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, true, 0, []string{testDomain})
+}
+
+func TestIngressNoCommonCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.Ingress,
+					UseExternalAddress: true,
+					HideCommon:         true,
+					DomainName:         testDomain,
+					NodePortOverride:   100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.EqualValues(t, replicas, len(statefulSet.Spec.Template.Spec.HostAliases), "Since external address is used for advertising, host aliases should be used for every node.")
+	for i, hostAlias := range statefulSet.Spec.Template.Spec.HostAliases {
+		assert.EqualValues(t, cloudSsKey.Namespace+"-"+statefulSet.Name+"-"+strconv.Itoa(i)+"."+testDomain, hostAlias.Hostnames[0], "Since external address is used for advertising, host aliases should be used for every node.")
+	}
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": instance.Namespace + "-$(POD_HOSTNAME)." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=100"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
+
+	// Check on individual node services
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		service := expectService(t, g, requests, expectedCloudRequest, nodeSKey, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}))
+		expectedNodeServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "external"})
+		testMapsEqual(t, "node '"+nodeName+"' service labels", util.MergeLabelsOrAnnotations(expectedNodeServiceLabels, testNodeServiceLabels), service.Labels)
+		testMapsEqual(t, "node '"+nodeName+"' service annotations", testNodeServiceAnnotations, service.Annotations)
+		assert.EqualValues(t, 100, service.Spec.Ports[0].Port, "Wrong port on node Service")
+		assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on node Service")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, false, int(replicas), []string{testDomain})
+}
+
+func TestIngressUseInternalAddressCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.Ingress,
+					UseExternalAddress: false,
+					DomainName:         testDomain,
+					NodePortOverride:   100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "Since external address is not used, there is no need for host aliases.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + expectedCloudRequest.Namespace,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=100"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
+
+	// Check on individual node services
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		service := expectService(t, g, requests, expectedCloudRequest, nodeSKey, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}))
+		expectedNodeServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "external"})
+		testMapsEqual(t, "node '"+nodeName+"' service labels", util.MergeLabelsOrAnnotations(expectedNodeServiceLabels, testNodeServiceLabels), service.Labels)
+		testMapsEqual(t, "node '"+nodeName+"' service annotations", testNodeServiceAnnotations, service.Annotations)
+		assert.EqualValues(t, 100, service.Spec.Ports[0].Port, "Wrong port on node Service")
+		assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on node Service")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, true, int(replicas), []string{testDomain})
+}
+
+func TestIngressExtraDomainsCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:                solr.Ingress,
+					UseExternalAddress:    true,
+					DomainName:            testDomain,
+					AdditionalDomainNames: testAdditionalDomains,
+					NodePortOverride:      100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.EqualValues(t, replicas, len(statefulSet.Spec.Template.Spec.HostAliases), "Since external address is used for advertising, host aliases should be used for every node.")
+	for i, hostAlias := range statefulSet.Spec.Template.Spec.HostAliases {
+		assert.EqualValues(t, cloudSsKey.Namespace+"-"+statefulSet.Name+"-"+strconv.Itoa(i)+"."+testDomain, hostAlias.Hostnames[0], "Since external address is used for advertising, host aliases should be used for every node.")
+	}
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": instance.Namespace + "-$(POD_HOSTNAME)." + testDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=100"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
+
+	// Check on individual node services
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		service := expectService(t, g, requests, expectedCloudRequest, nodeSKey, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}))
+		expectedNodeServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "external"})
+		testMapsEqual(t, "node '"+nodeName+"' service labels", util.MergeLabelsOrAnnotations(expectedNodeServiceLabels, testNodeServiceLabels), service.Labels)
+		testMapsEqual(t, "node '"+nodeName+"' service annotations", testNodeServiceAnnotations, service.Annotations)
+		assert.EqualValues(t, 100, service.Spec.Ports[0].Port, "Wrong port on node Service")
+		assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on node Service")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, true, int(replicas), append([]string{testDomain}, testAdditionalDomains...))
+}
+
+func TestIngressKubeDomainCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	replicas := int32(4)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			Replicas: &replicas,
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				External: &solr.ExternalAddressability{
+					Method:             solr.Ingress,
+					UseExternalAddress: false,
+					DomainName:         testDomain,
+					NodePortOverride:   100,
+				},
+				PodPort:           3000,
+				CommonServicePort: 4000,
+				KubeDomain:        testKubeDomain,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				IngressOptions: &solr.IngressOptions{
+					Annotations: testIngressAnnotations,
+					Labels:      testIngressLabels,
+				},
+				NodeServiceOptions: &solr.ServiceOptions{
+					Annotations: testNodeServiceAnnotations,
+					Labels:      testNodeServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "Since external address is not used, there is no need for host aliases.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + expectedCloudRequest.Namespace + ".svc." + testKubeDomain,
+		"SOLR_PORT": "3000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=100"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments (Solr advertising port)")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 4000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
+
+	// Check on individual node services
+	nodeNames := instance.GetAllSolrNodeNames()
+	assert.EqualValues(t, replicas, len(nodeNames), "SolrCloud has incorrect number of nodeNames.")
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		service := expectService(t, g, requests, expectedCloudRequest, nodeSKey, util.MergeLabelsOrAnnotations(statefulSet.Spec.Selector.MatchLabels, map[string]string{"statefulset.kubernetes.io/pod-name": nodeName}))
+		expectedNodeServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "external"})
+		testMapsEqual(t, "node '"+nodeName+"' service labels", util.MergeLabelsOrAnnotations(expectedNodeServiceLabels, testNodeServiceLabels), service.Labels)
+		testMapsEqual(t, "node '"+nodeName+"' service annotations", testNodeServiceAnnotations, service.Annotations)
+		assert.EqualValues(t, 100, service.Spec.Ports[0].Port, "Wrong port on node Service")
+		assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on node Service")
+	}
+
+	// Check the ingress
+	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	testMapsEqual(t, "ingress labels", util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), testIngressLabels), ingress.Labels)
+	testMapsEqual(t, "ingress annotations", testIngressAnnotations, ingress.Annotations)
+	testIngressRules(t, ingress, true, int(replicas), []string{testDomain})
+}
+
+func testIngressRules(t *testing.T, ingress *extv1.Ingress, withCommon bool, withNodes int, domainNames []string) {
+	expected := 0
+	if withCommon {
+		expected += 1
+	}
+	if withNodes > 0 {
+		expected += withNodes
+	}
+	perDomain := expected
+	numDomains := len(domainNames)
+	expected *= numDomains
+	assert.EqualValues(t, expected, len(ingress.Spec.Rules), "Wrong number of ingress rules.")
+	for i := 0; i < perDomain; i++ {
+		// Common Rules
+		ruleName := "common"
+		hostAppend := ""
+		port := 4000
+		serviceSuffix := "common"
+
+		// Node Rules
+		if i > 0 || !withCommon {
+			nodeNum := strconv.Itoa(i - 1)
+			if !withCommon {
+				nodeNum = strconv.Itoa(i)
+			}
+			ruleName = "node-" + nodeNum
+			hostAppend = "-" + nodeNum
+			serviceSuffix = nodeNum
+			port = 100
+		}
+		for j, domainName := range domainNames {
+			ruleIndex := j + i*numDomains
+			rule := ingress.Spec.Rules[ruleIndex]
+			expectedHost := expectedCloudRequest.Namespace + "-" + expectedCloudRequest.Name + "-solrcloud" + hostAppend + "." + domainName
+			assert.EqualValues(t, expectedHost, rule.Host, "Wrong host for ingress rule: "+ruleName)
+			assert.EqualValues(t, 1, len(rule.HTTP.Paths), "Wrong number of path rules in ingress host: "+ruleName)
+			path := rule.HTTP.Paths[0]
+			assert.EqualValues(t, "", path.Path, "There should be no path value for ingress rule: "+ruleName)
+			assert.EqualValues(t, expectedCloudRequest.Name+"-solrcloud-"+serviceSuffix, path.Backend.ServiceName, "Wrong service name for ingress rule: "+ruleName)
+			assert.EqualValues(t, port, path.Backend.ServicePort.IntVal, "Wrong port name for ingress rule: "+ruleName)
+		}
+	}
+}
diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go
index 851b2b6..b86e730 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -26,7 +26,6 @@
 	"github.com/onsi/gomega"
 	"golang.org/x/net/context"
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -98,12 +97,6 @@
 
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
-	// The instance object may not be a valid object because it might be missing some required fields.
-	// Please modify the instance object by adding required fields and then remove the following if statement.
-	if apierrors.IsInvalid(err) {
-		t.Logf("failed to create object, got an invalid object error: %v", err)
-		return
-	}
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -113,6 +106,9 @@
 
 	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
 
+	// Host Alias Tests
+	assert.Nil(t, statefulSet.Spec.Template.Spec.HostAliases, "There is no need for host aliases because traffic is going directly to pods.")
+
 	// Env Variable Tests
 	expectedEnvVars := map[string]string{
 		"ZK_HOST":        "host:7271/",
@@ -145,10 +141,10 @@
 	expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
 
 	// Check the ingress
-	expectNoIngress(g, requests, cloudIKey)
+	expectNoIngress(g, cloudIKey)
 }
 
-func TestCloudReconcileWithIngress(t *testing.T) {
+func TestCustomKubeOptionsCloudReconcile(t *testing.T) {
 	ingressBaseDomain := "ing.base.domain"
 	SetIngressBaseUrl(ingressBaseDomain)
 	UseEtcdCRD(false)
@@ -229,12 +225,6 @@
 
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
-	// The instance object may not be a valid object because it might be missing some required fields.
-	// Please modify the instance object by adding required fields and then remove the following if statement.
-	if apierrors.IsInvalid(err) {
-		t.Logf("failed to create object, got an invalid object error: %v", err)
-		return
-	}
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -249,7 +239,7 @@
 	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
 	expectedEnvVars := map[string]string{
 		"ZK_HOST":   "host:7271/",
-		"SOLR_HOST": instance.NodeIngressUrl("$(POD_HOSTNAME)", ingressBaseDomain),
+		"SOLR_HOST": "default-$(POD_HOSTNAME).ing.base.domain",
 		"SOLR_PORT": "8983",
 		"GC_TUNE":   "gc Options",
 	}
@@ -269,11 +259,8 @@
 	testMapsEqual(t, "common service labels", util.MergeLabelsOrAnnotations(expectedCommonServiceLabels, testCommonServiceLabels), service.Labels)
 	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
 
-	// Check the headless Service
-	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Selector.MatchLabels)
-	expectedHeadlessServiceLabels := util.MergeLabelsOrAnnotations(instance.SharedLabelsWith(instance.Labels), map[string]string{"service-type": "headless"})
-	testMapsEqual(t, "headless service labels", util.MergeLabelsOrAnnotations(expectedHeadlessServiceLabels, testHeadlessServiceLabels), service.Labels)
-	testMapsEqual(t, "headless service annotations", testHeadlessServiceAnnotations, service.Annotations)
+	// Check that the headless Service does not exist
+	expectNoService(g, cloudHsKey, "Headless service shouldn't exist, but it does.")
 
 	// Check the ingress
 	ingress := expectIngress(g, requests, expectedCloudRequest, cloudIKey)
@@ -340,12 +327,6 @@
 
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
-	// The instance object may not be a valid object because it might be missing some required fields.
-	// Please modify the instance object by adding required fields and then remove the following if statement.
-	if apierrors.IsInvalid(err) {
-		t.Logf("failed to create object, got an invalid object error: %v", err)
-		return
-	}
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -406,12 +387,6 @@
 
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
-	// The instance object may not be a valid object because it might be missing some required fields.
-	// Please modify the instance object by adding required fields and then remove the following if statement.
-	if apierrors.IsInvalid(err) {
-		t.Logf("failed to create object, got an invalid object error: %v", err)
-		return
-	}
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -477,12 +452,6 @@
 
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
-	// The instance object may not be a valid object because it might be missing some required fields.
-	// Please modify the instance object by adding required fields and then remove the following if statement.
-	if apierrors.IsInvalid(err) {
-		t.Logf("failed to create object, got an invalid object error: %v", err)
-		return
-	}
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
@@ -497,6 +466,9 @@
 	assert.NotNil(t, instance.Spec.BusyBoxImage, "Bad Default - instance.Spec.BusyBoxImage")
 	assert.Equal(t, solr.DefaultBusyBoxImageRepo, instance.Spec.BusyBoxImage.Repository, "Bad Default - instance.Spec.BusyBoxImage.Repository")
 	assert.Equal(t, solr.DefaultBusyBoxImageVersion, instance.Spec.BusyBoxImage.Tag, "Bad Default - instance.Spec.BusyBoxImage.Tag")
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.CommonServicePort, "Bad Default - instance.Spec.SolrAddressability.CommonServicePort")
+	assert.Equal(t, 8983, instance.Spec.SolrAddressability.PodPort, "Bad Default - instance.Spec.SolrAddressability.PodPort")
+	assert.Nil(t, instance.Spec.SolrAddressability.External, "Bad Default - instance.Spec.SolrAddressability.External")
 
 	// Check the default Zookeeper
 	assert.Equal(t, solr.DefaultZkReplicas, *instance.Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Replicas, "Bad Default - Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Replicas")
@@ -510,3 +482,212 @@
 	assert.Equal(t, 1, len(instance.Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Persistence.PersistentVolumeClaimSpec.Resources.Requests), "Bad Default - Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Persistence.PersistentVolumeClaimSpec.Resources length")
 	assert.Equal(t, 1, len(instance.Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Persistence.PersistentVolumeClaimSpec.AccessModes), "Bad Default - Spec.ZookeeperRef.ProvidedZookeeper.Zookeeper.Persistence.PersistentVolumeClaimSpec.AccesModes length")
 }
+
+func TestIngressDefaults(t *testing.T) {
+	SetIngressBaseUrl("test.ingress.url")
+	UseEtcdCRD(false)
+	UseZkCRD(true)
+	g := gomega.NewGomegaWithT(t)
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ProvidedZookeeper: &solr.ProvidedZookeeper{
+					Zookeeper: &solr.ZookeeperSpec{
+						Replicas:                  nil,
+						Image:                     nil,
+						PersistentVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{},
+						ZookeeperPod:              solr.ZookeeperPodPolicy{},
+					},
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	emptyRequests(requests)
+
+	g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+
+	// Solr defaults
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.CommonServicePort, "Bad Default - instance.Spec.SolrAddressability.CommonServicePort")
+	assert.Equal(t, 8983, instance.Spec.SolrAddressability.PodPort, "Bad Default - instance.Spec.SolrAddressability.PodPort")
+	assert.NotNil(t, instance.Spec.SolrAddressability.External, "Bad Default - instance.Spec.SolrAddressability.External")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideNodes, "Bad Default - instance.Spec.SolrAddressability.External.HideNodes")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideCommon, "Bad Default - instance.Spec.SolrAddressability.External.HideCommon")
+	assert.Equal(t, "test.ingress.url", instance.Spec.SolrAddressability.External.DomainName, "Bad Default - instance.Spec.SolrAddressability.External.DomainName")
+	assert.Equal(t, solr.Ingress, instance.Spec.SolrAddressability.External.Method, "Bad Default - instance.Spec.SolrAddressability.External.Method")
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.External.NodePortOverride, "Bad Default - instance.Spec.SolrAddressability.External.NodePortOverride")
+
+	// Test NodeServicePort automatic change when using ExternalDNS
+	instance.Spec.SolrAddressability.External = &solr.ExternalAddressability{}
+	instance.Spec.SolrAddressability.External.Method = solr.ExternalDNS
+	instance.Spec.SolrAddressability.External.DomainName = "test.ingress.url"
+	instance.Spec.SolrAddressability.PodPort = 8000
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Update(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	emptyRequests(requests)
+
+	g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+
+	// Solr defaults
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.CommonServicePort, "Bad Default - instance.Spec.SolrAddressability.CommonServicePort")
+	assert.Equal(t, 8000, instance.Spec.SolrAddressability.PodPort, "Bad Default - instance.Spec.SolrAddressability.PodPort")
+	assert.NotNil(t, instance.Spec.SolrAddressability.External, "Bad Default - instance.Spec.SolrAddressability.External")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideNodes, "Bad Default - instance.Spec.SolrAddressability.External.HideNodes")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideCommon, "Bad Default - instance.Spec.SolrAddressability.External.HideCommon")
+	assert.Equal(t, "test.ingress.url", instance.Spec.SolrAddressability.External.DomainName, "Bad Default - instance.Spec.SolrAddressability.External.DomainName")
+	assert.Equal(t, solr.ExternalDNS, instance.Spec.SolrAddressability.External.Method, "Bad Default - instance.Spec.SolrAddressability.External.Method")
+	assert.Equal(t, 0, instance.Spec.SolrAddressability.External.NodePortOverride, "Bad Default - instance.Spec.SolrAddressability.External.NodePortOverride")
+
+	// Test NodePortOverride automatic set with an ingress that has nodes exposed.
+	instance.Spec.SolrAddressability.External.Method = solr.Ingress
+	instance.Spec.SolrAddressability.PodPort = 7000
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Update(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	emptyRequests(requests)
+
+	g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+
+	// Solr defaults
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.CommonServicePort, "Bad Default - instance.Spec.SolrAddressability.CommonServicePort")
+	assert.Equal(t, 7000, instance.Spec.SolrAddressability.PodPort, "Bad Default - instance.Spec.SolrAddressability.PodPort")
+	assert.NotNil(t, instance.Spec.SolrAddressability.External, "Bad Default - instance.Spec.SolrAddressability.External")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideNodes, "Bad Default - instance.Spec.SolrAddressability.External.HideNodes")
+	assert.Equal(t, false, instance.Spec.SolrAddressability.External.HideCommon, "Bad Default - instance.Spec.SolrAddressability.External.HideCommon")
+	assert.Equal(t, "test.ingress.url", instance.Spec.SolrAddressability.External.DomainName, "Bad Default - instance.Spec.SolrAddressability.External.DomainName")
+	assert.Equal(t, solr.Ingress, instance.Spec.SolrAddressability.External.Method, "Bad Default - instance.Spec.SolrAddressability.External.Method")
+	assert.Equal(t, 80, instance.Spec.SolrAddressability.External.NodePortOverride, "Bad Default - instance.Spec.SolrAddressability.External.NodePortOverride")
+}
+
+func TestExternalKubeDomainCloudReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(false)
+	g := gomega.NewGomegaWithT(t)
+
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					InternalConnectionString: "host:7271",
+				},
+			},
+			SolrAddressability: solr.SolrAddressabilityOptions{
+				PodPort:           2000,
+				CommonServicePort: 5000,
+				KubeDomain:        testKubeDomain,
+			},
+			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+				CommonServiceOptions: &solr.ServiceOptions{
+					Annotations: testCommonServiceAnnotations,
+					Labels:      testCommonServiceLabels,
+				},
+				HeadlessServiceOptions: &solr.ServiceOptions{
+					Annotations: testHeadlessServiceAnnotations,
+					Labels:      testHeadlessServiceLabels,
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+
+	// Env Variable Tests
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + instance.Namespace + ".svc." + testKubeDomain,
+		"SOLR_PORT": "2000",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+	assert.ElementsMatch(t, []string{"-DhostPort=2000"}, statefulSet.Spec.Template.Spec.Containers[0].Args, "Wrong Solr container arguments")
+
+	// Check the client Service
+	service := expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "common service annotations", testCommonServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 5000, service.Spec.Ports[0].Port, "Wrong port on common Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on common Service")
+
+	// Check the headless Service
+	service = expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+	testMapsEqual(t, "headless service annotations", testHeadlessServiceAnnotations, service.Annotations)
+	assert.EqualValues(t, 2000, service.Spec.Ports[0].Port, "Wrong port on headless Service")
+	assert.EqualValues(t, "solr-client", service.Spec.Ports[0].TargetPort.StrVal, "Wrong podPort name on headless Service")
+
+	// Make sure individual Node services don't exist
+	nodeNames := instance.GetAllSolrNodeNames()
+	for _, nodeName := range nodeNames {
+		nodeSKey := types.NamespacedName{Name: nodeName, Namespace: "default"}
+		expectNoService(g, nodeSKey, "Node service shouldn't exist, but it does.")
+	}
+
+	// Check the ingress
+	expectNoIngress(g, cloudIKey)
+}
diff --git a/controllers/solrprometheusexporter_controller_test.go b/controllers/solrprometheusexporter_controller_test.go
index 9e3617b..088ca21 100644
--- a/controllers/solrprometheusexporter_controller_test.go
+++ b/controllers/solrprometheusexporter_controller_test.go
@@ -93,7 +93,7 @@
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedMetricsRequest)))
 
-	expectNoConfigMap(g, requests, metricsCMKey)
+	expectNoConfigMap(g, metricsCMKey)
 
 	deployment := expectDeployment(t, g, requests, expectedMetricsRequest, metricsDKey, "")
 
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index f8abb32..73772a0 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -24,6 +24,7 @@
 	"net/url"
 	"sort"
 	"strconv"
+	"strings"
 
 	solr "github.com/bloomberg/solr-operator/api/v1beta1"
 	appsv1 "k8s.io/api/apps/v1"
@@ -35,9 +36,7 @@
 )
 
 const (
-	SolrClientPort        = 8983
 	SolrClientPortName    = "solr-client"
-	ExtSolrClientPort     = 80
 	ExtSolrClientPortName = "ext-solr-client"
 	BackupRestoreVolume   = "backup-restore"
 
@@ -49,9 +48,10 @@
 // replicas: the number of replicas for the SolrCloud instance
 // storage: the size of the storage for the SolrCloud instance (e.g. 100Gi)
 // zkConnectionString: the connectionString of the ZK instance to connect to
-func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, ingressBaseDomain string, hostNameIPs map[string]string) *appsv1.StatefulSet {
+func GenerateStatefulSet(solrCloud *solr.SolrCloud, solrCloudStatus *solr.SolrCloudStatus, hostNameIPs map[string]string) *appsv1.StatefulSet {
 	gracePeriodTerm := int64(10)
-	fsGroup := int64(SolrClientPort)
+	solrPodPort := solrCloud.Spec.SolrAddressability.PodPort
+	fsGroup := int64(solrPodPort)
 	defaultMode := int32(420)
 
 	labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
@@ -164,11 +164,8 @@
 	}
 
 	// if an ingressBaseDomain is provided, the node should be addressable outside of the cluster
-	solrHostName := solrCloud.ExternalNodeUrl("$(POD_HOSTNAME)", ingressBaseDomain, false)
-	solrAdressingPort := 8983
-	if ingressBaseDomain != "" {
-		solrAdressingPort = 80
-	}
+	solrHostName := solrCloud.AdvertisedNodeHost("$(POD_HOSTNAME)")
+	solrAdressingPort := solrCloud.NodePort()
 
 	// Environment Variables
 	envVars := []corev1.EnvVar{
@@ -182,7 +179,7 @@
 		},
 		{
 			Name:  "SOLR_PORT",
-			Value: strconv.Itoa(SolrClientPort),
+			Value: strconv.Itoa(solrPodPort),
 		},
 		{
 			Name: "POD_HOSTNAME",
@@ -274,7 +271,7 @@
 							ImagePullPolicy: solrCloud.Spec.SolrImage.PullPolicy,
 							Ports: []corev1.ContainerPort{
 								{
-									ContainerPort: SolrClientPort,
+									ContainerPort: int32(solrPodPort),
 									Name:          SolrClientPortName,
 									Protocol:      "TCP",
 								},
@@ -292,7 +289,7 @@
 									HTTPGet: &corev1.HTTPGetAction{
 										Scheme: corev1.URISchemeHTTP,
 										Path:   "/solr/admin/info/system",
-										Port:   intstr.FromInt(SolrClientPort),
+										Port:   intstr.FromInt(solrPodPort),
 									},
 								},
 							},
@@ -306,7 +303,7 @@
 									HTTPGet: &corev1.HTTPGetAction{
 										Scheme: corev1.URISchemeHTTP,
 										Path:   "/solr/admin/info/system",
-										Port:   intstr.FromInt(SolrClientPort),
+										Port:   intstr.FromInt(solrPodPort),
 									},
 								},
 							},
@@ -540,6 +537,17 @@
 
 	var annotations map[string]string
 
+	// Add externalDNS annotation if necessary
+	extOpts := solrCloud.Spec.SolrAddressability.External
+	if extOpts != nil && extOpts.Method == solr.ExternalDNS && !extOpts.HideCommon {
+		annotations = make(map[string]string, 1)
+		urls := []string{solrCloud.ExternalDnsDomain(extOpts.DomainName)}
+		for _, domain := range extOpts.AdditionalDomainNames {
+			urls = append(urls, solrCloud.ExternalDnsDomain(domain))
+		}
+		annotations["external-dns.alpha.kubernetes.io/hostname"] = strings.Join(urls, ",")
+	}
+
 	customOptions := solrCloud.Spec.CustomSolrKubeOptions.CommonServiceOptions
 	if nil != customOptions {
 		labels = MergeLabelsOrAnnotations(labels, customOptions.Labels)
@@ -555,7 +563,7 @@
 		},
 		Spec: corev1.ServiceSpec{
 			Ports: []corev1.ServicePort{
-				{Name: ExtSolrClientPortName, Port: ExtSolrClientPort, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt(SolrClientPort)},
+				{Name: ExtSolrClientPortName, Port: int32(solrCloud.Spec.SolrAddressability.CommonServicePort), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)},
 			},
 			Selector: selectorLabels,
 		},
@@ -576,6 +584,17 @@
 
 	var annotations map[string]string
 
+	// Add externalDNS annotation if necessary
+	extOpts := solrCloud.Spec.SolrAddressability.External
+	if extOpts != nil && extOpts.Method == solr.ExternalDNS && !extOpts.HideNodes {
+		annotations = make(map[string]string, 1)
+		urls := []string{solrCloud.ExternalDnsDomain(extOpts.DomainName)}
+		for _, domain := range extOpts.AdditionalDomainNames {
+			urls = append(urls, solrCloud.ExternalDnsDomain(domain))
+		}
+		annotations["external-dns.alpha.kubernetes.io/hostname"] = strings.Join(urls, ",")
+	}
+
 	customOptions := solrCloud.Spec.CustomSolrKubeOptions.HeadlessServiceOptions
 	if nil != customOptions {
 		labels = MergeLabelsOrAnnotations(labels, customOptions.Labels)
@@ -591,7 +610,7 @@
 		},
 		Spec: corev1.ServiceSpec{
 			Ports: []corev1.ServicePort{
-				{Name: SolrClientPortName, Port: SolrClientPort, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt(SolrClientPort)},
+				{Name: ExtSolrClientPortName, Port: int32(solrCloud.NodePort()), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)},
 			},
 			Selector:                 selectorLabels,
 			ClusterIP:                corev1.ClusterIPNone,
@@ -632,7 +651,7 @@
 		Spec: corev1.ServiceSpec{
 			Selector: selectorLabels,
 			Ports: []corev1.ServicePort{
-				{Name: ExtSolrClientPortName, Port: ExtSolrClientPort, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt(SolrClientPort)},
+				{Name: ExtSolrClientPortName, Port: int32(solrCloud.NodePort()), Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString(SolrClientPortName)},
 			},
 			PublishNotReadyAddresses: true,
 		},
@@ -669,11 +688,11 @@
 	return requireUpdate
 }
 
-// GenerateCommonIngress returns a new Ingress pointer generated for the entire SolrCloud, pointing to all instances
+// GenerateIngress returns a new Ingress pointer generated for the entire SolrCloud, pointing to all instances
 // solrCloud: SolrCloud instance
 // nodeStatuses: []SolrNodeStatus the nodeStatuses
 // ingressBaseDomain: string baseDomain of the ingress
-func GenerateCommonIngress(solrCloud *solr.SolrCloud, nodeNames []string, ingressBaseDomain string) (ingress *extv1.Ingress) {
+func GenerateIngress(solrCloud *solr.SolrCloud, nodeNames []string, ingressBaseDomain string) (ingress *extv1.Ingress) {
 	labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels())
 	var annotations map[string]string
 
@@ -683,28 +702,10 @@
 		annotations = MergeLabelsOrAnnotations(annotations, customOptions.Annotations)
 	}
 
-	rules := []extv1.IngressRule{
-		{
-			Host: solrCloud.CommonIngressUrl(ingressBaseDomain),
-			IngressRuleValue: extv1.IngressRuleValue{
-				HTTP: &extv1.HTTPIngressRuleValue{
-					Paths: []extv1.HTTPIngressPath{
-						{
-							Backend: extv1.IngressBackend{
-								ServiceName: solrCloud.CommonServiceName(),
-								ServicePort: intstr.FromInt(ExtSolrClientPort),
-							},
-						},
-					},
-				},
-			},
-		},
-	}
+	extOpts := solrCloud.Spec.SolrAddressability.External
 
-	for _, nodeName := range nodeNames {
-		ingressRule := CreateNodeIngressRule(solrCloud, nodeName, ingressBaseDomain)
-		rules = append(rules, ingressRule)
-	}
+	// Create advertised domain name and possible additional domain names
+	rules := CreateSolrIngressRules(solrCloud, nodeNames, append([]string{extOpts.DomainName}, extOpts.AdditionalDomainNames...))
 
 	ingress = &extv1.Ingress{
 		ObjectMeta: metav1.ObjectMeta{
@@ -720,20 +721,63 @@
 	return ingress
 }
 
-// CreateNodeIngressRule returns a new Ingress Rule generated for a specific Solr Node
+// CreateSolrIngressRules returns all applicable ingress rules for a cloud.
+// solrCloud: SolrCloud instance
+// nodeNames: the names for each of the solr pods
+// domainName: string Domain for the ingress rule to use
+func CreateSolrIngressRules(solrCloud *solr.SolrCloud, nodeNames []string, domainNames []string) []extv1.IngressRule {
+	var ingressRules []extv1.IngressRule
+	if !solrCloud.Spec.SolrAddressability.External.HideCommon {
+		for _, domainName := range domainNames {
+			ingressRules = append(ingressRules, CreateCommonIngressRule(solrCloud, domainName))
+		}
+	}
+	if !solrCloud.Spec.SolrAddressability.External.HideNodes {
+		for _, nodeName := range nodeNames {
+			for _, domainName := range domainNames {
+				ingressRules = append(ingressRules, CreateNodeIngressRule(solrCloud, nodeName, domainName))
+			}
+		}
+	}
+	return ingressRules
+}
+
+// CreateCommonIngressRule returns a new Ingress Rule generated for a SolrCloud under the given domainName
+// solrCloud: SolrCloud instance
+// domainName: string Domain for the ingress rule to use
+func CreateCommonIngressRule(solrCloud *solr.SolrCloud, domainName string) (ingressRule extv1.IngressRule) {
+	ingressRule = extv1.IngressRule{
+		Host: solrCloud.ExternalCommonUrl(domainName, false),
+		IngressRuleValue: extv1.IngressRuleValue{
+			HTTP: &extv1.HTTPIngressRuleValue{
+				Paths: []extv1.HTTPIngressPath{
+					{
+						Backend: extv1.IngressBackend{
+							ServiceName: solrCloud.CommonServiceName(),
+							ServicePort: intstr.FromInt(solrCloud.Spec.SolrAddressability.CommonServicePort),
+						},
+					},
+				},
+			},
+		},
+	}
+	return ingressRule
+}
+
+// CreateNodeIngressRule returns a new Ingress Rule generated for a specific Solr Node under the given domainName
 // solrCloud: SolrCloud instance
 // nodeName: string Name of the node
-// ingressBaseDomain: string base domain for the ingress controller
-func CreateNodeIngressRule(solrCloud *solr.SolrCloud, nodeName string, ingressBaseDomain string) (ingressRule extv1.IngressRule) {
+// domainName: string Domain for the ingress rule to use
+func CreateNodeIngressRule(solrCloud *solr.SolrCloud, nodeName string, domainName string) (ingressRule extv1.IngressRule) {
 	ingressRule = extv1.IngressRule{
-		Host: solrCloud.NodeIngressUrl(nodeName, ingressBaseDomain),
+		Host: solrCloud.ExternalNodeUrl(nodeName, domainName, false),
 		IngressRuleValue: extv1.IngressRuleValue{
 			HTTP: &extv1.HTTPIngressRuleValue{
 				Paths: []extv1.HTTPIngressPath{
 					{
 						Backend: extv1.IngressBackend{
 							ServiceName: nodeName,
-							ServicePort: intstr.FromInt(ExtSolrClientPort),
+							ServicePort: intstr.FromInt(solrCloud.NodePort()),
 						},
 					},
 				},
diff --git a/docs/solr-cloud/README.md b/docs/solr-cloud/README.md
index 0c85b47..582f260 100644
--- a/docs/solr-cloud/README.md
+++ b/docs/solr-cloud/README.md
@@ -2,7 +2,7 @@
 
 The Solr Operator supports creating and managing Solr Clouds.
 
-A reference on the available options for the SolrCloud CRD can be found [here](solr-cloud-crd.md).
+To find how to configure the SolrCloud best for your use case, please refer to the [documentation on available SolrCloud CRD options](solr-cloud-crd.md).
 
 This page outlines how to create, update and delete a SolrCloud in Kubernetes.
 
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index ace75c5..5f3688a 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -3,6 +3,30 @@
 The SolrCloud CRD allows users to spin up a Solr cloud in a very configurable way.
 Those configuration options are layed out on this page.
 
+
+## Addressability
+
+The SolrCloud CRD provides users the ability to define how it is addressed, through `SolrCloud.spec.solrAddressability`.
+This provides the following options:
+
+- **`podPort`** - The port on which the pod is listening. This is also that the port that the Solr Jetty service will listen on. (Defaults to `8983`)
+- **`commonServicePort`** - The port on which the common service is exposed. (Defaults to `80`)
+- **`kubeDomain`** - Specifies an override of the default Kubernetes cluster domain name, `cluster.local`. This option should only be used if the Kubernetes cluster has been setup with a custom domain name.
+- **`external`** - Expose the cloud externally, outside of the kubernetes cluster in which it is running.
+  - **`method`** - (Required) The method by which your cloud will be exposed externally.
+  Currently available options are [`Ingress`](https://kubernetes.io/docs/concepts/services-networking/ingress/) and [`ExternalDNS`](https://github.com/kubernetes-sigs/external-dns).
+  The goal is to support more methods in the future, such as LoadBalanced Services.
+  - **`domainName`** - (Required) The primary domain name to open your cloud endpoints on. If `useExternalAddress` is set to `true`, then this is the domain that will be used in Solr Node names.
+  - **`additionalDomainNames`** - You can choose to listen on additional domains for each endpoint, however Solr will not register itself under these names.
+  - **`useExternalAddress`** - Use the external address to advertise the SolrNode. If a domain name is required for the chosen external `method`, then the one provided in `domainName` will be used.
+  - **`hideCommon`** - Do not externally expose the common service (one endpoint for all solr nodes).
+  - **`hideNodes`** - Do not externally expose each node. (This cannot be set to `true` if the cloud is running across multiple kubernetes clusters)
+  - **`nodePortOverride`** - Make the Node Service(s) override the podPort. This is only available for the `Ingress` external method. If `hideNodes` is set to `true`, then this option is ignored. If provided, his port will be used to advertise the Solr Node. \
+  If `method: Ingress` and `hideNodes: false`, then this value defaults to `80` since that is the default port that ingress controllers listen on.
+
+**Note:** Unless both `external.method=Ingress` and `external.hideNodes=false`, a headless service will be used to make each Solr Node in the statefulSet addressable.
+If both of those criteria are met, then an individual ClusterIP Service will be created for each Solr Node/Pod.
+
 ## Zookeeper Reference
 
 Solr Clouds require an Apache Zookeeper to connect to.
diff --git a/example/test_solrcloud_addressability.yaml b/example/test_solrcloud_addressability.yaml
new file mode 100644
index 0000000..6abf94f
--- /dev/null
+++ b/example/test_solrcloud_addressability.yaml
@@ -0,0 +1,36 @@
+apiVersion: solr.bloomberg.com/v1beta1
+kind: SolrCloud
+metadata:
+  name: ingress-cloud
+spec:
+  replicas: 3
+  solrImage:
+    tag: 8.2.0
+  solrAddressability:
+    podPort: 10000
+    commonServicePort: 80
+    external:
+      method: Ingress
+      useExternalAddress: false
+      domainName: "kube.example.com"
+      additionalDomainNames:
+        - "another.kube.example.com"
+        - "another.kube.other.com"
+      nodePortOverride: 80
+
+---
+apiVersion: solr.bloomberg.com/v1beta1
+kind: SolrCloud
+metadata:
+  name: external-dns-cloud
+spec:
+  replicas: 3
+  solrImage:
+    tag: 8.2.0
+  solrAddressability:
+    podPort: 10000
+    commonServicePort: 80
+    external:
+      method: ExternalDNS
+      useExternalAddress: true
+      domainName: "kube.example.com"
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index fe38944..615fd2a 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -17,4 +17,4 @@
 maintainers:
   - name: Houston Putman
     email: houston@apache.org
-icon: https://lucene.apache.org/theme/images/solr/identity/Solr_Logo_on_white.png
+icon: https://lucene.apache.org/theme/images/solr/identity/Solr_Logo_on_white.png
\ No newline at end of file
diff --git a/helm/solr-operator/README.md b/helm/solr-operator/README.md
index 417a90d..0bb45f0 100644
--- a/helm/solr-operator/README.md
+++ b/helm/solr-operator/README.md
@@ -120,9 +120,9 @@
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
 | watchNamespaces | string | `""` | A comma-separated list of namespaces that the solr operator should watch. If empty, the solr operator will watch all namespaces in the cluster. If set to `true`, this will be populated with the namespace that the operator is deployed to. |
-| ingressBaseDomain | string | `""` | If you have a base domain that points to your ingress controllers for this kubernetes cluster, you can provide this. SolrClouds will then begin to use ingresses that utilize this base domain. E.g. `solrcloud-test.<base.domain>` |
 | useZkOperator | string | `"true"` | This option enables the use of provided Zookeeper instances for SolrClouds |
 | useEtcdOperator | string | `"false"` | This option enables the use of provided Zetcd instances for SolrClouds |
+| ingressBaseDomain | string | `""` | **NOTE: This feature is deprecated and will be removed in `v0.3.0`. The option is now provided within the SolrCloud CRD.** If you have a base domain that points to your ingress controllers for this kubernetes cluster, you can provide this. SolrClouds will then begin to use ingresses that utilize this base domain. E.g. `solrcloud-test.<base.domain>` |
 
 ### Running the Solr Operator
 
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index 9377f0f..372b07e 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -5244,6 +5244,96 @@
               description: The number of solr nodes to run
               format: int32
               type: integer
+            solrAddressability:
+              description: Customize how Solr is addressed both internally and externally
+                in Kubernetes.
+              properties:
+                commonServicePort:
+                  description: CommonServicePort defines the port to have the common
+                    Solr service listen on. Defaults to 80
+                  type: integer
+                external:
+                  description: External defines the way in which this SolrCloud nodes
+                    should be made addressable externally, from outside the Kubernetes
+                    cluster. If none is provided, the Solr Cloud will not be made
+                    addressable externally.
+                  properties:
+                    additionalDomains:
+                      description: Provide additional domainNames that the Ingress
+                        or ExternalDNS should listen on. This option is ignored with
+                        the LoadBalancer method.
+                      items:
+                        type: string
+                      type: array
+                    domainName:
+                      description: "Override the domainName provided as startup parameters
+                        to the operator, used by ingresses and externalDNS. The common
+                        and/or node services will be addressable by unique names under
+                        the given domain. e.g. default-example-solrcloud.given.domain.name.com
+                        \n This options will be required for the Ingress and ExternalDNS
+                        methods once the ingressBaseDomain startup parameter is removed.
+                        \n For the LoadBalancer method, this field is optional and
+                        will only be used when useExternalAddress=true. If used with
+                        the LoadBalancer method, you will need DNS routing to the
+                        LoadBalancer IP address through the url template given above."
+                      type: string
+                    hideCommon:
+                      description: Do not expose the common Solr service externally.
+                        This affects a single service. Defaults to false.
+                      type: boolean
+                    hideNodes:
+                      description: Do not expose each of the Solr Node services externally.
+                        The number of services this affects could range from 1 (a
+                        headless service for ExternalDNS) to the number of Solr pods
+                        your cloud contains (individual node services for Ingress/LoadBalancer).
+                        Defaults to false.
+                      type: boolean
+                    method:
+                      description: The way in which this SolrCloud's service(s) should
+                        be made addressable externally.
+                      enum:
+                      - Ingress
+                      - ExternalDNS
+                      type: string
+                    nodePortOverride:
+                      description: "NodePortOverride defines the port to have all
+                        Solr node service(s) listen on and advertise itself as if
+                        advertising through an Ingress or LoadBalancer. This overrides
+                        the default usage of the podPort. \n This is option is only
+                        used when HideNodes=false, otherwise the the port each Solr
+                        Node will advertise itself with the podPort. This option is
+                        also unavailable with the ExternalDNS method. \n If using
+                        method=Ingress, your ingress controller is required to listen
+                        on this port. If your ingress controller is not listening
+                        on the podPort, then this option is required for solr to be
+                        addressable via an Ingress. \n Defaults to 80 if HideNodes=false
+                        and method=Ingress, otherwise this is optional."
+                      type: integer
+                    useExternalAddress:
+                      description: "Use the external address to advertise the SolrNode,
+                        defaults to false. \n If false, the external address will
+                        be available, however Solr (and clients using the CloudSolrClient
+                        in SolrJ) will only be aware of the internal URLs. If true,
+                        Solr will startup with the hostname of the external address.
+                        \n NOTE: This option cannot be true when hideNodes is set
+                        to true. So it will be auto-set to false if that is the case.
+                        \n Deprecation warning: When an ingress-base-domain is passed
+                        in to the operator, this value defaults to true."
+                      type: boolean
+                  required:
+                  - method
+                  type: object
+                kubeDomain:
+                  description: KubeDomain allows for the specification of an override
+                    of the default "cluster.local" Kubernetes cluster domain. Only
+                    use this option if the Kubernetes cluster has been setup with
+                    a custom domain.
+                  type: string
+                podPort:
+                  description: PodPort defines the port to have the Solr Pod listen
+                    on. Defaults to 8983
+                  type: integer
+              type: object
             solrGCTune:
               description: Set GC Tuning configuration through GC_TUNE environment
                 variable