diff --git a/deploy/kustomize/crds/kobs.io_applications.yaml b/deploy/kustomize/crds/kobs.io_applications.yaml index 9b0a565f2..57fb8c411 100644 --- a/deploy/kustomize/crds/kobs.io_applications.yaml +++ b/deploy/kustomize/crds/kobs.io_applications.yaml @@ -50,6 +50,10 @@ spec: type: string namespace: type: string + placeholders: + additionalProperties: + type: string + type: object title: type: string required: diff --git a/deploy/kustomize/crds/kobs.io_dashboards.yaml b/deploy/kustomize/crds/kobs.io_dashboards.yaml index ff5daaba4..71898aa2e 100644 --- a/deploy/kustomize/crds/kobs.io_dashboards.yaml +++ b/deploy/kustomize/crds/kobs.io_dashboards.yaml @@ -43,6 +43,17 @@ spec: type: string namespace: type: string + placeholders: + items: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + type: array rows: items: properties: diff --git a/deploy/kustomize/crds/kobs.io_teams.yaml b/deploy/kustomize/crds/kobs.io_teams.yaml index c7b687458..23aa8e526 100644 --- a/deploy/kustomize/crds/kobs.io_teams.yaml +++ b/deploy/kustomize/crds/kobs.io_teams.yaml @@ -48,6 +48,10 @@ spec: type: string namespace: type: string + placeholders: + additionalProperties: + type: string + type: object title: type: string required: diff --git a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go index 911f8c026..f7b3becde 100644 --- a/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go +++ b/pkg/api/apis/application/v1beta1/zz_generated.deepcopy.go @@ -106,7 +106,9 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { if in.Dashboards != nil { in, out := &in.Dashboards, &out.Dashboards *out = make([]dashboardv1beta1.Reference, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/pkg/api/apis/dashboard/v1beta1/types.go b/pkg/api/apis/dashboard/v1beta1/types.go index 98b4be610..9d41fc369 100644 --- a/pkg/api/apis/dashboard/v1beta1/types.go +++ b/pkg/api/apis/dashboard/v1beta1/types.go @@ -27,12 +27,18 @@ type DashboardList struct { } type DashboardSpec struct { - Cluster string `json:"cluster,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - Title string `json:"title,omitempty"` + Cluster string `json:"cluster,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Placeholders []Placeholder `json:"placeholders,omitempty"` + Rows []Row `json:"rows"` +} + +type Placeholder struct { + Name string `json:"name"` Description string `json:"description,omitempty"` - Rows []Row `json:"rows"` } type Row struct { @@ -56,9 +62,10 @@ type Plugin struct { } type Reference struct { - Cluster string `json:"cluster,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name"` - Title string `json:"title"` - Description string `json:"description,omitempty"` + Cluster string `json:"cluster,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Placeholders map[string]string `json:"placeholders,omitempty"` } diff --git a/pkg/api/apis/dashboard/v1beta1/zz_generated.deepcopy.go b/pkg/api/apis/dashboard/v1beta1/zz_generated.deepcopy.go index 61a4c129e..768768e75 100644 --- a/pkg/api/apis/dashboard/v1beta1/zz_generated.deepcopy.go +++ b/pkg/api/apis/dashboard/v1beta1/zz_generated.deepcopy.go @@ -88,6 +88,11 @@ func (in *DashboardList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) { *out = *in + if in.Placeholders != nil { + in, out := &in.Placeholders, &out.Placeholders + *out = make([]Placeholder, len(*in)) + copy(*out, *in) + } if in.Rows != nil { in, out := &in.Rows, &out.Rows *out = make([]Row, len(*in)) @@ -125,6 +130,22 @@ func (in *Panel) DeepCopy() *Panel { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Placeholder) DeepCopyInto(out *Placeholder) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Placeholder. +func (in *Placeholder) DeepCopy() *Placeholder { + if in == nil { + return nil + } + out := new(Placeholder) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Plugin) DeepCopyInto(out *Plugin) { *out = *in @@ -149,6 +170,13 @@ func (in *Plugin) DeepCopy() *Plugin { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Reference) DeepCopyInto(out *Reference) { *out = *in + if in.Placeholders != nil { + in, out := &in.Placeholders, &out.Placeholders + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/api/apis/team/v1beta1/zz_generated.deepcopy.go b/pkg/api/apis/team/v1beta1/zz_generated.deepcopy.go index 05224b547..b81a7a708 100644 --- a/pkg/api/apis/team/v1beta1/zz_generated.deepcopy.go +++ b/pkg/api/apis/team/v1beta1/zz_generated.deepcopy.go @@ -128,7 +128,9 @@ func (in *TeamSpec) DeepCopyInto(out *TeamSpec) { if in.Dashboards != nil { in, out := &in.Dashboards, &out.Dashboards *out = make([]dashboardv1beta1.Reference, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/pkg/app/app.go b/pkg/app/app.go index aac6526bb..cdf054848 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -89,8 +89,12 @@ func New(isDevelopment bool) (*Server, error) { staticHandler := http.StripPrefix("/", http.FileServer(http.Dir(assetsDir))) router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { + // When kobs is run in development mode and the request path starts with "/api", we redirect these requests + // to the port, where the API is running ("15220"). We have to return 307 as status code, to preserve the + // used http method. This can be used to test the production build of the React app locally without the need + // of another proxy, which handles the redirect. if isDevelopment && strings.HasPrefix(r.URL.Path, "/api") { - http.Redirect(w, r, "http://localhost:15220"+r.URL.Path+"?"+r.URL.RawQuery, http.StatusMovedPermanently) + http.Redirect(w, r, "http://localhost:15220"+r.URL.Path+"?"+r.URL.RawQuery, http.StatusTemporaryRedirect) return } diff --git a/plugins/dashboards/dashboards.go b/plugins/dashboards/dashboards.go index 2ff5d1a03..9235b4d38 100644 --- a/plugins/dashboards/dashboards.go +++ b/plugins/dashboards/dashboards.go @@ -8,6 +8,7 @@ import ( "github.com/kobsio/kobs/pkg/api/clusters" "github.com/kobsio/kobs/pkg/api/middleware/errresponse" "github.com/kobsio/kobs/pkg/api/plugins/plugin" + "github.com/kobsio/kobs/plugins/dashboards/pkg/placeholders" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -79,8 +80,8 @@ func (router *Router) getDashboards(w http.ResponseWriter, r *http.Request) { // Loop through all the provided references and set the cluster and namespace from the defaults, when it is not // provided. Then we are using the GetDashboard function for a cluster to get the dashboard by namespace and name. - // Finally we are adding the title from the reference as dashboard title and we are adding the dashboard to a list - // of dashboards. + // Finally we are replacing the placeholders in a dashboard with the provided values and we are adding the title + // from the reference as dashboard title and we are adding the dashboard to a list of dashboards. for _, reference := range data.References { if reference.Cluster == "" { reference.Cluster = data.Defaults.Cluster @@ -102,6 +103,14 @@ func (router *Router) getDashboards(w http.ResponseWriter, r *http.Request) { return } + if reference.Placeholders != nil { + dashboard, err = placeholders.Replace(reference.Placeholders, *dashboard) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not replace placeholders")) + return + } + } + dashboard.Title = reference.Title dashboards = append(dashboards, dashboard) } diff --git a/plugins/dashboards/pkg/placeholders/helpers.go b/plugins/dashboards/pkg/placeholders/helpers.go new file mode 100644 index 000000000..0e852f46f --- /dev/null +++ b/plugins/dashboards/pkg/placeholders/helpers.go @@ -0,0 +1,40 @@ +package placeholders + +import ( + "bytes" + "encoding/json" + "html/template" + + dashboard "github.com/kobsio/kobs/pkg/api/apis/dashboard/v1beta1" +) + +// Replace replaces the placeholders in a dashboard, with the provided values from the placeholders map. The +// placeholders must use the following format in the dashboard to be replaced: "{% .placeholder %}". +// To replace the placeholders we have to convert the dashboard to it's json string representation, which is then passed +// to the template together with the placeholders. The result is then unmarshaled back to the dashboard struct and +// returned. +func Replace(placeholders map[string]string, dash dashboard.DashboardSpec) (*dashboard.DashboardSpec, error) { + out, err := json.Marshal(dash) + if err != nil { + return nil, err + } + + tpl, err := template.New("dashboard").Delims("{%", "%}").Parse(string(out)) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = tpl.Execute(&buf, placeholders) + if err != nil { + return nil, err + } + + var dashPlaceholders dashboard.DashboardSpec + err = json.Unmarshal([]byte(buf.String()), &dashPlaceholders) + if err != nil { + return nil, err + } + + return &dashPlaceholders, nil +} diff --git a/plugins/dashboards/src/utils/interfaces.ts b/plugins/dashboards/src/utils/interfaces.ts index 228e7abfe..6bf545b41 100644 --- a/plugins/dashboards/src/utils/interfaces.ts +++ b/plugins/dashboards/src/utils/interfaces.ts @@ -4,9 +4,15 @@ export interface IDashboard { name: string; title: string; description?: string; + placeholders?: IPlaceholder[]; rows: IRow[]; } +export interface IPlaceholder { + name: string; + description?: string; +} + export interface IRow { title?: string; description?: string; @@ -34,4 +40,9 @@ export interface IReference { name: string; title: string; description?: string; + placeholders?: IPlaceholders; +} + +export interface IPlaceholders { + [key: string]: string; }