Skip to content

Commit

Permalink
load-balancer-address support for ClusterIP
Browse files Browse the repository at this point in the history
- Support retrieving ClusterIP services with the load-balancer-address
command.
- Rename load-balancer-addresss to service-address since it no longer
applies to just load balancer services.
  • Loading branch information
lkysow committed Mar 23, 2020
1 parent 6c6e52f commit ddbdd57
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 63 deletions.
6 changes: 3 additions & 3 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
cmdDeleteCompletedJob "github.com/hashicorp/consul-k8s/subcommand/delete-completed-job"
cmdInjectConnect "github.com/hashicorp/consul-k8s/subcommand/inject-connect"
cmdLifecycleSidecar "github.com/hashicorp/consul-k8s/subcommand/lifecycle-sidecar"
cmdLoadBalancerAddress "github.com/hashicorp/consul-k8s/subcommand/load-balancer-address"
cmdServerACLInit "github.com/hashicorp/consul-k8s/subcommand/server-acl-init"
cmdServiceAddress "github.com/hashicorp/consul-k8s/subcommand/service-address"
cmdSyncCatalog "github.com/hashicorp/consul-k8s/subcommand/sync-catalog"
cmdVersion "github.com/hashicorp/consul-k8s/subcommand/version"
"github.com/hashicorp/consul-k8s/version"
Expand Down Expand Up @@ -46,8 +46,8 @@ func init() {
return &cmdDeleteCompletedJob.Command{UI: ui}, nil
},

"load-balancer-address": func() (cli.Command, error) {
return &cmdLoadBalancerAddress.Command{UI: ui}, nil
"service-address": func() (cli.Command, error) {
return &cmdServiceAddress.Command{UI: ui}, nil
},

"version": func() (cli.Command, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package loadbalanceraddress
package serviceaddress

import (
"errors"
Expand All @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/cli"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
Expand Down Expand Up @@ -80,23 +81,44 @@ func (c *Command) Run(args []string) int {

// Run until we get an address from the service.
var address string
var unretryableErr error
backoff.Retry(withErrLogger(log, func() error {
svc, err := c.k8sClient.CoreV1().Services(c.flagNamespace).Get(c.flagServiceName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("getting service %s: %s", c.flagServiceName, err)
}
for _, ingr := range svc.Status.LoadBalancer.Ingress {
if ingr.IP != "" {
address = ingr.IP
return nil
} else if ingr.Hostname != "" {
address = ingr.Hostname
return nil
switch svc.Spec.Type {
case v1.ServiceTypeClusterIP:
address = svc.Spec.ClusterIP
return nil
case v1.ServiceTypeNodePort:
unretryableErr = errors.New("services of type NodePort are not supported")
return nil
case v1.ServiceTypeExternalName:
unretryableErr = errors.New("services of type ExternalName are not supported")
return nil
case v1.ServiceTypeLoadBalancer:
for _, ingr := range svc.Status.LoadBalancer.Ingress {
if ingr.IP != "" {
address = ingr.IP
return nil
} else if ingr.Hostname != "" {
address = ingr.Hostname
return nil
}
}
return fmt.Errorf("service %s has no ingress IP or hostname", c.flagServiceName)
default:
unretryableErr = fmt.Errorf("unknown service type %q", svc.Spec.Type)
return nil
}
return fmt.Errorf("service %s has no ingress IP or hostname", c.flagServiceName)
}), backoff.NewConstantBackOff(c.retryDuration))

if unretryableErr != nil {
c.UI.Error(unretryableErr.Error())
return 1
}

// Write the address to file.
err := ioutil.WriteFile(c.flagOutputFile, []byte(address), 0600)
if err != nil {
Expand Down Expand Up @@ -145,12 +167,15 @@ func (c *Command) Help() string {
return c.help
}

const synopsis = "Output Kubernetes LoadBalancer service ingress address to file"
const synopsis = "Output Kubernetes Service address to file"
const help = `
Usage: consul-k8s load-balancer-address [options]
Usage: consul-k8s service-address [options]
Waits until the Kubernetes service specified by -name in namespace
-k8s-namespace is created and has an ingress address. Then writes the
address to -output-file.
-k8s-namespace is created, then writes its address to -output-file.
The address written depends on service type:
ClusterIP - Cluster IP
NodePort - Not supported
LoadBalancer - External ingress IP or hostname
ExternalName - Not Supported
`
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package loadbalanceraddress
package serviceaddress

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes/fake"
)

Expand Down Expand Up @@ -57,7 +59,7 @@ func TestRun_UnableToWriteToFile(t *testing.T) {

// Create the service.
k8s := fake.NewSimpleClientset()
_, err := k8s.CoreV1().Services(k8sNS).Create(kubeSvc(svcName, expAddress, ""))
_, err := k8s.CoreV1().Services(k8sNS).Create(kubeLoadBalancerSvc(svcName, expAddress, ""))
require.NoError(err)

// Run command with an unwriteable file.
Expand All @@ -76,39 +78,64 @@ func TestRun_UnableToWriteToFile(t *testing.T) {
"Unable to write address to file: open /this/filepath/does/not/exist: no such file or directory")
}

// Test running with different permutations of ingress.
func TestRun_LoadBalancerIngressPermutations(t *testing.T) {
// Test running with different service types.
func TestRun_ServiceTypes(t *testing.T) {
t.Parallel()
require := require.New(t)

// All services will have the name "service-name"
cases := map[string]struct {
Ingress []v1.LoadBalancerIngress
ExpAddress string
Service *v1.Service
ServiceModificationF func(*v1.Service)
ExpErr string
ExpAddress string
}{
"ip": {
Ingress: []v1.LoadBalancerIngress{
{
IP: "1.2.3.4",
},
},
"ClusterIP": {
Service: kubeClusterIPSvc("service-name"),
ExpAddress: "5.6.7.8",
},
"NodePort": {
Service: kubeNodePortSvc("service-name"),
ExpErr: "services of type NodePort are not supported",
},
"LoadBalancer IP": {
Service: kubeLoadBalancerSvc("service-name", "1.2.3.4", ""),
ExpAddress: "1.2.3.4",
},
"hostname": {
Ingress: []v1.LoadBalancerIngress{
{
Hostname: "example.com",
},
},
"LoadBalancer Hostname": {
Service: kubeLoadBalancerSvc("service-name", "", "example.com"),
ExpAddress: "example.com",
},
"empty first ingress": {
Ingress: []v1.LoadBalancerIngress{
{},
{
IP: "1.2.3.4",
"LoadBalancer IP and hostname": {
Service: kubeLoadBalancerSvc("service-name", "1.2.3.4", "example.com"),
ExpAddress: "1.2.3.4",
},
"LoadBalancer first ingress empty": {
Service: kubeLoadBalancerSvc("service-name", "1.2.3.4", "example.com"),
ServiceModificationF: func(svc *v1.Service) {
svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{
{},
{
IP: "5.6.7.8",
},
}
},
ExpAddress: "5.6.7.8",
},
"ExternalName": {
Service: kubeExternalNameSvc("service-name"),
ExpErr: "services of type ExternalName are not supported",
},
"invalid name": {
Service: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "service-name",
},
Spec: v1.ServiceSpec{
Type: "invalid",
},
},
ExpAddress: "1.2.3.4",
ExpErr: "unknown service type \"invalid\"",
},
}

Expand All @@ -119,17 +146,10 @@ func TestRun_LoadBalancerIngressPermutations(t *testing.T) {

// Create the service.
k8s := fake.NewSimpleClientset()
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: c.Ingress,
},
},
if c.ServiceModificationF != nil {
c.ServiceModificationF(c.Service)
}
_, err := k8s.CoreV1().Services(k8sNS).Create(svc)
_, err := k8s.CoreV1().Services(k8sNS).Create(c.Service)
require.NoError(err)

// Run command.
Expand All @@ -148,11 +168,15 @@ func TestRun_LoadBalancerIngressPermutations(t *testing.T) {
"-name", svcName,
"-output-file", outputFile,
})
require.Equal(0, responseCode, ui.ErrorWriter.String())
actAddressBytes, err := ioutil.ReadFile(outputFile)
require.NoError(err)
require.Equal(c.ExpAddress, string(actAddressBytes))

if c.ExpErr != "" {
require.Equal(1, responseCode)
require.Contains(ui.ErrorWriter.String(), c.ExpErr)
} else {
require.Equal(0, responseCode, ui.ErrorWriter.String())
actAddressBytes, err := ioutil.ReadFile(outputFile)
require.NoError(err)
require.Equal(c.ExpAddress, string(actAddressBytes))
}
})
}
}
Expand Down Expand Up @@ -194,18 +218,17 @@ func TestRun_FileWrittenAfterRetry(t *testing.T) {
k8s := fake.NewSimpleClientset()

if c.InitialService {
_, err := k8s.CoreV1().Services(k8sNS).Create(&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
})
svc := kubeLoadBalancerSvc(svcName, "", "")
// Reset the status to nothing.
svc.Status = v1.ServiceStatus{}
_, err := k8s.CoreV1().Services(k8sNS).Create(svc)
require.NoError(t, err)
}

// Create/update the service after delay.
go func() {
time.Sleep(c.UpdateDelay)
svc := kubeSvc(svcName, ip, "")
svc := kubeLoadBalancerSvc(svcName, ip, "")
var err error
if c.InitialService {
_, err = k8s.CoreV1().Services(k8sNS).Update(svc)
Expand Down Expand Up @@ -240,11 +263,26 @@ func TestRun_FileWrittenAfterRetry(t *testing.T) {
}
}

func kubeSvc(name string, ip string, hostname string) *v1.Service {
func kubeLoadBalancerSvc(name string, ip string, hostname string) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.ServiceSpec{
Type: "LoadBalancer",
ClusterIP: "9.0.1.2",
Ports: []v1.ServicePort{
{
Name: "http",
Protocol: "TCP",
Port: 80,
TargetPort: intstr.IntOrString{
IntVal: 8080,
},
NodePort: 32001,
},
},
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
Expand All @@ -257,3 +295,60 @@ func kubeSvc(name string, ip string, hostname string) *v1.Service {
},
}
}

func kubeNodePortSvc(name string) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.ServiceSpec{
Type: "NodePort",
ClusterIP: "1.2.3.4",
Ports: []v1.ServicePort{
{
Name: "http",
Protocol: "TCP",
Port: 80,
TargetPort: intstr.IntOrString{
IntVal: 8080,
},
NodePort: 32000,
},
},
},
}
}

func kubeClusterIPSvc(name string) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.ServiceSpec{
Type: "ClusterIP",
ClusterIP: "5.6.7.8",
Ports: []v1.ServicePort{
{
Name: "http",
Protocol: "TCP",
Port: 80,
TargetPort: intstr.IntOrString{
IntVal: 8080,
},
},
},
},
}
}

func kubeExternalNameSvc(name string) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.ServiceSpec{
Type: "ExternalName",
ExternalName: fmt.Sprintf("%s.example.com", name),
},
}
}

0 comments on commit ddbdd57

Please sign in to comment.