Compare commits
No commits in common. "e269f5e491cfc88f8e4fb0f5dd362506471c40fe" and "67905eac36c1c011871b4658023530e4b22c48f7" have entirely different histories.
e269f5e491
...
67905eac36
35
README.md
35
README.md
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
_The minimal, declarative service catalog._
|
_The minimal, declarative service catalog._
|
||||||
|
|
||||||
|
Pronounced "MC" as in the master of ceremonies.
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|
||||||
In the last three jobs I've worked at, it's always been a hassle trying to locate the various dashboards, documentation,
|
In the last three jobs I've worked at, it's always been a hassle trying to locate the various dashboards, documentation,
|
||||||
@ -58,38 +60,11 @@ func main() {
|
|||||||
|
|
||||||
Once you've built your catalog, you can easily run a landing page by executing the catalog file.
|
Once you've built your catalog, you can easily run a landing page by executing the catalog file.
|
||||||
|
|
||||||
```sh
|
```
|
||||||
$ go run ./catalog.go
|
$ go run ./catalog.go
|
||||||
```
|
```
|
||||||
|
|
||||||
This starts a web server for you to interact with on `localhost:8080`. If `:8080` is already in use, you can configure
|
This starts a web server for you to interact with on `localhost:8080`. If `:8080` is already in use, you can configure
|
||||||
the bind address by passing the `-bind_address` flag with the desired host and port.
|
the bind address be passing the `-bind_address` flag with the desired host and port.
|
||||||
|
|
||||||
<p align="center">
|
![Screenshot](screenshot.png)
|
||||||
<img src="screenshot.png" alt="Screenshot" width="72%"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
### Exporting your catalog
|
|
||||||
|
|
||||||
Instead of needing to compile a binary or host your catalog using `go run`, you can export your catalog to HTML or JSON.
|
|
||||||
This makes it easy to drop into existing self-host platforms or leverage with other popular systems.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go run ./catalog.go -output html > index.html
|
|
||||||
$ go run ./catalog.go -output json > catalog.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Protecting your catalog using oauth-proxy
|
|
||||||
|
|
||||||
Regardless of how you host your catalog, you'll likely want to protect access to it. An easy way to do this is using the
|
|
||||||
[oauth-proxy][] project. This project provides common OAuth2 client functionality to any project, making it easy to
|
|
||||||
require authentication in order to access a system / project.
|
|
||||||
|
|
||||||
<!-- TODO: write up guide and link to it from here -->
|
|
||||||
|
|
||||||
Until I have more of a concrete guide, you can follow my setup [here](https://github.com/mjpitz/mjpitz/blob/main/infra/helm/catalog/values.yaml).
|
|
||||||
A simple analogy to this deployment would be a docker compose file with two services, one for the oauth-proxy and the
|
|
||||||
other for the catalog (bound to 127.0.0.1). Using the new `-output` functionality, this deployment could definitely
|
|
||||||
be simplified.
|
|
||||||
|
|
||||||
[oauth-proxy]: https://oauth2-proxy.github.io/oauth2-proxy
|
|
||||||
|
@ -14,8 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.catalog {
|
div.catalog {
|
||||||
max-width: 1600px;
|
max-width: 800px;
|
||||||
min-width: 1200px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,14 +31,14 @@
|
|||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.col-65 {
|
div.col-60 {
|
||||||
max-width: 65%;
|
max-width: 60%;
|
||||||
min-width: 65%;
|
min-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.col-35 {
|
div.col-40 {
|
||||||
max-width: 35%;
|
max-width: 40%;
|
||||||
min-width: 35%;
|
min-width: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.catalog h1 {
|
div.catalog h1 {
|
||||||
@ -53,7 +52,7 @@
|
|||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
padding: 20px 26px;
|
padding: 20px 26px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 9px 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.catalog div.service img.logo {
|
div.catalog div.service img.logo {
|
||||||
@ -82,55 +81,52 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="catalog">
|
<div class="catalog">
|
||||||
{{- range $i, $service := .Services }}
|
<h1>Service Catalog</h1>
|
||||||
{{- if $i | mod 2 | eq 0 }}<div class="row">{{- end }}
|
{{- range $service := .Services }}
|
||||||
<div class="col col-50">
|
<div class="service">
|
||||||
<div class="service">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col col-60" style="padding-right: 10px">
|
||||||
<div class="col col-65" style="padding-right: 10px">
|
<div>
|
||||||
<div>
|
{{- if $service.LogoURL }}
|
||||||
{{- if $service.LogoURL }}
|
<img class="logo" src="{{ $service.LogoURL }}" />
|
||||||
<img class="logo" src="{{ $service.LogoURL }}" />
|
{{- end }}
|
||||||
|
|
||||||
|
<h2 class="name">{{ $service.Label }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- if $service.URL }}
|
||||||
|
<p class="url"><a href="{{ $service.URL }}">{{ $service.URL }}</a></p>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if $service.Description }}
|
||||||
|
<p class="description">{{ $service.Description }}</p>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- range $key, $value := $service.Metadata }}
|
||||||
|
<div class="metadata row">
|
||||||
|
<div class="col-50"><b>{{ $key }}</b></div>
|
||||||
|
<div class="col-50">{{ $value }}</div>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col col-40">
|
||||||
|
{{- range $group := $service.LinkGroups }}
|
||||||
|
<div class="link-group">
|
||||||
|
<h3 class="label">{{ $group.Label }}</h3>
|
||||||
|
<ul>
|
||||||
|
{{- range $link := $group.Links }}
|
||||||
|
<li class="link">
|
||||||
|
<a href="{{ $link.URL }}">{{ $link.Label }}</a>
|
||||||
|
</li>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
</ul>
|
||||||
<h2 class="name">{{ $service.Label }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{- if $service.URL }}
|
|
||||||
<p class="url"><a href="{{ $service.URL }}">{{ $service.URL }}</a></p>
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- if $service.Description }}
|
|
||||||
<p class="description">{{ $service.Description }}</p>
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- range $kv := $service.Metadata }}
|
|
||||||
<div class="metadata row">
|
|
||||||
<div class="col col-35"><b>{{ $kv.Key }}</b></div>
|
|
||||||
<div class="col col-65">{{ $kv.Value }}</div>
|
|
||||||
</div>
|
|
||||||
{{- end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col col-35">
|
|
||||||
{{- range $group := $service.LinkGroups }}
|
|
||||||
<div class="link-group">
|
|
||||||
<h3 class="label">{{ $group.Label }}</h3>
|
|
||||||
<ul>
|
|
||||||
{{- range $link := $group.Links }}
|
|
||||||
<li class="link">
|
|
||||||
<a href="{{ $link.URL }}">{{ $link.Label }}</a>
|
|
||||||
</li>
|
|
||||||
{{- end }}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{- end }}
|
|
||||||
</div>
|
</div>
|
||||||
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{- if $i | mod 2 | eq 1 }}</div>{{- end }}
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -6,13 +6,10 @@ package catalog
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mjpitz/emc/catalog/service"
|
"github.com/mjpitz/emc/catalog/service"
|
||||||
@ -21,11 +18,6 @@ import (
|
|||||||
//go:embed index.html.tpl
|
//go:embed index.html.tpl
|
||||||
var catalog string
|
var catalog string
|
||||||
|
|
||||||
var funcs = map[string]any{
|
|
||||||
"mod": func(mod, v int) int { return v % mod },
|
|
||||||
"eq": func(exp, act int) bool { return exp == act },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec defines the requirements for hosting a catalog.
|
// Spec defines the requirements for hosting a catalog.
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Services []service.Spec
|
Services []service.Spec
|
||||||
@ -44,44 +36,30 @@ func Service(label string, options ...service.Option) Option {
|
|||||||
// Serve provides command line functionality for running the service catalog.
|
// Serve provides command line functionality for running the service catalog.
|
||||||
func Serve(options ...Option) {
|
func Serve(options ...Option) {
|
||||||
addr := flag.String("bind_address", "127.0.0.1:8080", "the address the service should bind to when serving content")
|
addr := flag.String("bind_address", "127.0.0.1:8080", "the address the service should bind to when serving content")
|
||||||
output := flag.String("output", "", "set to output the catalog to stdout (valid html, json)")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
t := template.Must(template.New("catalog").Parse(catalog))
|
||||||
|
|
||||||
spec := Spec{}
|
spec := Spec{}
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
opt(&spec)
|
opt(&spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
html := bytes.NewBuffer(nil)
|
||||||
var err error
|
err := t.Execute(html, spec)
|
||||||
|
|
||||||
switch {
|
|
||||||
case output == nil || *output == "" || *output == "html":
|
|
||||||
err = template.Must(template.New("catalog").Funcs(funcs).Parse(catalog)).Execute(buffer, spec)
|
|
||||||
case *output == "json":
|
|
||||||
err = json.NewEncoder(buffer).Encode(spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("failed to render output", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if output != nil && *output != "" {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, err = io.Copy(os.Stdout, bytes.NewReader(buffer.Bytes()))
|
http.ServeContent(w, r, "", start, bytes.NewReader(html.Bytes()))
|
||||||
if err != nil {
|
})
|
||||||
log.Fatal("failed to write buffer to stdout", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeContent(w, r, "", start, bytes.NewReader(buffer.Bytes()))
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("serving on %s\n", *addr)
|
log.Printf("serving on %s\n", *addr)
|
||||||
err = http.ListenAndServe(*addr, http.DefaultServeMux)
|
err = http.ListenAndServe(*addr, http.DefaultServeMux)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("failed to serve content", err)
|
panic(err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ import (
|
|||||||
// New constructs a spec given a label and set of options.
|
// New constructs a spec given a label and set of options.
|
||||||
func New(label string, options ...Option) Spec {
|
func New(label string, options ...Option) Spec {
|
||||||
spec := Spec{
|
spec := Spec{
|
||||||
Label: label,
|
Label: label,
|
||||||
|
Metadata: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
@ -23,19 +24,13 @@ func New(label string, options ...Option) Spec {
|
|||||||
// Option defines an optional component of the spec.
|
// Option defines an optional component of the spec.
|
||||||
type Option func(spec *Spec)
|
type Option func(spec *Spec)
|
||||||
|
|
||||||
// KV defines a metadata entry.
|
|
||||||
type KV struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec defines the elements needed to render a service.
|
// Spec defines the elements needed to render a service.
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Label string
|
Label string
|
||||||
LogoURL string
|
LogoURL string
|
||||||
Description string
|
Description string
|
||||||
URL string
|
URL string
|
||||||
Metadata []KV
|
Metadata map[string]string
|
||||||
LinkGroups []linkgroup.Spec
|
LinkGroups []linkgroup.Spec
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,19 +56,9 @@ func URL(url string) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata allows additional metadata to be attached to a service.
|
// Metadata allows additional metadata to be attached to a service.
|
||||||
func Metadata(kvs ...string) Option {
|
func Metadata(key, value string) Option {
|
||||||
// ensure even number of parameters
|
|
||||||
if len(kvs)%2 > 0 {
|
|
||||||
kvs = append(kvs, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(spec *Spec) {
|
return func(spec *Spec) {
|
||||||
for i := 0; i < len(kvs); i += 2 {
|
spec.Metadata[key] = value
|
||||||
spec.Metadata = append(spec.Metadata, KV{
|
|
||||||
Key: kvs[i],
|
|
||||||
Value: kvs[i+1],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mjpitz/emc/catalog"
|
|
||||||
"github.com/mjpitz/emc/catalog/linkgroup"
|
|
||||||
"github.com/mjpitz/emc/catalog/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
catalog.Serve(
|
|
||||||
catalog.Service(
|
|
||||||
"CI/CD",
|
|
||||||
service.LogoURL("https://th.bing.com/th/id/OIP.wd0WnO0MF56eQ23LR8XzRAAAAA?pid=ImgDet&rs=1"),
|
|
||||||
service.URL("https://deploy.example.com"),
|
|
||||||
service.Description("Continuous integration and delivery platform."),
|
|
||||||
service.Metadata(
|
|
||||||
"Key1", "Key1Value1",
|
|
||||||
"Key1", "Key1Value2",
|
|
||||||
"Key2", "Key2Value1",
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Dashboards",
|
|
||||||
linkgroup.Link("System", "#"),
|
|
||||||
linkgroup.Link("Queue", "#"),
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Documentation",
|
|
||||||
linkgroup.Link("Developing", "#"),
|
|
||||||
linkgroup.Link("Releasing", "#"),
|
|
||||||
linkgroup.Link("Contributing", "#"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
catalog.Service(
|
|
||||||
"Version Control",
|
|
||||||
service.LogoURL(""),
|
|
||||||
service.URL("https://code.example.com"),
|
|
||||||
service.Description("System source code."),
|
|
||||||
service.Metadata(
|
|
||||||
"Key1", "Key1Value1",
|
|
||||||
"Key1", "Key1Value2",
|
|
||||||
"Key2", "Key2Value1",
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Dashboards",
|
|
||||||
linkgroup.Link("System", "#"),
|
|
||||||
linkgroup.Link("Database", "#"),
|
|
||||||
linkgroup.Link("Queue", "#"),
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Documentation",
|
|
||||||
linkgroup.Link("Developing", "#"),
|
|
||||||
linkgroup.Link("Releasing", "#"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
catalog.Service(
|
|
||||||
"Monitoring",
|
|
||||||
service.Metadata(
|
|
||||||
"Key1", "Key1Value1",
|
|
||||||
"Key1", "Key1Value2",
|
|
||||||
"Key2", "Key2Value1",
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Dashboards",
|
|
||||||
linkgroup.Link("System", "#"),
|
|
||||||
linkgroup.Link("Database", "#"),
|
|
||||||
linkgroup.Link("Queue", "#"),
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Documentation",
|
|
||||||
linkgroup.Link("Developing", "#"),
|
|
||||||
linkgroup.Link("Releasing", "#"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
catalog.Service(
|
|
||||||
"Alerting",
|
|
||||||
service.Metadata(
|
|
||||||
"Key1", "Key1Value1",
|
|
||||||
"Key1", "Key1Value2",
|
|
||||||
"Key2", "Key2Value1",
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Dashboards",
|
|
||||||
linkgroup.Link("System", "#"),
|
|
||||||
linkgroup.Link("Database", "#"),
|
|
||||||
linkgroup.Link("Queue", "#"),
|
|
||||||
),
|
|
||||||
service.LinkGroup(
|
|
||||||
"Documentation",
|
|
||||||
linkgroup.Link("Developing", "#"),
|
|
||||||
linkgroup.Link("Releasing", "#"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 406 KiB |
Loading…
Reference in New Issue
Block a user