Skip to content

Commit

Permalink
Merge pull request #6 from numberly/deduplicate-multiple-ingresses
Browse files Browse the repository at this point in the history
Add healthcheck-host annotation, deduplicate redundant cluster ingresses
  • Loading branch information
Aluxima authored May 23, 2024
2 parents cf0e6cd + 1ab969e commit 1585f99
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
bin/
command
testing
ca
config
envoy
launch.json
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Yggdrasil allows for some customisation of the route and cluster config per Ingr
| Name | type |
|--------------------------------------------------------------|----------|
| [yggdrasil.uswitch.com/healthcheck-path](#health-check-path) | string |
| [yggdrasil.uswitch.com/healthcheck-host](#health-check-host) | string |
| [yggdrasil.uswitch.com/timeout](#timeouts) | duration |
| [yggdrasil.uswitch.com/cluster-timeout](#timeouts) | duration |
| [yggdrasil.uswitch.com/route-timeout](#timeouts) | duration |
Expand All @@ -85,6 +86,9 @@ Yggdrasil allows for some customisation of the route and cluster config per Ingr
### Health Check Path
Specifies a path to configure a [HTTP health check](https://www.envoyproxy.io/docs/envoy/v1.19.0/api-v3/config/core/v3/health_check.proto#config-core-v3-healthcheck-httphealthcheck) to. Envoy will not route to clusters that fail health checks.

### Health Check Host
Permit to change the host of the healthcheck when using wildcard. Example: healthcheck for `*.my-app.example.com` can't work natively, you can configure a specific path with `yggdrasil.uswitch.com/healthcheck-host: health.my-app.example.com`.

* [config.core.v3.HealthCheck.HttpHealthCheck.Path](https://www.envoyproxy.io/docs/envoy/v1.19.0/api-v3/config/core/v3/health_check.proto#envoy-v3-api-field-config-core-v3-healthcheck-httphealthcheck-path)

### Timeouts
Expand Down
7 changes: 3 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -214,12 +213,12 @@ func main(*cobra.Command, []string) error {
certPath := certificate.Cert
keyPath := certificate.Key

certBytes, err := ioutil.ReadFile(certPath)
certBytes, err := os.ReadFile(certPath)
if err != nil {
log.Fatalf("Failed to read %s: %v", certPath, err)
}

keyBytes, err := ioutil.ReadFile(keyPath)
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
log.Fatalf("Failed to read %s: %v", keyPath, err)
}
Expand Down Expand Up @@ -286,7 +285,7 @@ func createSources(clusters []clusterConfig) ([]*kubernetes.Clientset, error) {
var token string

if cluster.TokenPath != "" {
bytes, err := ioutil.ReadFile(cluster.TokenPath)
bytes, err := os.ReadFile(cluster.TokenPath)
if err != nil {
return sources, err
}
Expand Down
1 change: 1 addition & 0 deletions envoy
Submodule envoy added at db32ac
22 changes: 22 additions & 0 deletions launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--config=./config/config.json",
"--upstream-port=443",
"--ca=/etc/ssl/certs/",
"--envoy-port=443",
"--envoy-listener-ipv4-address=127.0.0.1",
"--max-ejection-percentage=100",
"--retry-on", "connect-failure,5xx"]
}
]
}
2 changes: 1 addition & 1 deletion pkg/envoy/boilerplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ func makeCluster(c cluster, ca string, healthCfg UpstreamHealthCheck, outlierPer
}
}

healthChecks := makeHealthChecks(c.VirtualHost, c.HealthCheckPath, healthCfg)
healthChecks := makeHealthChecks(c.HealthCheckHost, c.HealthCheckPath, healthCfg)

endpoints := make([]*endpoint.LbEndpoint, len(addresses))

Expand Down
53 changes: 52 additions & 1 deletion pkg/envoy/ingress_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type cluster struct {
Name string
VirtualHost string
HealthCheckPath string
HealthCheckHost string // with Wildcard, the HealthCheck host can be different than the VirtualHost
HttpVersion string
Timeout time.Duration
Hosts []LBHost
Expand All @@ -126,6 +127,10 @@ func (c *cluster) Equals(other *cluster) bool {
return false
}

if c.HealthCheckHost != other.HealthCheckHost {
return false
}

if c.HealthCheckPath != other.HealthCheckPath {
return false
}
Expand Down Expand Up @@ -215,18 +220,36 @@ func newEnvoyIngress(host string, timeouts DefaultTimeouts) *envoyIngress {
Hosts: []LBHost{},
Timeout: timeouts.Cluster,
HealthCheckPath: "",
HealthCheckHost: host,
},
}
}

func (ing *envoyIngress) addUpstream(host string, weight uint32) {
ing.cluster.Hosts = append(ing.cluster.Hosts, LBHost{host, weight})
// Check if the host is already in the list
// If we wan't to avoid using a for loop, maybe we could implement a Map for a faster lookup.
// time complexity O(1) vs 0(n) for each iteration.
for _, h := range ing.cluster.Hosts {
if h.Host == host {
// Host found, so we don't add the duplicate
logrus.Debugf("Duplicate host found for upstream, not adding : %s for cluster : %s", host, ing.cluster.Name)
return
}
}

// No duplicate found, append the new host
ing.cluster.Hosts = append(ing.cluster.Hosts, LBHost{Host: host, Weight: weight})
logrus.Debugf("Host added on upstream list : %s for cluster : %s", host, ing.cluster.Name)
}

func (ing *envoyIngress) addHealthCheckPath(path string) {
ing.cluster.HealthCheckPath = path
}

func (ing *envoyIngress) addHealthCheckHost(host string) {
ing.cluster.HealthCheckHost = host
}

func (ing *envoyIngress) addTimeout(timeout time.Duration) {
ing.cluster.Timeout = timeout
ing.vhost.Timeout = timeout
Expand Down Expand Up @@ -329,13 +352,29 @@ func (envoyIng *envoyIngress) addRetryOn(ingress *k8s.Ingress) {
}
}

// isWildcard checks if the given host rule is a wildcard.
func isWildcard(ruleHost string) bool {
// Check if the ruleHost starts with '*.'
return strings.HasPrefix(ruleHost, "*.")
}

func validateSubdomain(ruleHost, host string) bool {
if strings.HasPrefix(ruleHost, "*.") {
ruleHost = ruleHost[2:]
}
return strings.HasSuffix(host, ruleHost)
}

func translateIngresses(ingresses []*k8s.Ingress, syncSecrets bool, secrets []*v1.Secret, timeouts DefaultTimeouts) *envoyConfiguration {
cfg := &envoyConfiguration{}
envoyIngresses := map[string]*envoyIngress{}

for _, i := range ingresses {
for _, j := range i.Upstreams {
for _, ruleHost := range i.RulesHosts {

isWildcard := isWildcard(ruleHost)

_, ok := envoyIngresses[ruleHost]
if !ok {
envoyIngresses[ruleHost] = newEnvoyIngress(ruleHost, timeouts)
Expand All @@ -351,6 +390,18 @@ func translateIngresses(ingresses []*k8s.Ingress, syncSecrets bool, secrets []*v
envoyIngress.addUpstream(j, 1)
}

if isWildcard {
if i.Annotations["yggdrasil.uswitch.com/healthcheck-host"] != "" {
envoyIngress.addHealthCheckHost(i.Annotations["yggdrasil.uswitch.com/healthcheck-host"])
if !validateSubdomain(ruleHost, envoyIngress.cluster.HealthCheckHost) {
logrus.Warnf("Healthcheck %s is not on the same subdomain for %s, annotation will be skipped", envoyIngress.cluster.HealthCheckHost, ruleHost)
envoyIngress.cluster.HealthCheckHost = ruleHost
}
} else {
logrus.Warnf("Be careful, healthcheck can't work for wildcard host : %s", envoyIngress.cluster.HealthCheckHost)
}
}

if i.Annotations["yggdrasil.uswitch.com/healthcheck-path"] != "" {
envoyIngress.addHealthCheckPath(i.Annotations["yggdrasil.uswitch.com/healthcheck-path"])
}
Expand Down

0 comments on commit 1585f99

Please sign in to comment.