Skip to content

Commit

Permalink
show number of hosts for each url
Browse files Browse the repository at this point in the history
  • Loading branch information
reddec committed Dec 16, 2021
1 parent 7d819f5 commit a9a7d86
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 34 deletions.
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ Features:
* Automatic discovery of Ingress objects, configurable by annotations
* Supports static configuration (in addition to Ingress objects)
* Multiarch docker images: for amd64 and for arm64
* Automatic even-based updates

Notes:
Limitations:

* Supports only v1/Ingress kind.
* Doesn't support Ingress Reference kind, only Service type
* Hosts number per Ingress calculated each Ingress update or after refresh (30s by default)

<img alt="image" src="https://user-images.githubusercontent.com/6597086/145249365-52035d08-469d-460e-b42c-e6af5d271e10.png">
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ require (
github.com/coreos/go-oidc/v3 v3.1.0
github.com/jessevdk/go-flags v1.5.0
github.com/reddec/run-http-server v0.0.0-20211110142919-824037361ead
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/api v0.22.4
k8s.io/apimachinery v0.22.4
k8s.io/client-go v0.22.4
)

Expand All @@ -27,6 +29,7 @@ require (
github.com/json-iterator/go v1.1.11 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
Expand All @@ -38,7 +41,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apimachinery v0.22.4 // indirect
k8s.io/klog/v2 v2.9.0 // indirect
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
Expand Down
6 changes: 6 additions & 0 deletions ingress-dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ rules:
- namespaces
verbs:
- get
- apiGroups:
- ''
resources:
- services
verbs:
- get
- apiGroups:
- networking.k8s.io
resources:
Expand Down
65 changes: 49 additions & 16 deletions internal/kube_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package internal

import (
"context"
"fmt"
"log"
"sort"
"strconv"
"sync"
"time"

v12 "k8s.io/api/networking/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
Expand All @@ -30,7 +33,7 @@ func WatchKubernetes(global context.Context, clientset *kubernetes.Clientset, re
ctx, cancel := context.WithCancel(global)
defer cancel()

watcher := newWatcher(reciever)
watcher := newWatcher(ctx, reciever, clientset)

var wg sync.WaitGroup

Expand All @@ -50,27 +53,31 @@ func WatchKubernetes(global context.Context, clientset *kubernetes.Clientset, re
wg.Wait()
}

func newWatcher(receiver Receiver) *kubeWatcher {
func newWatcher(global context.Context, receiver Receiver, clientset *kubernetes.Clientset) *kubeWatcher {
return &kubeWatcher{
global: global,
cache: make(map[string]Ingress),
receiver: receiver,
checkLogos: make(chan struct{}, 1),
clientset: clientset,
}
}

type kubeWatcher struct {
global context.Context
clientset *kubernetes.Clientset
cache map[string]Ingress
lock sync.RWMutex
receiver Receiver
checkLogos chan struct{}
}

func (kw *kubeWatcher) OnAdd(obj interface{}) {
kw.upsertIngress(obj)
kw.upsertIngress(kw.global, obj)
}

func (kw *kubeWatcher) OnUpdate(_, newObj interface{}) {
kw.upsertIngress(newObj)
kw.upsertIngress(kw.global, newObj)
}

func (kw *kubeWatcher) OnDelete(obj interface{}) {
Expand All @@ -85,8 +92,8 @@ func (kw *kubeWatcher) OnDelete(obj interface{}) {
func (kw *kubeWatcher) runLogoFetcher(ctx context.Context) {
for {
for _, ing := range kw.items() {
if !ing.Hide && ing.LogoURL == "" && len(ing.URLs) > 0 {
ing.LogoURL = detectIconURL(ctx, ing.URLs[0])
if !ing.Hide && ing.LogoURL == "" && len(ing.Refs) > 0 {
ing.LogoURL = detectIconURL(ctx, ing.Refs[0].URL)
if ing.LogoURL != "" {
kw.updateLogo(ing)
}
Expand All @@ -109,10 +116,10 @@ func (kw *kubeWatcher) runWatcher(ctx context.Context, clientset *kubernetes.Cli
informer.Run(ctx.Done())
}

func (kw *kubeWatcher) upsertIngress(obj interface{}) {
func (kw *kubeWatcher) upsertIngress(ctx context.Context, obj interface{}) {
defer kw.notify()
ing := obj.(*v12.Ingress)
ingress := inspectIngress(ing)
ingress := kw.inspectIngress(ctx, ing)

kw.lock.Lock()
defer kw.lock.Unlock()
Expand Down Expand Up @@ -149,7 +156,7 @@ func (kw *kubeWatcher) updateLogo(ingress Ingress) {
kw.cache[ingress.UID] = old
}

func inspectIngress(ing *v12.Ingress) Ingress {
func (kw *kubeWatcher) inspectIngress(ctx context.Context, ing *v12.Ingress) Ingress {
return Ingress{
Name: ing.Name,
Namespace: ing.Namespace,
Expand All @@ -159,7 +166,7 @@ func inspectIngress(ing *v12.Ingress) Ingress {
Description: ing.Annotations[AnnoDescription],
LogoURL: ing.Annotations[AnnoLogoURL],
Hide: toBool(ing.Annotations[AnnoHide], false),
URLs: toURLs(ing.Spec),
Refs: kw.getRefs(ctx, ing),
}
}

Expand All @@ -174,23 +181,49 @@ func toList(cache map[string]Ingress) []Ingress {
return cp
}

func toURLs(spec v12.IngressSpec) []string {
func (kw *kubeWatcher) getRefs(ctx context.Context, ing *v12.Ingress) []Ref {
proto := "http://"
if len(spec.TLS) > 0 {
if len(ing.Spec.TLS) > 0 {
proto = "https://"
}

var urls []string
for _, rule := range spec.Rules {
var refs []Ref
for _, rule := range ing.Spec.Rules {
baseURL := proto + rule.Host
if rule.HTTP != nil {
for _, path := range rule.HTTP.Paths {
urls = append(urls, baseURL+path.Path)
var ref = Ref{
URL: baseURL + path.Path,
}
numPods, err := kw.getPodsNum(ctx, ing.Namespace, path.Backend.Service)
if err != nil {
log.Println("failed to get pods num for ingress", ing.Name, "in", ing.Namespace, "for path", path.Path, "-", err)
} else {
ref.Pods = numPods
}
refs = append(refs, ref)
}
}
}
return refs
}

func (kw *kubeWatcher) getPodsNum(ctx context.Context, ns string, svc *v12.IngressServiceBackend) (int, error) {
if svc == nil {
return 0, nil
}
info, err := kw.clientset.CoreV1().Services(ns).Get(ctx, svc.Name, v1.GetOptions{})
if err != nil {
return 0, fmt.Errorf("get service %s in %s: %w", svc.Name, ns, err)
}

var extHosts = len(info.Spec.ExternalIPs)
if extHosts == 0 && info.Spec.ExternalName != "" {
// reference by DNS to external host
extHosts = 1
}

return urls
return len(info.Spec.ClusterIPs) + extHosts, nil
}

func toBool(value string, defaultValue bool) bool {
Expand Down
45 changes: 32 additions & 13 deletions internal/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ import (
)

type Ingress struct {
ID string `yaml:"id"`
UID string `yaml:"uid"`
Title string `yaml:"title"`
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Description string `yaml:"description"`
Hide bool `yaml:"hide"`
URLs []string `yaml:"urls"`
LogoURL string `yaml:"logo_url"`
ID string `yaml:"id"` // human readable ID (namespace with name)
UID string `yaml:"uid"` // machine readable ID (guid in Kube)
Title string `yaml:"title"` // custom title in dashboard, overwrites Name
Name string `yaml:"name"` // ingress name as in Kube
Namespace string `yaml:"namespace"` // Kube namespace for ingress
Description string `yaml:"description"` // optional, human-readable description of Ingress
Hide bool `yaml:"hide"` // hidden Ingresses will not appear in UI
LogoURL string `yaml:"logo_url"` // custom URL for icon
Refs []Ref `yaml:"-"`
}

type Ref struct {
URL string // link to ingress
Pods int // number of pods linked to the service
Static bool // is reference defined statically (for static refs, pods number has no sense)
}

func (ingress Ingress) Label() string {
Expand All @@ -42,8 +48,8 @@ func (ingress Ingress) Logo() string {
}
if strings.HasPrefix(ingress.LogoURL, "/") {
// relative to domain
for _, u := range ingress.URLs {
return strings.TrimRight(u, "/") + ingress.LogoURL
for _, u := range ingress.Refs {
return strings.TrimRight(u.URL, "/") + ingress.LogoURL
}
}
return ingress.LogoURL
Expand Down Expand Up @@ -121,6 +127,11 @@ func visibleIngresses(list []Ingress) []Ingress {
//
// Empty location is a special case and cause returning empty slice.
func LoadDefinitions(location string) ([]Ingress, error) {
type YamlIngress struct {
Ingress `yaml:",inline"`
URLs []string `yaml:"urls"`
}

if location == "" {
return nil, nil
}
Expand All @@ -145,15 +156,23 @@ func LoadDefinitions(location string) ([]Ingress, error) {

var decoder = yaml.NewDecoder(f)
for {
var ingress Ingress
var ingress YamlIngress
err := decoder.Decode(&ingress)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("decode config %s: %w", path, err)
}
ans = append(ans, ingress)

for _, u := range ingress.URLs {
ingress.Refs = append(ingress.Refs, Ref{
URL: u,
Static: true,
})
}

ans = append(ans, ingress.Ingress)
}

return nil
Expand Down
58 changes: 58 additions & 0 deletions internal/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package internal_test

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/reddec/ingress-dashboard/internal"
"github.com/stretchr/testify/require"
)

func TestLoadDefinitions(t *testing.T) {
tempDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

err = ioutil.WriteFile(filepath.Join(tempDir, "demo.yaml"), []byte(`
---
name: Some site
namespace: external links # used in 'from'
urls:
- https://example.com
---
name: Google
namespace: external links
logo_url: https://www.google.ru/favicon.ico
description: |
Well-known search engine
urls:
- https://google.com
- http://example.com
`), 0755)
require.NoError(t, err)

list, err := internal.LoadDefinitions(tempDir)
require.NoError(t, err)
require.Len(t, list, 2)

require.Equal(t, internal.Ingress{
Name: "Some site",
Namespace: "external links",
Refs: []internal.Ref{
{URL: "https://example.com", Static: true},
},
}, list[0])

require.Equal(t, internal.Ingress{
Name: "Google",
Namespace: "external links",
Description: "Well-known search engine\n",
LogoURL: "https://www.google.ru/favicon.ico",
Refs: []internal.Ref{
{URL: "https://google.com", Static: true},
{URL: "http://example.com", Static: true},
},
}, list[1])
}
29 changes: 26 additions & 3 deletions internal/static/assets/templates/index.gotemplate
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@
{{with $ingress.Description}}
<p>{{.}}</p>
{{end}}
{{range $url := $ingress.URLs}}
<p>
<a href="{{$url}}" target="_blank">{{$url}}</a>
{{range $ref := $ingress.Refs}}
<p class="ref">
<a href="{{$ref.URL}}" target="_blank">{{$ref.URL}}</a>
</p>
{{- if not $ref.Static}}
{{if $ref.Pods}}
<p class="meta-info">
{{$ref.Pods}} host{{if gt $ref.Pods 1}}s{{end}}
</p>
{{else}}
<p class="meta-info {{if not $ref.Pods}}warn{{end}}">no hosts!</p>
{{end}}
{{- end}}
{{end}}
</div>
{{end}}
Expand Down Expand Up @@ -88,5 +97,19 @@
justify-content: space-between;
padding: 0.5em;
}

.ref {
margin-bottom: 0;
}

.meta-info {
margin-top: 0 !important;
font-size: x-small;
color: #999999;
}

.warn {
color: darkred;
}
</style>
</html>

0 comments on commit a9a7d86

Please sign in to comment.