diff --git a/cmd/ingress-dashboard/main.go b/cmd/ingress-dashboard/main.go index 6dd1f86..1e34da2 100644 --- a/cmd/ingress-dashboard/main.go +++ b/cmd/ingress-dashboard/main.go @@ -34,6 +34,7 @@ type Config struct { Auth string `long:"auth" env:"AUTH" description:"Auth scheme" default:"none" choice:"none" choice:"oidc" choice:"basic"` BasicUser string `long:"basic-user" env:"BASIC_USER" description:"Basic Auth username"` BasicPassword string `long:"basic-password" env:"BASIC_PASSWORD" description:"Basic Auth password"` + StaticSource string `long:"static-source" env:"STATIC_SOURCE" description:"Location of static ingress definitions" ` } func main() { @@ -63,6 +64,11 @@ func run(cfg Config) error { defer cancel() svc := internal.New() + staticDefinitions, err := internal.LoadDefinitions(cfg.StaticSource) + if err != nil { + return fmt.Errorf("load static definitions: %w", err) + } + svc.Prepend(staticDefinitions) secured, err := cfg.secureHandler(ctx, svc) if err != nil { diff --git a/docs/index.md b/docs/index.md index c381977..7928794 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,107 @@ spec: ingress-dashboard relies on annotations in each [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) object to configure dashboard. -## Annotation + +## Static source + +For the external resource (without Ingress definitions) it is possible to +define static dashboard definitions. + +Static definitions always placed before dynamic. No automatic logo URL detection available. + +To enable static source define environment `STATIC_SOURCE=/path/to/source`, where source +could be directory or single file. + +Directories are scanned recursively for each file with extension `.yml`, `.yaml`, or `.json`. +YAML documents may contain multiple definitions. + +Support fields: + +* `name` - resource label +* `namespace` - (optional) resource namespace, used in `from` +* `description` - (optional) resource description +* `hide` - (optional) mark resource as hidden or not. Default is `false` +* `urls` - list of urls +* `logo_url` - (optional) URL for log + + +Example: + +```yaml +--- +name: Some site +namespace: external links +urls: + - https://example.com +--- +name: Google +logo_url: https://www.google.ru/favicon.ico +description: | + Well-known search engine +urls: + - https://google.com +``` + +Example usage in kubernetes with [config map](https://kubernetes.io/docs/concepts/configuration/configmap/): + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: static + namespace: ingress-dashboard +data: + static.yaml: | + --- + name: Some site + namespace: external links + urls: + - https://example.com + --- + name: Google + logo_url: https://www.google.ru/favicon.ico + description: | + Well-known search engine + urls: + - https://google.com +``` + +> Hint: you may use Kustomize [config map generator](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/#configmapgenerator) to simplify the process + +And update deployment + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "dashboard" + namespace: "ingress-dashboard" +spec: + # ... + template: + # ... + spec: + # ... + containers: + - name: "dashboard" + # ... + env: + # ... + - name: STATIC_SOURCE + value: /static + # ... + volumeMounts: + - name: config-volume + mountPath: /static + # ... + volumes: + - name: config-volume + configMap: + name: static +``` + +## Annotations All annotations are optional. diff --git a/example/static.yaml b/example/static.yaml new file mode 100644 index 0000000..48de898 --- /dev/null +++ b/example/static.yaml @@ -0,0 +1,13 @@ +--- +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 \ No newline at end of file diff --git a/go.mod b/go.mod index 11ee63b..c9cce0a 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/reddec/run-http-server v0.0.0-20211110142919-824037361ead 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/client-go v0.22.4 ) @@ -37,7 +38,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 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // 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 diff --git a/internal/service.go b/internal/service.go index 89eee80..b738c63 100644 --- a/internal/service.go +++ b/internal/service.go @@ -1,13 +1,20 @@ package internal import ( + "errors" + "fmt" "html/template" + "io" + "io/fs" "net/http" + "os" + "path/filepath" "strings" "sync/atomic" "github.com/reddec/ingress-dashboard/internal/auth" "github.com/reddec/ingress-dashboard/internal/static" + "gopkg.in/yaml.v3" ) type Ingress struct { @@ -60,9 +67,10 @@ func New() *Service { } type Service struct { - cache atomic.Value // []Ingress - page *template.Template - router *http.ServeMux + cache atomic.Value // []Ingress + prepend atomic.Value // []Ingres + page *template.Template + router *http.ServeMux } func (svc *Service) Set(ingress []Ingress) { @@ -73,14 +81,25 @@ func (svc *Service) Get() []Ingress { return svc.cache.Load().([]Ingress) } +// Prepend static list of ingresses. +func (svc *Service) Prepend(ingress []Ingress) { + svc.prepend.Store(ingress) +} + func (svc *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) { svc.router.ServeHTTP(writer, request) } +func (svc *Service) getList() []Ingress { + prepend := svc.prepend.Load().([]Ingress) + main := svc.cache.Load().([]Ingress) + return append(prepend, main...) +} + func (svc *Service) getIndex(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/html") _ = svc.page.Execute(writer, UIContext{ - Ingresses: visibleIngresses(svc.Get()), + Ingresses: visibleIngresses(svc.getList()), User: auth.UserFromContext(request.Context()), }) } @@ -94,3 +113,48 @@ func visibleIngresses(list []Ingress) []Ingress { } return cp } + +// LoadDefinitions scans location (file or dir) for YAML/JSON (.yml, .yaml, .json) definitions of Ingress. +// Directories scanned recursive and each file can contain multiple definitions. +// +// Empty location is a special case and cause returning empty slice. +func LoadDefinitions(location string) ([]Ingress, error) { + if location == "" { + return nil, nil + } + var ans []Ingress + err := filepath.Walk(location, func(path string, info fs.FileInfo, err error) error { + if info.IsDir() { + return nil + } + if err != nil { + return err + } + ext := filepath.Ext(path) + if !(ext == ".yml" || ext == ".yaml" || ext == ".json") { + return nil + } + + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("open config file: %w", err) + } + defer f.Close() + + var decoder = yaml.NewDecoder(f) + for { + var ingress Ingress + 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) + } + + return nil + }) + return ans, err +}