Skip to content

Commit a63bd08

Browse files
Simple K8s API discovery method (#68)
* Simple K8s API discovery method * Need to synchronize access to discovered * Support for discovering nodes as well as services * Add all tests for K8s API * Update README Co-Authored-By: "Gavin Heavyside" <[email protected]>
1 parent 302d631 commit a63bd08

File tree

13 files changed

+926
-60
lines changed

13 files changed

+926
-60
lines changed

README.md

+28-7
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Defaults are in bold at the end of the line:
165165
**info**
166166
* `SIDECAR_LOGGING_FORMAT`: Logging format to use (text, json) **text**
167167
* `SIDECAR_DISCOVERY`: Which discovery backends to use as a csv array
168-
(static, docker) **`[ docker ]`**
168+
(static, docker, kubernetes_api) **`[ docker ]`**
169169
* `SIDECAR_SEEDS`: csv array of IP addresses used to seed the cluster.
170170
* `SIDECAR_CLUSTER_NAME`: The name of the Sidecar cluster. Restricts membership
171171
to hosts with the same cluster name.
@@ -226,6 +226,13 @@ Defaults are in bold at the end of the line:
226226
* `ENVOY_GRPC_PORT`: The port for the Envoy API gRPC server **`7776`**
227227

228228

229+
* `KUBE_API_IP`: The IP address at which to reach the Kubernetes API **`127.0.0.1`**
230+
* `KUBE_API_PORT`: The port to use to contact the Kubernetes API **`8080`**
231+
* `NAMESPACE`: The namespace against which we should do discovery **`default`**
232+
* `KUBE_TIMEOUT`: How long until we time out calling the Kube API? **`3s`**
233+
* `CREDS_PATH`: Where do we find the token file containing API auth credentials?
234+
**`/var/run/secrets/kubernetes.io/serviceaccount`**
235+
229236
### Ports
230237

231238
Sidecar requires both TCP and UDP protocols be open on the port configured via
@@ -235,14 +242,14 @@ protocol (Memberlist) runs on.
235242

236243
## Discovery
237244

238-
Sidecar supports both Docker-based discovery and a discovery mechanism where
239-
you publish services into a JSON file locally, called "static". These can then
240-
be advertised as running services just like they would be from a Docker host.
241-
These are configured with the `SIDECAR_DISCOVERY` environment variable. Using
242-
both would look like:
245+
Sidecar supports Docker-based discovery, a discovery mechanism where you
246+
publish services into a JSON file locally, called "static", and from the
247+
Kubernetes API. These can then be advertised as running services just like they
248+
would be from a Docker host. These are configured with the `SIDECAR_DISCOVERY`
249+
environment variable. Using all of them would look like:
243250

244251
```bash
245-
export SIDECAR_DISCOVERY=static,docker
252+
export SIDECAR_DISCOVERY=static,docker,kubernetes_api
246253
```
247254

248255
Zero or more options may be supplied. Note that if nothing is in this section,
@@ -396,6 +403,20 @@ web UI.
396403

397404
A further example is available in the `fixtures/` directory used by the tests.
398405

406+
### Configuring Kubernetes API Discovery
407+
408+
This method of discovery will enale you to bridge together an existing Sidecar
409+
cluster with a Kubernetes cluster that will make services availabel to the
410+
Sidecar cluster. It will announce all of the Kubernetes services that it finds
411+
available and map them to a port in the 30000+ range, with the expectation
412+
being that your have configured services to run with a NodePort in that range.
413+
414+
This is most useful for transitioning services from one cluster to another. You
415+
can run one or more Sidecar instances per Kubernetes cluster and they will show
416+
up like services exported from other discovery mechanisms with the exception
417+
that version information is not passed. The environment variables for
418+
configuring the behavior of this discovery method are described above.
419+
399420
Sidecar Events and Listeners
400421
----------------------------
401422

config/config.go

+26-14
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,22 @@ type ServicesConfig struct {
3939
}
4040

4141
type SidecarConfig struct {
42-
ExcludeIPs []string `envconfig:"EXCLUDE_IPS" default:"192.168.168.168"`
43-
Discovery []string `envconfig:"DISCOVERY" default:"docker"`
44-
StatsAddr string `envconfig:"STATS_ADDR"`
45-
PushPullInterval time.Duration `envconfig:"PUSH_PULL_INTERVAL" default:"20s"`
46-
GossipMessages int `envconfig:"GOSSIP_MESSAGES" default:"15"`
47-
GossipInterval time.Duration `envconfig:"GOSSIP_INTERVAL" default:"200ms"`
48-
HandoffQueueDepth int `envconfig:"HANDOFF_QUEUE_DEPTH" default:"1024"`
49-
LoggingFormat string `envconfig:"LOGGING_FORMAT"`
50-
LoggingLevel string `envconfig:"LOGGING_LEVEL" default:"info"`
51-
DefaultCheckEndpoint string `envconfig:"DEFAULT_CHECK_ENDPOINT" default:"/version"`
52-
Seeds []string `envconfig:"SEEDS"`
53-
ClusterName string `envconfig:"CLUSTER_NAME" default:"default"`
54-
AdvertiseIP string `envconfig:"ADVERTISE_IP"`
55-
BindPort int `envconfig:"BIND_PORT" default:"7946"`
42+
ExcludeIPs []string `envconfig:"EXCLUDE_IPS" default:"192.168.168.168"`
43+
Discovery []string `envconfig:"DISCOVERY" default:"docker"`
44+
StatsAddr string `envconfig:"STATS_ADDR"`
45+
PushPullInterval time.Duration `envconfig:"PUSH_PULL_INTERVAL" default:"20s"`
46+
GossipMessages int `envconfig:"GOSSIP_MESSAGES" default:"15"`
47+
GossipInterval time.Duration `envconfig:"GOSSIP_INTERVAL" default:"200ms"`
48+
HandoffQueueDepth int `envconfig:"HANDOFF_QUEUE_DEPTH" default:"1024"`
49+
LoggingFormat string `envconfig:"LOGGING_FORMAT"`
50+
LoggingLevel string `envconfig:"LOGGING_LEVEL" default:"info"`
51+
DefaultCheckEndpoint string `envconfig:"DEFAULT_CHECK_ENDPOINT" default:"/version"`
52+
Seeds []string `envconfig:"SEEDS"`
53+
ClusterName string `envconfig:"CLUSTER_NAME" default:"default"`
54+
AdvertiseIP string `envconfig:"ADVERTISE_IP"`
55+
BindPort int `envconfig:"BIND_PORT" default:"7946"`
56+
Debug bool `envconfig:"DEBUG" default:"false"`
57+
DiscoverySleepInterval time.Duration `envconfig:"DISCOVERY_SLEEP_INTERVAL" default:"1s"`
5658
}
5759

5860
type DockerConfig struct {
@@ -63,10 +65,19 @@ type StaticConfig struct {
6365
ConfigFile string `envconfig:"CONFIG_FILE" default:"static.json"`
6466
}
6567

68+
type K8sAPIConfig struct {
69+
KubeAPIIP string `envconfig:"KUBE_API_IP" default:"127.0.0.1"`
70+
KubeAPIPort int `envconfig:"KUBE_API_PORT" default:"8080"`
71+
Namespace string `envconfig:"NAMESPACE" default:"default"`
72+
KubeTimeout time.Duration `envconfig:"KUBE_TIMEOUT" default:"3s"`
73+
CredsPath string `envconfig:"CREDS_PATH" default:"/var/run/secrets/kubernetes.io/serviceaccount"`
74+
}
75+
6676
type Config struct {
6777
Sidecar SidecarConfig // SIDECAR_
6878
DockerDiscovery DockerConfig // DOCKER_
6979
StaticDiscovery StaticConfig // STATIC_
80+
K8sAPIDiscovery K8sAPIConfig // K8S_
7081
Services ServicesConfig // SERVICES_
7182
HAproxy HAproxyConfig // HAPROXY_
7283
Envoy EnvoyConfig // ENVOY_
@@ -80,6 +91,7 @@ func ParseConfig() *Config {
8091
envconfig.Process("sidecar", &config.Sidecar),
8192
envconfig.Process("docker", &config.DockerDiscovery),
8293
envconfig.Process("static", &config.StaticDiscovery),
94+
envconfig.Process("k8s", &config.K8sAPIDiscovery),
8395
envconfig.Process("services", &config.Services),
8496
envconfig.Process("haproxy", &config.HAproxy),
8597
envconfig.Process("envoy", &config.Envoy),

discovery/fixtures/bad-fixture/token

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this would be a token

discovery/fixtures/ca.crt

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDPDCCAiQCCQCKqd63pT0THjANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJn
3+
YjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xFjAUBgNVBAoMDUNv
4+
bW11bml0eS5jb20xFjAUBgNVBAMMDWNvbW11bml0eS5jb20wIBcNMjIxMTIyMTEy
5+
MzQ1WhgPMzAyMjAzMjUxMTIzNDVaMF8xCzAJBgNVBAYTAmdiMQ8wDQYDVQQIDAZM
6+
b25kb24xDzANBgNVBAcMBkxvbmRvbjEWMBQGA1UECgwNQ29tbXVuaXR5LmNvbTEW
7+
MBQGA1UEAwwNY29tbXVuaXR5LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
8+
AQoCggEBAKO3y1q/ELzfZ4vaLptkxy45RNLSgGwE63J6kXSjfsyGLoOg4ZHaqbWX
9+
y/7EsLeHKI1kzj9qV0ygaexbTgAfTKBQZoXxHgMuGMCQRy/SSlNHwG4W7IkJ+bFf
10+
hja2KDleY26ROq1vbTODu50408Mm50c5ynU05Qcu/jDGcHkM6dkb93T7aKzWLAgf
11+
aKxtAnYuHq2MvI+y3E0Qeqo78aaoRYw6L2WSn6xGQoKjStdCb1vRb5xX8Ed8kUDf
12+
cWY6EhsA3dUb+5vBEIhwnzkojJyeCteG//mWrd52ntMoFooEKvcXrFZj70kVJgYo
13+
V7yDNv6SraIxfXIDlsyY+w8wtGDmNX0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
14+
L4UyZ2IwGq0HFhephmEHc+3NX2tpEIyjKaDdd2VnDKDNgJfXRjzaV4PxrPzlsN3H
15+
unrpRGLzjIy+PVUjhKOpoMzZDdGAXSu6Zpi4wlZ8PdFJbD+CM7WEQGJNytbO6nkW
16+
r5RvlWGFrR52DB5XqyVTwfGr4ugI0C0H0dyfJOVn67m6gqZnntJ9dAHCFPbRKFxo
17+
JrO2Rdx1ZimxVKkK+Gs7bqjmx1Mj4GBm/NOGVwdMBDGWb57dFXnh93xxFnruQ1QR
18+
lXu6dYfcmewOyLDc5WmFXzsGzhSDjPmHi7BjpTbRaQ4h32J0Rv9/9R3mivC7nflt
19+
lztQHTUAk1+on3ARvoERWQ==
20+
-----END CERTIFICATE-----

discovery/fixtures/token

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this would be a token

discovery/kubernetes_api_discovery.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package discovery
2+
3+
import (
4+
"encoding/json"
5+
"sync"
6+
"time"
7+
8+
"github.com/NinesStack/sidecar/service"
9+
"github.com/relistan/go-director"
10+
log "github.com/sirupsen/logrus"
11+
)
12+
13+
// A K8sAPIDiscoverer is a discovery mechanism that assumes that a K8s cluster
14+
// with be fronted by a load balancer and that all the ports exposed will match
15+
// up on both the load balancer and the backing pods. It relies on an underlying
16+
// command to run the discovery. This is normally `kubectl`.
17+
type K8sAPIDiscoverer struct {
18+
Namespace string
19+
20+
Command K8sDiscoveryAdapter
21+
22+
discoveredSvcs *K8sServices
23+
discoveredNodes *K8sNodes
24+
lock sync.RWMutex
25+
}
26+
27+
// NewK8sAPIDiscoverer returns a properly configured K8sAPIDiscoverer
28+
func NewK8sAPIDiscoverer(kubeHost string, kubePort int, namespace string, timeout time.Duration, credsPath string) *K8sAPIDiscoverer {
29+
30+
cmd := NewKubeAPIDiscoveryCommand(kubeHost, kubePort, namespace, timeout, credsPath)
31+
32+
return &K8sAPIDiscoverer{
33+
discoveredSvcs: &K8sServices{},
34+
discoveredNodes: &K8sNodes{},
35+
Namespace: namespace,
36+
Command: cmd,
37+
}
38+
}
39+
40+
// Services implements part of the Discoverer interface and looks at the last
41+
// cached data from the Command (`kubectl`) and returns services in a format
42+
// that Sidecar can manage.
43+
func (k *K8sAPIDiscoverer) Services() []service.Service {
44+
k.lock.RLock()
45+
defer k.lock.RUnlock()
46+
47+
// Enumerate all the K8s nodes we discovered, and for each one, emit all the
48+
// services that we separately discovered. This means we will attempt to hit
49+
// the NodePort for each of the nodes when looking for this service.
50+
var services []service.Service
51+
for _, node := range k.discoveredNodes.Items {
52+
hostname, ip := getIPHostForNode(&node)
53+
54+
for _, item := range k.discoveredSvcs.Items {
55+
svc := service.Service{
56+
ID: item.Metadata.UID,
57+
Name: item.Metadata.Labels.ServiceName,
58+
Image: item.Metadata.Labels.ServiceName + ":kubernetes-hosted",
59+
Created: item.Metadata.CreationTimestamp,
60+
Hostname: hostname,
61+
ProxyMode: "http",
62+
Status: service.ALIVE,
63+
Updated: time.Now().UTC(),
64+
}
65+
for _, port := range item.Spec.Ports {
66+
svc.Ports = append(svc.Ports, service.Port{
67+
Type: "tcp",
68+
Port: int64(port.NodePort),
69+
ServicePort: int64(port.Port),
70+
IP: ip,
71+
})
72+
}
73+
services = append(services, svc)
74+
}
75+
}
76+
77+
return services
78+
}
79+
80+
func getIPHostForNode(node *K8sNode) (hostname string, ip string) {
81+
for _, address := range node.Status.Addresses {
82+
if address.Type == "InternalIP" {
83+
ip = address.Address
84+
}
85+
86+
if address.Type == "Hostname" {
87+
hostname = address.Address
88+
}
89+
}
90+
91+
return hostname, ip
92+
}
93+
94+
// HealthCheck implements part of the Discoverer interface and returns the
95+
// built-in AlwaysSuccessful check, on the assumption that the underlying load
96+
// balancer we are pointing to will have already health checked the service.
97+
func (k *K8sAPIDiscoverer) HealthCheck(svc *service.Service) (string, string) {
98+
return "AlwaysSuccessful", ""
99+
}
100+
101+
// Listeners implements part of the Discoverer interface and always returns
102+
// an empty list because it doesn't make sense in this context.
103+
func (k *K8sAPIDiscoverer) Listeners() []ChangeListener {
104+
return []ChangeListener{}
105+
}
106+
107+
// Run is part of the Discoverer interface and calls the Command in a loop,
108+
// which is injected as a Looper.
109+
func (k *K8sAPIDiscoverer) Run(looper director.Looper) {
110+
looper.Loop(func() error {
111+
data, err := k.getServices()
112+
if err != nil {
113+
log.Errorf("Failed to unmarshal services json: %s, %s", err, string(data))
114+
}
115+
116+
data, err = k.getNodes()
117+
if err != nil {
118+
log.Errorf("Failed to unmarshal nodes json: %s, %s", err, string(data))
119+
}
120+
121+
return nil
122+
})
123+
}
124+
125+
func (k *K8sAPIDiscoverer) getServices() ([]byte, error) {
126+
data, err := k.Command.GetServices()
127+
if err != nil {
128+
log.Errorf("Failed to invoke K8s API discovery: %s", err)
129+
}
130+
131+
k.lock.Lock()
132+
err = json.Unmarshal(data, &k.discoveredSvcs)
133+
k.lock.Unlock()
134+
return data, err
135+
}
136+
137+
func (k *K8sAPIDiscoverer) getNodes() ([]byte, error) {
138+
data, err := k.Command.GetNodes()
139+
if err != nil {
140+
log.Errorf("Failed to invoke K8s API discovery: %s", err)
141+
}
142+
143+
k.lock.Lock()
144+
err = json.Unmarshal(data, &k.discoveredNodes)
145+
k.lock.Unlock()
146+
return data, err
147+
}

0 commit comments

Comments
 (0)