Repository: linode/linode-cloud-controller-manager
Pull request #130 (2023-05-15 12:56:49)

Adding an environment vairiable to change default hostname only ingress abilities.
### General:
Adding the ability to set the Hostname Only Ingress vian ENVVARS because Gateway API does not yet support annotations.

See the conversation over at cilium: https://github.com/cilium/cilium/issues/25357


* [x] Have you removed all sensitive information, including but not limited to access keys and passwords?
* [x] Have you checked to ensure there aren't other open or closed [Pull Requests](../../pulls) for the same bug/feature/question?

### Pull Request Guidelines:

1. [x] Does your submission pass tests?
1. [x] Have you added tests? 
1. [x] Are you addressing a single feature in this PR? 
1. [x] Are your commits atomic, addressing one change per commit?
1. [x] Are you following the conventions of the language? 
1. [x] Have you saved your large formatting changes for a different PR, so we can focus on your work?
1. [x] Have you explained your rationale for why this feature is needed? 
1. [x] Have you linked your PR to an [open issue](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/)



srust: Would you mind squashing some commits? Otherwise looks good to go

Patch:
diff --git a/README.md b/README.md
index bd0158b..5c29627 100644
--- a/README.md
+++ b/README.md
@@ -230,7 +230,7 @@ _See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command documen
 
 ### Configurations
 
-There are other variables that can be set to a different value. For list of all the modifiable variables/values, take a look at './deploy/chart/values.yaml'. 
+There are other variables that can be set to a different value. For list of all the modifiable variables/values, take a look at './deploy/chart/values.yaml'.
 
 Values can be set/overrided by using the '--set var=value,...' flag or by passing in a custom-values.yaml using '-f custom-values.yaml'.
 
diff --git a/cloud/linode/common.go b/cloud/linode/common.go
index c4562c3..221816b 100644
--- a/cloud/linode/common.go
+++ b/cloud/linode/common.go
@@ -1,9 +1,14 @@
 package linode
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
+
+	"github.com/linode/linodego"
+	"k8s.io/apimachinery/pkg/types"
+	cloudprovider "k8s.io/cloud-provider"
 )
 
 const providerIDPrefix = "linode://"
@@ -26,3 +31,34 @@ func parseProviderID(providerID string) (int, error) {
 	}
 	return id, nil
 }
+
+func linodeFilterListOptions(targetLabel string) *linodego.ListOptions {
+	jsonFilter := fmt.Sprintf(`{"label":%q}`, targetLabel)
+	return linodego.NewListOptions(0, jsonFilter)
+}
+
+func linodeByName(ctx context.Context, client Client, nodeName types.NodeName) (*linodego.Instance, error) {
+	linodes, err := client.ListInstances(ctx, linodeFilterListOptions(string(nodeName)))
+	if err != nil {
+		return nil, err
+	}
+
+	if len(linodes) == 0 {
+		return nil, cloudprovider.InstanceNotFound
+	} else if len(linodes) > 1 {
+		return nil, fmt.Errorf("Multiple instances found with name %v", nodeName)
+	}
+
+	return &linodes[0], nil
+}
+
+func linodeByID(ctx context.Context, client Client, id int) (*linodego.Instance, error) {
+	instance, err := client.GetInstance(ctx, id)
+	if err != nil {
+		return nil, err
+	}
+	if instance == nil {
+		return nil, fmt.Errorf("linode not found with id %v", id)
+	}
+	return instance, nil
+}
diff --git a/cloud/linode/instances.go b/cloud/linode/instances.go
index c803b32..3402159 100644
--- a/cloud/linode/instances.go
+++ b/cloud/linode/instances.go
@@ -3,10 +3,8 @@ package linode
 import (
 	"context"
 	"fmt"
-	"os"
+	"net/http"
 	"strconv"
-	"sync"
-	"time"
 
 	"github.com/linode/linode-cloud-controller-manager/sentry"
 	"github.com/linode/linodego"
@@ -15,56 +13,12 @@ import (
 	cloudprovider "k8s.io/cloud-provider"
 )
 
-type nodeCache struct {
-	sync.RWMutex
-	nodes      map[int]*linodego.Instance
-	lastUpdate time.Time
-	ttl        time.Duration
-}
-
-// refreshInstances conditionally loads all instances from the Linode API and caches them.
-// It does not refresh if the last update happened less than `nodeCache.ttl` ago.
-func (nc *nodeCache) refreshInstances(ctx context.Context, client Client) error {
-	nc.Lock()
-	defer nc.Unlock()
-
-	if time.Since(nc.lastUpdate) < nc.ttl {
-		return nil
-	}
-
-	instances, err := client.ListInstances(ctx, nil)
-	if err != nil {
-		return err
-	}
-	nc.nodes = make(map[int]*linodego.Instance)
-	for _, instance := range instances {
-		instance := instance
-		nc.nodes[instance.ID] = &instance
-	}
-	nc.lastUpdate = time.Now()
-
-	return nil
-}
-
 type instances struct {
 	client Client
-
-	nodeCache *nodeCache
 }
 
 func newInstances(client Client) cloudprovider.InstancesV2 {
-	var timeout int
-	if raw, ok := os.LookupEnv("LINODE_INSTANCE_CACHE_TTL"); ok {
-		timeout, _ = strconv.Atoi(raw)
-	}
-	if timeout == 0 {
-		timeout = 15
-	}
-
-	return &instances{client, &nodeCache{
-		nodes: make(map[int]*linodego.Instance),
-		ttl:   time.Duration(timeout) * time.Second,
-	}}
+	return &instances{client}
 }
 
 type instanceNoIPAddressesError struct {
@@ -75,33 +29,7 @@ func (e instanceNoIPAddressesError) Error() string {
 	return fmt.Sprintf("instance %d has no IP addresses", e.id)
 }
 
-func (i *instances) linodeByName(nodeName types.NodeName) (*linodego.Instance, error) {
-	i.nodeCache.RLock()
-	defer i.nodeCache.RUnlock()
-	for _, node := range i.nodeCache.nodes {
-		if node.Label == string(nodeName) {
-			return node, nil
-		}
-	}
-
-	return nil, cloudprovider.InstanceNotFound
-}
-
-func (i *instances) linodeByID(id int) (*linodego.Instance, error) {
-	i.nodeCache.RLock()
-	defer i.nodeCache.RUnlock()
-	instance, ok := i.nodeCache.nodes[id]
-	if !ok {
-		return nil, cloudprovider.InstanceNotFound
-	}
-	return instance, nil
-}
-
 func (i *instances) lookupLinode(ctx context.Context, node *v1.Node) (*linodego.Instance, error) {
-	if err := i.nodeCache.refreshInstances(ctx, i.client); err != nil {
-		return nil, err
-	}
-
 	providerID := node.Spec.ProviderID
 	nodeName := types.NodeName(node.Name)
 
@@ -116,16 +44,16 @@ func (i *instances) lookupLinode(ctx context.Context, node *v1.Node) (*linodego.
 		}
 		sentry.SetTag(ctx, "linode_id", strconv.Itoa(id))
 
-		return i.linodeByID(id)
+		return linodeByID(ctx, i.client, id)
 	}
 
-	return i.linodeByName(nodeName)
+	return linodeByName(ctx, i.client, nodeName)
 }
 
 func (i *instances) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
 	ctx = sentry.SetHubOnContext(ctx)
 	if _, err := i.lookupLinode(ctx, node); err != nil {
-		if err == cloudprovider.InstanceNotFound {
+		if apiError, ok := err.(*linodego.Error); ok && apiError.Code == http.StatusNotFound {
 			return false, nil
 		}
 		sentry.CaptureError(ctx, err)
diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go
index 8d7436a..b3e4cda 100644
--- a/cloud/linode/loadbalancers.go
+++ b/cloud/linode/loadbalancers.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"os"
 	"strconv"
 	"strings"
 	"time"
@@ -776,14 +777,26 @@ func makeLoadBalancerStatus(service *v1.Service, nb *linodego.NodeBalancer) *v1.
 		Hostname: *nb.Hostname,
 	}
 	if !getServiceBoolAnnotation(service, annLinodeHostnameOnlyIngress) {
-		ingress.IP = *nb.IPv4
+		if val := envBoolOptions("LINODE_HOSTNAME_ONLY_INGRESS"); val {
+			klog.Infof("LINODE_HOSTNAME_ONLY_INGRESS:  (%v)", val)
+		} else {
+			ingress.IP = *nb.IPv4
+		}
 	}
-
 	return &v1.LoadBalancerStatus{
 		Ingress: []v1.LoadBalancerIngress{ingress},
 	}
 }
 
+// Checks for a truth value in an environment variable
+func envBoolOptions(o string) bool {
+	boolValue, err := strconv.ParseBool(os.Getenv(o))
+	if err != nil {
+		return false
+	}
+	return boolValue
+}
+
 // getServiceNn returns the services namespaced name.
 func getServiceNn(service *v1.Service) string {
 	return fmt.Sprintf("%s/%s", service.Namespace, service.Name)
diff --git a/deploy/chart/templates/daemonset.yaml b/deploy/chart/templates/daemonset.yaml
index c91c821..5d1511c 100644
--- a/deploy/chart/templates/daemonset.yaml
+++ b/deploy/chart/templates/daemonset.yaml
@@ -47,6 +47,7 @@ spec:
                 secretKeyRef:
                   name: ccm-linode
                   key: region
+            {{- toYaml .Values.env | nindent 12 }}
       volumes:
         - name: k8s
           hostPath:
diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml
index 1a63e51..44e5635 100644
--- a/deploy/chart/values.yaml
+++ b/deploy/chart/values.yaml
@@ -1,4 +1,4 @@
-# apiToken [Required] - Must be a Linode APIv4 Personal Access Token with all permissions. (https://cloud.linode.com/profile/tokens) 
+# apiToken [Required] - Must be a Linode APIv4 Personal Access Token with all permissions. (https://cloud.linode.com/profile/tokens)
 apiToken: ""
 
 # region [Required] - Must be a Linode region. (https://api.linode.com/v4/regions)
@@ -20,7 +20,7 @@ image:
 namespace: "kube-system"
 
 # Set of default tolerations
-tolerations: 
+tolerations:
  # The CCM can run on Nodes tainted as masters
   - key: "node-role.kubernetes.io/master"
     effect: "NoSchedule"
@@ -37,3 +37,9 @@ tolerations:
   - key: node.kubernetes.io/unreachable
     operator: Exists
     effect: NoSchedule
+# This section adds the ability to pass environment variables to adjust CCM defaults
+# https://github.com/linode/linode-cloud-controller-manager/blob/master/cloud/linode/loadbalancers.go
+# LINODE_HOSTNAME_ONLY_INGRESS type bool is supported
+# env:
+  # - name: EXAMPLE_ENV_VAR
+  #   value: "true"
\ No newline at end of file


Test patch:
diff --git a/cloud/linode/instances_test.go b/cloud/linode/instances_test.go
index acfba0a..f42068a 100644
--- a/cloud/linode/instances_test.go
+++ b/cloud/linode/instances_test.go
@@ -2,8 +2,10 @@ package linode
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
+	"net/http"
 	"strconv"
 	"testing"
 
@@ -31,11 +33,23 @@ func TestInstanceExists(t *testing.T) {
 	defer ctrl.Finish()
 
 	client := NewMockClient(ctrl)
+	instances := newInstances(client)
+
+	t.Run("should propagate generic api error", func(t *testing.T) {
+		node := nodeWithProviderID(providerIDPrefix + "123")
+		expectedErr := errors.New("some error")
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(nil, expectedErr)
+
+		exists, err := instances.InstanceExists(ctx, node)
+		assert.ErrorIs(t, err, expectedErr)
+		assert.False(t, exists)
+	})
 
 	t.Run("should return false if linode does not exist (by providerID)", func(t *testing.T) {
-		instances := newInstances(client)
 		node := nodeWithProviderID(providerIDPrefix + "123")
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(nil, &linodego.Error{
+			Code: http.StatusNotFound,
+		})
 
 		exists, err := instances.InstanceExists(ctx, node)
 		assert.NoError(t, err)
@@ -43,10 +57,13 @@ func TestInstanceExists(t *testing.T) {
 	})
 
 	t.Run("should return false if linode does not exist (by name)", func(t *testing.T) {
-		instances := newInstances(client)
 		name := "some-name"
 		node := nodeWithName(name)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		notFound := &linodego.Error{
+			Code: http.StatusNotFound,
+		}
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return(nil, notFound)
 
 		exists, err := instances.InstanceExists(ctx, node)
 		assert.NoError(t, err)
@@ -54,13 +71,12 @@ func TestInstanceExists(t *testing.T) {
 	})
 
 	t.Run("should return true if linode exists (by provider)", func(t *testing.T) {
-		instances := newInstances(client)
 		node := nodeWithProviderID(providerIDPrefix + "123")
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
-			{ID: 123,
-				Label:  "mock",
-				Region: "us-east",
-				Type:   "g6-standard-2"},
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(&linodego.Instance{
+			ID:     123,
+			Label:  "mock",
+			Region: "us-east",
+			Type:   "g6-standard-2",
 		}, nil)
 
 		exists, err := instances.InstanceExists(ctx, node)
@@ -69,11 +85,10 @@ func TestInstanceExists(t *testing.T) {
 	})
 
 	t.Run("should return true if linode exists (by name)", func(t *testing.T) {
-		instances := newInstances(client)
 		name := "some-name"
 		node := nodeWithName(name)
 
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
+		client.EXPECT().ListInstances(gomock.Any(), linodeFilterListOptions(name)).Times(1).Return([]linodego.Instance{
 			{ID: 123, Label: name},
 		}, nil)
 
@@ -89,12 +104,13 @@ func TestMetadataRetrieval(t *testing.T) {
 	defer ctrl.Finish()
 
 	client := NewMockClient(ctrl)
+	instances := newInstances(client)
 
 	t.Run("errors when linode does not exist (by name)", func(t *testing.T) {
-		instances := newInstances(client)
 		name := "does-not-exist"
 		node := nodeWithName(name)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{}, nil)
 
 		meta, err := instances.InstanceMetadata(ctx, node)
 		assert.ErrorIs(t, err, cloudprovider.InstanceNotFound)
@@ -102,19 +118,18 @@ func TestMetadataRetrieval(t *testing.T) {
 	})
 
 	t.Run("fails when linode does not exist (by provider)", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 456302
 		providerID := providerIDPrefix + strconv.Itoa(id)
 		node := nodeWithProviderID(providerID)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		getInstanceErr := &linodego.Error{Code: http.StatusNotFound}
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(nil, getInstanceErr)
 		meta, err := instances.InstanceMetadata(ctx, node)
 
-		assert.ErrorIs(t, err, cloudprovider.InstanceNotFound)
+		assert.ErrorIs(t, err, getInstanceErr)
 		assert.Nil(t, meta)
 	})
 
 	t.Run("should return data when linode is found (by name)", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 123
 		name := "mock-instance"
 		node := nodeWithName(name)
@@ -122,7 +137,8 @@ func TestMetadataRetrieval(t *testing.T) {
 		privateIPv4 := net.ParseIP("192.168.133.65")
 		linodeType := "g6-standard-1"
 		region := "us-east"
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{
 			{ID: id, Label: name, Type: linodeType, Region: region, IPv4: []*net.IP{&publicIPv4, &privateIPv4}},
 		}, nil)
 
@@ -168,7 +184,6 @@ func TestMetadataRetrieval(t *testing.T) {
 
 	for _, test := range ipTests {
 		t.Run(fmt.Sprintf("addresses are retrieved - %s", test.name), func(t *testing.T) {
-			instances := newInstances(client)
 			id := 192910
 			name := "my-instance"
 			providerID := providerIDPrefix + strconv.Itoa(id)
@@ -185,8 +200,8 @@ func TestMetadataRetrieval(t *testing.T) {
 
 			linodeType := "g6-standard-1"
 			region := "us-east"
-			client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
-				{ID: id, Label: name, Type: linodeType, Region: region, IPv4: ips},
+			client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+				ID: id, Label: name, Type: linodeType, Region: region, IPv4: ips,
 			}, nil)
 
 			meta, err := instances.InstanceMetadata(ctx, node)
@@ -210,23 +225,19 @@ func TestMalformedProviders(t *testing.T) {
 	defer ctrl.Finish()
 
 	client := NewMockClient(ctrl)
+	instances := newInstances(client)
 
 	t.Run("fails on malformed providerID", func(t *testing.T) {
-		instances := newInstances(client)
 		providerID := "bogus://bogus"
 		node := nodeWithProviderID(providerID)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
-
 		meta, err := instances.InstanceMetadata(ctx, node)
 		assert.ErrorIs(t, err, invalidProviderIDError{providerID})
 		assert.Nil(t, meta)
 	})
 
 	t.Run("fails on non-numeric providerID", func(t *testing.T) {
-		instances := newInstances(client)
 		providerID := providerIDPrefix + "abc"
 		node := nodeWithProviderID(providerID)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
 		meta, err := instances.InstanceMetadata(ctx, node)
 
 		assert.ErrorIs(t, err, invalidProviderIDError{providerID})
@@ -240,12 +251,12 @@ func TestInstanceShutdown(t *testing.T) {
 	defer ctrl.Finish()
 
 	client := NewMockClient(ctrl)
+	instances := newInstances(client)
 
 	t.Run("fails when instance not found (by provider)", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 12345
 		node := nodeWithProviderID(providerIDPrefix + strconv.Itoa(id))
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(nil, linodego.Error{Code: http.StatusNotFound})
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.Error(t, err)
@@ -253,10 +264,13 @@ func TestInstanceShutdown(t *testing.T) {
 	})
 
 	t.Run("fails when instance not found (by name)", func(t *testing.T) {
-		instances := newInstances(client)
 		name := "some-name"
 		node := nodeWithName(name)
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, nil)
+		notFound := &linodego.Error{
+			Code: http.StatusNotFound,
+		}
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return(nil, notFound)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.Error(t, err)
@@ -264,11 +278,10 @@ func TestInstanceShutdown(t *testing.T) {
 	})
 
 	t.Run("returns true when instance is shut down", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 12345
 		node := nodeWithProviderID(providerIDPrefix + strconv.Itoa(id))
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
-			{ID: id, Label: "offline-linode", Status: linodego.InstanceOffline},
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+			ID: id, Label: "offline-linode", Status: linodego.InstanceOffline,
 		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
@@ -277,11 +290,11 @@ func TestInstanceShutdown(t *testing.T) {
 	})
 
 	t.Run("returns true when instance is shutting down", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 12345
 		node := nodeWithProviderID(providerIDPrefix + strconv.Itoa(id))
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
-			{ID: id, Label: "shutting-down-linode", Status: linodego.InstanceShuttingDown}}, nil)
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+			ID: id, Label: "shutting-down-linode", Status: linodego.InstanceShuttingDown,
+		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.NoError(t, err)
@@ -289,11 +302,10 @@ func TestInstanceShutdown(t *testing.T) {
 	})
 
 	t.Run("returns false when instance is running", func(t *testing.T) {
-		instances := newInstances(client)
 		id := 12345
 		node := nodeWithProviderID(providerIDPrefix + strconv.Itoa(id))
-		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
-			{ID: id, Label: "running-linode", Status: linodego.InstanceRunning},
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+			ID: id, Label: "running-linode", Status: linodego.InstanceRunning,
 		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go
index 68c3645..f4ad2c6 100644
--- a/cloud/linode/loadbalancers_test.go
+++ b/cloud/linode/loadbalancers_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"reflect"
 	"strconv"
 	"testing"
@@ -166,6 +167,10 @@ func TestCCMLoadBalancers(t *testing.T) {
 			name: "makeLoadBalancerStatus",
 			f:    testMakeLoadBalancerStatus,
 		},
+		{
+			name: "makeLoadBalancerStatusEnvVar",
+			f:    testMakeLoadBalancerStatusEnvVar,
+		},
 		{
 			name: "Cleanup does not call the API unless Service annotated",
 			f:    testCleanupDoesntCall,
@@ -1448,6 +1453,55 @@ func testMakeLoadBalancerStatus(t *testing.T, client *linodego.Client, _ *fakeAP
 	}
 }
 
+func testMakeLoadBalancerStatusEnvVar(t *testing.T, client *linodego.Client, _ *fakeAPI) {
+	ipv4 := "192.168.0.1"
+	hostname := "nb-192-168-0-1.newark.nodebalancer.linode.com"
+	nb := &linodego.NodeBalancer{
+		IPv4:     &ipv4,
+		Hostname: &hostname,
+	}
+
+	svc := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        "test",
+			Annotations: make(map[string]string, 1),
+		},
+	}
+
+	expectedStatus := &v1.LoadBalancerStatus{
+		Ingress: []v1.LoadBalancerIngress{{
+			Hostname: hostname,
+			IP:       ipv4,
+		}},
+	}
+	status := makeLoadBalancerStatus(svc, nb)
+	if !reflect.DeepEqual(status, expectedStatus) {
+		t.Errorf("expected status for basic service to be %#v; got %#v", expectedStatus, status)
+	}
+
+	os.Setenv("LINODE_HOSTNAME_ONLY_INGRESS", "true")
+	expectedStatus.Ingress[0] = v1.LoadBalancerIngress{Hostname: hostname}
+	status = makeLoadBalancerStatus(svc, nb)
+	if !reflect.DeepEqual(status, expectedStatus) {
+		t.Errorf("expected status for %q annotated service to be %#v; got %#v", annLinodeHostnameOnlyIngress, expectedStatus, status)
+	}
+
+	os.Setenv("LINODE_HOSTNAME_ONLY_INGRESS", "false")
+	expectedStatus.Ingress[0] = v1.LoadBalancerIngress{Hostname: hostname}
+	status = makeLoadBalancerStatus(svc, nb)
+	if reflect.DeepEqual(status, expectedStatus) {
+		t.Errorf("expected status for %q annotated service to be %#v; got %#v", annLinodeHostnameOnlyIngress, expectedStatus, status)
+	}
+
+	os.Setenv("LINODE_HOSTNAME_ONLY_INGRESS", "banana")
+	expectedStatus.Ingress[0] = v1.LoadBalancerIngress{Hostname: hostname}
+	status = makeLoadBalancerStatus(svc, nb)
+	if reflect.DeepEqual(status, expectedStatus) {
+		t.Errorf("expected status for %q annotated service to be %#v; got %#v", annLinodeHostnameOnlyIngress, expectedStatus, status)
+	}
+	os.Unsetenv("LINODE_HOSTNAME_ONLY_INGRESS")
+}
+
 func testCleanupDoesntCall(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) {
 	region := "us-west"
 	nb1, err := client.CreateNodeBalancer(context.TODO(), linodego.NodeBalancerCreateOptions{Region: region})