Skip to content

Commit

Permalink
feat(kubernetes): add support for Custom Resources (#117)
Browse files Browse the repository at this point in the history
* feat(helm): add CRD definition and related access policy

* feat(helm): add sample Application manifest

* docs(crd): update accordingly

* feat(crd): make icon non-required field

* feat(kubernetes): add kubernetes custom resource type

* refactor(api): simplify getting kubernetes client

* feat(kubernetes): add support for kubernetes crds

* style(go-fmt): apply formatter

* docs(comments): minor changes

* style(golang): just to match how it is used in ingresses file

* fix(client): process error
  • Loading branch information
agrrh authored May 25, 2023
1 parent 87d3da6 commit f77fe68
Show file tree
Hide file tree
Showing 16 changed files with 433 additions and 73 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Hajimari is a simplistically beautiful startpage designed to be the entrypoint f
## Features

- Web and app search with customizable search providers
- Dynamically list apps discovered from Kubernetes ingresses
- Dynamically list apps discovered from Kubernetes Ingresses or Custom Resources
- Display replica status for ingress endpoints
- Support for non-Kubernetes apps via custom apps config
- Customizable list of bookmarks
Expand Down Expand Up @@ -69,7 +69,7 @@ Hajimari will need access to a kubeconfig file for a service account with [acces

### Ingresses

Hajimari looks for specific annotations on ingresses.
Hajimari looks for specific annotations on [Ingresses](https://kubernetes.io/docs/concepts/services-networking/ingress/).

- Add the following annotations to your ingresses in order for it to be discovered by Hajimari:

Expand Down Expand Up @@ -111,6 +111,24 @@ Hajimari supports the following configuration options that can be modified by ei
| customApps | A list of custom apps to add to the discovered apps | [] | \[\][AppGroup](#appgroup) |
| globalBookmarks | A list of bookmark groups to add | [] | \[\][BookmarkGroup](#bookmarkgroup) |

### HajimariApp objects

It also possible to define Apps via Kubernetes [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) for those using Istio's [Virtual Services](https://istio.io/latest/docs/reference/config/networking/virtual-service/), Traefik's [IngressRoutes](https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/) or other solutions, which does not reply on native Ingress objects:

```yaml
apiVersion: hajimari.io/v1alpha1
kind: Application
metadata:
name: hajimari-issues
spec:
name: Hajimari Issues
group: info
icon: simple-icons:github
url: https://github.com/toboshii/hajimari/issues
info: Submit issue to this project
targetBlank: true
```
#### NamespaceSelector
It is a selector for selecting namespaces either selecting all namespaces or a list of namespaces, or filtering namespaces through labels.
Expand Down
15 changes: 15 additions & 0 deletions charts/hajimari/templates/app-sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{- if .Values.createCRAppSample }}

apiVersion: hajimari.io/v1alpha1
kind: Application
metadata:
name: hajimari-issues
spec:
name: Hajimari Issues
group: info
icon: simple-icons:github
url: https://github.com/toboshii/hajimari/issues
info: Submit issue to this project
targetBlank: true

{{- end }}
52 changes: 52 additions & 0 deletions charts/hajimari/templates/crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: applications.hajimari.io
spec:
conversion:
strategy: None
group: hajimari.io
names:
kind: Application
listKind: ApplicationList
plural: applications
singular: application
preserveUnknownFields: false
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
required:
- name
- group
- url
properties:
name:
type: string
group:
type: string
icon:
type: string
url:
type: string
info:
type: string
targetBlank:
type: boolean
status:
type: object
7 changes: 5 additions & 2 deletions charts/hajimari/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "common.names.fullname" . }}
labels: {{- include "common.labels" . | nindent 4 }}
labels: {{- include "common.labels" . | nindent 4 }}
rules:
- apiGroups: ["", "extensions", "networking.k8s.io", "discovery.k8s.io"]
resources: ["ingresses", "namespaces", "endpointslices"]
verbs: ["get", "list"]
- apiGroups: ["hajimari.io"]
resources: ["applications"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
Expand All @@ -23,4 +26,4 @@ subjects:
- kind: ServiceAccount
name: {{ include "common.names.fullname" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end }}
5 changes: 4 additions & 1 deletion charts/hajimari/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ hajimari:
# url: 'https://example.com'
# icon: 'mdi:test-tube'
# info: This is a test app


# -- Create sample Custom Resource Application
createCRAppSample: false

# -- Set default bookmarks
globalBookmarks: []
# - group: Communicate
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.2
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mitchellh/mapstructure v1.5.0
github.com/onrik/logrus v0.9.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.13.0
Expand Down Expand Up @@ -36,7 +37,6 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand Down
105 changes: 105 additions & 0 deletions internal/hajimari/crdapps/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package crdapps

import (
"github.com/mitchellh/mapstructure"
"github.com/toboshii/hajimari/internal/annotations"
"github.com/toboshii/hajimari/internal/config"
"github.com/toboshii/hajimari/internal/kube/lists/crdapps"
"github.com/toboshii/hajimari/internal/kube/types/v1alpha1"
"github.com/toboshii/hajimari/internal/kube/wrappers"
"github.com/toboshii/hajimari/internal/log"
"github.com/toboshii/hajimari/internal/models"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
)

var (
logger = log.New()
)

// List struct is used for listing hajimari apps
type List struct {
appConfig config.Config
err error // Used for forwarding errors
items []models.AppGroup
dynClient dynamic.Interface
}

// NewList func creates a new instance of apps lister
func NewList(dynClient dynamic.Interface, appConfig config.Config) *List {
return &List{
appConfig: appConfig,
dynClient: dynClient,
}
}

// Populate function that populates a list of hajimari apps from ingresses in selected namespaces
func (al *List) Populate(namespaces ...string) *List {
appsList, err := crdapps.NewList(al.dynClient, al.appConfig).
Populate(namespaces...).
Get()

// Apply Instance filter
if len(al.appConfig.InstanceName) != 0 {
appsList, err = crdapps.NewList(al.dynClient, al.appConfig, appsList...).
Get()
}

if err != nil {
al.err = err
}

al.items = appsToHajimariApps(appsList)

return al
}

// Get function returns the apps currently present in List
func (al *List) Get() ([]models.AppGroup, error) {
return al.items, al.err
}

func appsToHajimariApps(apps []unstructured.Unstructured) (appGroups []models.AppGroup) {
for _, app := range apps {
name := app.GetName()
namespace := app.GetNamespace()

logger.Debugf("Found apps with Name '%v' in Namespace '%v'", name, namespace)

appObj := v1alpha1.Application{}
err := mapstructure.Decode(app.UnstructuredContent(), &appObj)
if err != nil {
logger.Error("Could not unmarshall object: %s/", name, namespace)
}

wrapper := wrappers.NewAppWrapper(&appObj)

groupMap := make(map[string]int, len(appGroups))
for i, v := range appGroups {
groupMap[v.Group] = i
}

if _, ok := groupMap[wrapper.GetGroup()]; !ok {
appGroups = append(appGroups, models.AppGroup{
Group: wrapper.GetGroup(),
})
}

appMap := make(map[string]int, len(appGroups))
for i, v := range appGroups {
appMap[v.Group] = i
}

if i, ok := appMap[wrapper.GetGroup()]; ok {
appGroups[i].Apps = append(appGroups[i].Apps, models.App{
Name: wrapper.GetName(),
Icon: wrapper.GetAnnotationValue(annotations.HajimariIconAnnotation),
URL: wrapper.GetURL(),
TargetBlank: wrapper.GetTargetBlank(),
Info: wrapper.GetInfo(),
})
}

}
return
}
20 changes: 13 additions & 7 deletions internal/handlers/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ func (rs *appResource) ListApps(w http.ResponseWriter, r *http.Request) {
return
}

cachedIngressApps := rs.service.GetCachedIngressApps()
// Collect Kubernetes apps

var ingressApps = make([]models.AppGroup, len(cachedIngressApps))
cachedKubeApps := rs.service.GetCachedKubeApps()

copy(ingressApps, cachedIngressApps)
var kubeApps = make([]models.AppGroup, len(cachedKubeApps))

copy(kubeApps, cachedKubeApps)

// Collect Custom apps

customAppsList := customapps.NewList(*appConfig)

Expand All @@ -48,18 +52,20 @@ func (rs *appResource) ListApps(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, ErrServerError(err))
}

// Merge apps together

var apps []models.AppGroup

for i, ingressAppGroup := range ingressApps {
for i, kubeAppGroup := range kubeApps {
for x, customAppGroup := range customApps {
if customAppGroup.Group == ingressAppGroup.Group {
ingressApps[i].Apps = append(ingressApps[i].Apps, customAppGroup.Apps...)
if customAppGroup.Group == kubeAppGroup.Group {
kubeApps[i].Apps = append(kubeApps[i].Apps, customAppGroup.Apps...)
customApps = append(customApps[:x], customApps[x+1:]...)
}
}
}

apps = append(ingressApps, customApps...)
apps = append(kubeApps, customApps...)

if err := render.RenderList(w, r, NewAppListResponse(apps)); err != nil {
render.Render(w, r, ErrServerError(err))
Expand Down
Loading

0 comments on commit f77fe68

Please sign in to comment.