port to new repo
This commit is contained in:
commit
8e4d7ddd72
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Mya Pitzeruse
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# emc
|
||||||
|
|
||||||
|
_The minimally declarative service catalog._
|
||||||
|
|
||||||
|
Pronounced "MC" as in the master of ceremonies.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
In the last three jobs I've worked at, it's always been a hassle to trying to locate the various dashboards,
|
||||||
|
documentation, and support for a given project. I joined effx to try and help address just that. As I've been spinning
|
||||||
|
up a new cluster, I found myself wanting a landing page for the systems that I use regularly.
|
||||||
|
|
||||||
|
## Building your catalog
|
||||||
|
|
||||||
|
The `emc` service catalog is defined using a simple Golang script. This makes it easy for engineers to drop in their
|
||||||
|
own functionality for rendering links, link groups, or services. For an example, see the provided `grafana` package
|
||||||
|
which includes several of my personal dashboards for different systems.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// catalog.go
|
||||||
|
|
||||||
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mjpitz/emc/catalog"
|
||||||
|
"github.com/mjpitz/emc/catalog/grafana"
|
||||||
|
"github.com/mjpitz/emc/catalog/linkgroup"
|
||||||
|
"github.com/mjpitz/emc/catalog/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
catalog.Serve(
|
||||||
|
catalog.Service(
|
||||||
|
"Drone",
|
||||||
|
service.LogoURL("https://path/to/drone-logo.png"),
|
||||||
|
service.URL("https://drone.example.com"),
|
||||||
|
service.Description("Drone is a self-service Continuous Integration platform for busy development teams."),
|
||||||
|
service.Metadata("Contact", "drone@example.com"),
|
||||||
|
service.LinkGroup(
|
||||||
|
"Dashboards",
|
||||||
|
linkgroup.Link("Drone", grafana.Drone("cicd", "drone")),
|
||||||
|
linkgroup.Link("Golang", grafana.Golang("cicd", "drone")),
|
||||||
|
linkgroup.Link("Litestream", grafana.Litestream("cicd", "drone")),
|
||||||
|
linkgroup.Link("Redis Queue", grafana.Redis("cicd", "drone-redis-queue")),
|
||||||
|
),
|
||||||
|
service.LinkGroup(
|
||||||
|
"Documentation",
|
||||||
|
linkgroup.Link("docs.drone.io", "https://docs.drone.io/"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hosting your catalog
|
||||||
|
|
||||||
|
Once you've built your catalog, you can easily run a landing page by executing the catalog file.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ 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
|
||||||
|
the bind address be passing the `-bind_address` flag with the desired host and port.
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
78
catalog/grafana/dsl.go
Normal file
78
catalog/grafana/dsl.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) 2022 Mya Pitzeruse
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
package grafana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Grafana provides functionality for rendering links to dashboards. This implementation provides some shorthand calls
|
||||||
|
// that assume the use of my personal dashboards (provided at https://github.com/mjpitz/mjpitz/tree/main/monitoring).
|
||||||
|
type Grafana string
|
||||||
|
|
||||||
|
// Link renders a link to a grafana deployment given a dashboard id, namespace, and job (assumes a kubernetes based
|
||||||
|
// deployment).
|
||||||
|
func (g Grafana) Link(dashboard, namespace, job string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s/d/%s?var-namespace=%s&var-job=%s",
|
||||||
|
g, url.PathEscape(dashboard), url.QueryEscape(namespace), url.QueryEscape(job),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Maddy(namespace, job string) string {
|
||||||
|
return g.Link("82a7b6b2c9516ef16c08616edc8a90c1", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Redis(namespace, job string) string {
|
||||||
|
return g.Link("36cf4a03d9f16d8a4221fecbbd2ff5c6", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Drone(namespace, job string) string {
|
||||||
|
return g.Link("2c241b4cea8d493ef632bf33b10d04cf", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Litestream(namespace, job string) string {
|
||||||
|
return g.Link("bf44e72e619451e2c85cda80fe17b28b", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Golang(namespace, job string) string {
|
||||||
|
return g.Link("8cd750f455ca9fc93a465fd9a34993cc", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Grafana) Gitea(namespace, job string) string {
|
||||||
|
return g.Link("354a485fe64f93ea707a7f6e061ff71b", namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default instance
|
||||||
|
|
||||||
|
var grafana = Grafana("https://grafana.pitz.tech")
|
||||||
|
|
||||||
|
func Link(dashboard, namespace, job string) string {
|
||||||
|
return grafana.Link(dashboard, namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Maddy(namespace, job string) string {
|
||||||
|
return grafana.Maddy(namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Redis(namespace, job string) string {
|
||||||
|
return grafana.Redis(namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Drone(namespace, job string) string {
|
||||||
|
return grafana.Drone(namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Litestream(namespace, job string) string {
|
||||||
|
return grafana.Litestream(namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Golang(namespace, job string) string {
|
||||||
|
return grafana.Golang(namespace, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gitea(namespace, job string) string {
|
||||||
|
return grafana.Gitea(namespace, job)
|
||||||
|
}
|
132
catalog/index.html.tpl
Normal file
132
catalog/index.html.tpl
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Service Catalog</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
padding: 60px 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.col {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.col-50 {
|
||||||
|
max-width: 50%;
|
||||||
|
min-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.col-60 {
|
||||||
|
max-width: 60%;
|
||||||
|
min-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.col-40 {
|
||||||
|
max-width: 40%;
|
||||||
|
min-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog div.service {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 20px 26px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog div.service img.logo {
|
||||||
|
max-height: 48px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog div.service h2.name {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog div.service p.url {}
|
||||||
|
div.catalog div.service p.description {}
|
||||||
|
|
||||||
|
div.catalog div.service div.metadata {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.catalog div.service div.link-group {}
|
||||||
|
div.catalog div.service div.link-group h3.label {}
|
||||||
|
div.catalog div.service div.link-group li.link {}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="catalog">
|
||||||
|
<h1>Service Catalog</h1>
|
||||||
|
{{- range $service := .Services }}
|
||||||
|
<div class="service">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-60" style="padding-right: 10px">
|
||||||
|
<div>
|
||||||
|
{{- if $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 }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
catalog/link/dsl.go
Normal file
18
catalog/link/dsl.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2022 Mya Pitzeruse
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
package link
|
||||||
|
|
||||||
|
// New constructs a link from the given label and url.
|
||||||
|
func New(label, url string) Spec {
|
||||||
|
return Spec{
|
||||||
|
Label: label,
|
||||||
|
URL: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec defines the elements needed to render a link.
|
||||||
|
type Spec struct {
|
||||||
|
Label string
|
||||||
|
URL string
|
||||||
|
}
|
35
catalog/linkgroup/dsl.go
Normal file
35
catalog/linkgroup/dsl.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2022 Mya Pitzeruse
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
package linkgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mjpitz/emc/catalog/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(label string, options ...Option) Spec {
|
||||||
|
spec := Spec{
|
||||||
|
Label: label,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option defines an optional component of the spec.
|
||||||
|
type Option func(spec *Spec)
|
||||||
|
|
||||||
|
// Spec defines the elements needed to render a link group.
|
||||||
|
type Spec struct {
|
||||||
|
Label string
|
||||||
|
Links []link.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func Link(label, url string) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.Links = append(spec.Links, link.New(label, url))
|
||||||
|
}
|
||||||
|
}
|
65
catalog/serve.go
Normal file
65
catalog/serve.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2022 Mya Pitzeruse
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"flag"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mjpitz/emc/catalog/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed index.html.tpl
|
||||||
|
var catalog string
|
||||||
|
|
||||||
|
// Spec defines the requirements for hosting a catalog.
|
||||||
|
type Spec struct {
|
||||||
|
Services []service.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option defines an optional component of the spec.
|
||||||
|
type Option func(spec *Spec)
|
||||||
|
|
||||||
|
// Service registers a known service with the catalog.
|
||||||
|
func Service(label string, options ...service.Option) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.Services = append(spec.Services, service.New(label, options...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve provides command line functionality for running the service catalog.
|
||||||
|
func Serve(options ...Option) {
|
||||||
|
addr := flag.String("bind_address", "127.0.0.1:8080", "the address the service should bind to when serving content")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
t := template.Must(template.New("catalog").Parse(catalog))
|
||||||
|
|
||||||
|
spec := Spec{}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
html := bytes.NewBuffer(nil)
|
||||||
|
err := t.Execute(html, spec)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeContent(w, r, "", start, bytes.NewReader(html.Bytes()))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("serving on %s\n", *addr)
|
||||||
|
err = http.ListenAndServe(*addr, http.DefaultServeMux)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
70
catalog/service/dsl.go
Normal file
70
catalog/service/dsl.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (C) 2022 Mya Pitzeruse
|
||||||
|
// The MIT License (MIT)
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mjpitz/mjpitz/apps/emc/internal/catalog/linkgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New constructs a spec given a label and set of options.
|
||||||
|
func New(label string, options ...Option) Spec {
|
||||||
|
spec := Spec{
|
||||||
|
Label: label,
|
||||||
|
Metadata: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(&spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option defines an optional component of the spec.
|
||||||
|
type Option func(spec *Spec)
|
||||||
|
|
||||||
|
// Spec defines the elements needed to render a service.
|
||||||
|
type Spec struct {
|
||||||
|
Label string
|
||||||
|
LogoURL string
|
||||||
|
Description string
|
||||||
|
URL string
|
||||||
|
Metadata map[string]string
|
||||||
|
LinkGroups []linkgroup.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoURL configures the icon for the service.
|
||||||
|
func LogoURL(url string) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.LogoURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description specifies a short, brief description about the service.
|
||||||
|
func Description(description string) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.Description = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL configures the services public facing URL that's presented to end users.
|
||||||
|
func URL(url string) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.URL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata allows additional metadata to be attached to a service.
|
||||||
|
func Metadata(key, value string) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.Metadata[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkGroup appends a group of links to the provided service.
|
||||||
|
func LinkGroup(label string, options ...linkgroup.Option) Option {
|
||||||
|
return func(spec *Spec) {
|
||||||
|
spec.LinkGroups = append(spec.LinkGroups, linkgroup.New(label, options...))
|
||||||
|
}
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/mjpitz/emc
|
||||||
|
|
||||||
|
go 1.18
|
0
go.sum
Normal file
0
go.sum
Normal file
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 406 KiB |
Loading…
Reference in New Issue
Block a user