Commit fb2e780d authored by ale's avatar ale

Initial commit

parents
Pipeline #1131 failed with stages
in 12 seconds
image: docker:latest
stages:
- build
- docker_build
- release
services:
- docker:dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
RELEASE_TAG: $CI_REGISTRY_IMAGE:latest
GIT_SUBMODULE_STRATEGY: recursive
build:
stage: build
image: "ai/build:stretch"
script:
- env DEBIAN_FRONTEND=noninteractive apt-get -qy install make golang-go
- make
artifacts:
paths:
- build/
docker_build:
stage: docker_build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.git.autistici.org
- docker build --build-arg ci_token=$CI_JOB_TOKEN --pull -t $IMAGE_TAG .
- docker push $IMAGE_TAG
dependencies:
- build
release:
stage: release
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.git.autistici.org
- docker pull $IMAGE_TAG
- docker tag $IMAGE_TAG $RELEASE_TAG
- docker push $RELEASE_TAG
only:
- master
FROM scratch
ADD build/float-dashboard /
CMD ["/float-dashboard"]
all: build/float-dashboard
build/float-dashboard: $(wildcard *.go templates/*.html)
-mkdir -p build
# Do not run 'go generate' on Docker builds.
#go generate
go build -o $@ -tags netgo -a -ldflags -w .
This diff is collapsed.
package main
//go:generate go-bindata --nocompress --pkg main static/... templates/...
//go:generate go run scripts/sri.go --package main --output sri_auto.go static
import (
"flag"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"git.autistici.org/ai3/go-common/serverutil"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
)
var (
addr = flag.String("addr", ":3000", "tcp `address` to listen on")
configPath = flag.String("config", "/etc/float/dashboard.yml", "dashboard config `file`")
floatServicesPath = flag.String("services", "/etc/float/services.json", "float services `file`")
tpl *template.Template
config *Config
services ServiceMap
)
// Config for the application.
type Config struct {
Domain string `yaml:"domain"`
PublicDomains []string `yaml:"public_domains"`
LogoImagePath string `yaml:"logo_image"`
HTTP *serverutil.ServerConfig `yaml:"http_server"`
}
func readYAML(path string, obj interface{}) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return yaml.Unmarshal(data, obj)
}
// Read the program's configuration file.
func mustReadConfig() *Config {
var c Config
if err := readYAML(*configPath, &c); err != nil {
log.Fatalf("error reading configuration: %v", err)
}
return &c
}
// Read the configured services.
func mustReadServices() ServiceMap {
m := make(map[string]*Service)
if err := readYAML(*floatServicesPath, &m); err != nil {
log.Fatalf("error reading services file: %v", err)
}
for name, s := range m {
s.Name = name
}
return m
}
// Parse the templates that are embedded with the binary (in bindata.go).
func mustParseEmbeddedTemplates() *template.Template {
root := template.New("").Funcs(template.FuncMap{
// Useful function to format something as YAML.
"yaml": func(obj interface{}) string {
if data, err := yaml.Marshal(obj); err == nil {
return string(data)
}
return ""
},
"sri_script": SRIScript,
"sri_stylesheet": SRIStylesheet,
})
files, err := AssetDir("templates")
if err != nil {
log.Fatalf("no asset dir for templates: %v", err)
}
for _, f := range files {
b, err := Asset("templates/" + f)
if err != nil {
log.Fatalf("could not read embedded template %s: %v", f, err)
}
if _, err := root.New(f).Parse(string(b)); err != nil {
log.Fatalf("error parsing template %s: %v", f, err)
}
}
return root
}
// A relatively strict CSP.
const contentSecurityPolicy = "default-src 'none'; img-src 'self' data:; script-src 'self'; style-src 'self'; font-src 'self';"
func withDynamicHeaders(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Expires", "-1")
w.Header().Set("X-Frame-Options", "NONE")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-Content-Type-Options", "nosniff")
if w.Header().Get("Content-Security-Policy") == "" {
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
}
h.ServeHTTP(w, r)
})
}
func renderTemplate(w io.Writer, name string, ctx map[string]interface{}) {
if ctx == nil {
ctx = make(map[string]interface{})
}
ctx["Config"] = config
ctx["Services"] = services
if err := tpl.ExecuteTemplate(w, name, ctx); err != nil {
log.Printf("template error: %v", err)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
renderTemplate(w, "index.html", nil)
}
func handleServiceDetails(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
s, ok := services[vars["service"]]
if !ok {
http.NotFound(w, r)
return
}
renderTemplate(w, "service_details.html", map[string]interface{}{
"Service": s,
})
}
func handleContainerDetails(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
s, ok := services[vars["service"]]
if !ok {
http.NotFound(w, r)
return
}
var c *Container
for _, cc := range s.Containers {
if cc.Name == vars["container"] {
c = &cc
break
}
}
if c == nil {
http.NotFound(w, r)
return
}
renderTemplate(w, "container_details.html", map[string]interface{}{
"Service": s,
"Container": c,
})
}
// Create the HTTP server.
func makeServer() http.Handler {
root := mux.NewRouter()
root.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(&assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo,
Prefix: "static",
})))
root.Handle("/service/{service}/details", withDynamicHeaders(http.HandlerFunc(handleServiceDetails)))
root.Handle("/service/{service}/container/{container}/details", withDynamicHeaders(http.HandlerFunc(handleContainerDetails)))
root.Handle("/", withDynamicHeaders(http.HandlerFunc(handleIndex)))
return root
}
func main() {
log.SetFlags(0)
flag.Parse()
tpl = mustParseEmbeddedTemplates()
config = mustReadConfig()
services = mustReadServices()
h := makeServer()
if err := serverutil.Serve(h, config.HTTP, *addr); err != nil {
log.Fatal(err)
}
}
type sortedStringList []string
func (l sortedStringList) Len() int { return len(l) }
func (l sortedStringList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l sortedStringList) Less(i, j int) bool { return l[i] < l[j] }
package main
import (
"crypto/sha512"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
var (
outputPath = flag.String("output", "", "output `file` name")
packageName = flag.String("package", "", "`name` of the package for generated code")
stripPrefix = flag.String("strip", "", "prefix to strip from `path`s to generate URLs")
)
func computeChecksum(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
sha := sha512.Sum384(data)
return "sha384-" + base64.StdEncoding.EncodeToString(sha[:]), nil
}
func urlForPath(path string) string {
path = strings.TrimPrefix(path, *stripPrefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}
func mkSRIMap(m map[string]string, dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
// Only match files with the desired extensions.
if info.IsDir() || !match(info.Name()) {
return nil
}
if cksum, err := computeChecksum(path); err == nil {
m[urlForPath(path)] = cksum
}
return nil
})
}
func match(name string) bool {
switch filepath.Ext(name) {
case ".js", ".json", ".css":
return true
default:
return false
}
}
func codegen(w io.Writer, m map[string]string) {
fmt.Fprintf(w, "package %s\n", *packageName)
io.WriteString(w, `
import (
"fmt"
"html/template"
)
var sriMap = map[string]string{
`)
for k, v := range m {
fmt.Fprintf(w, "\t%q: %q,\n", k, v)
}
io.WriteString(w, `}
func SRIScript(uri string) template.HTML {
s := fmt.Sprintf("<script src=\"%s\"", uri)
if sri, ok := sriMap[uri]; ok {
s += fmt.Sprintf(" crossorigin=\"\" integrity=\"%s\"", sri)
}
s += "></script>"
return template.HTML(s)
}
func SRIStylesheet(uri string) template.HTML {
s := fmt.Sprintf("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\"", uri)
if sri, ok := sriMap[uri]; ok {
s += fmt.Sprintf(" integrity=\"%s\"", sri)
}
s += ">"
return template.HTML(s)
}
`)
}
func main() {
log.SetFlags(0)
flag.Parse()
m := make(map[string]string)
for _, path := range flag.Args() {
if err := mkSRIMap(m, path); err != nil {
log.Println(err)
}
}
var w io.Writer = os.Stdout
if *outputPath != "" {
var err error
w, err = os.Create(*outputPath)
if err != nil {
log.Fatal(err)
}
}
codegen(w, m)
}
package main
import (
"fmt"
"sort"
)
type MonitoringEndpoint struct {
JobName string `yaml:"job_name"`
Port int `yaml:"port"`
Scheme string `yaml:"scheme,omitempty"`
}
func (m MonitoringEndpoint) TargetStatusURL() string {
return fmt.Sprintf("https://monitor.%s/graph?expr=ok{job=%s}", config.Domain, m.JobName)
}
func (m MonitoringEndpoint) ServiceDashboardURL() string {
return fmt.Sprintf("https://grafana.%s/service-dashboard?service=%s", config.Domain, m.JobName)
}
type PublicEndpoint struct {
Name string `yaml:"name"`
Domains []string `yaml:"domains,omitempty"`
Port int `yaml:"port"`
Scheme string `yaml:"scheme,omitempty"`
EnableSSOProxy bool `yaml:"enable_sso_proxy,omitempty"`
}
func (p PublicEndpoint) URL() string {
return fmt.Sprintf("https://%s.%s/", p.Name, config.PublicDomains[0])
}
func (p PublicEndpoint) LogsURL() string {
return fmt.Sprintf("https://logs.%s/kibana/bla/bla/?vhost=%s.%s", config.Domain, p.Name, config.PublicDomains[0])
}
// Boolean fields that default to true are problematic, so we use
// pointers to detect the case where the value is unset.
type ServiceCredentials struct {
Name string `yaml:"name"`
EnableClient *bool `yaml:"enable_client,omitempty"`
EnableServer *bool `yaml:"enable_server,omitempty"`
}
func (c *ServiceCredentials) HasClient() bool {
if c.EnableClient == nil {
return true
}
return *c.EnableClient
}
func (c *ServiceCredentials) HasServer() bool {
if c.EnableServer == nil {
return true
}
return *c.EnableServer
}
type Container struct {
Name string `yaml:"name"`
Image string `yaml:"image"`
Port int `yaml:"port"`
Volumes []map[string]string `yaml:"volumes,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
}
type Service struct {
Name string
NumInstances int `yaml:"num_instances,omitempty"`
SchedulingGroup string `yaml:"scheduling_group"`
MasterElection bool `yaml:"master_election,omitempty"`
MasterSchedulingGroup string `yaml:"master_scheduling_group,omitempty"`
Ports []int `yaml:"ports,omitempty"`
ServiceCredentials []ServiceCredentials `yaml:"service_credentials,omitempty"`
LDAPCredentials []struct {
Name string `yaml:"name"`
} `yaml:"ldap_credentials,omitempty"`
MonitoringEndpoints []MonitoringEndpoint `yaml:"monitoring_endpoints,omitempty"`
PublicEndpoints []PublicEndpoint `yaml:"public_endpoints,omitempty"`
Containers []Container `yaml:"containers,omitempty"`
}
type ServiceMap map[string]*Service
func (m ServiceMap) Keys() []string {
var out []string
for k := range m {
out = append(out, k)
}
sort.Sort(sortedStringList(out))
return out
}
func (s *Service) HasClientServiceCredentials() bool {
for _, c := range s.ServiceCredentials {
if c.HasClient() {
return true
}
}
return false
}
func (s *Service) HasServerServiceCredentials() bool {
for _, c := range s.ServiceCredentials {
if c.HasServer() {
return true
}
}
return false
}
func (s *Service) HasPublicURLs() bool {
return len(s.PublicEndpoints) > 0
}
func (s *Service) HasMonitoringEndpoints() bool {
return len(s.MonitoringEndpoints) > 0
}
package main
import (
"fmt"
"html/template"
)
var sriMap = map[string]string{
"/static/css/style.css": "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
"/static/js/bootstrap.bundle.min.js": "sha384-CS0nxkpPy+xUkNGhObAISrkg/xjb3USVCwy+0/NMzd5VxgY4CMCyTkItmy5n0voC",
"/static/js/jquery.slim.min.js": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
"/static/css/bootstrap.min.css": "sha384-Smlep5jCw/wG7hdkwQ/Z5nLIefveQRIY9nfy6xoR1uRYBtpZgI6339F5dgvm/e9B",
"/static/css/open-iconic-bootstrap.min.css": "sha384-wWci3BOzr88l+HNsAtr3+e5bk9qh5KfjU6gl/rbzfTYdsAVHBEbxB33veLYmFg/a",
}
func SRIScript(uri string) template.HTML {
s := fmt.Sprintf("<script src=\"%s\"", uri)
if sri, ok := sriMap[uri]; ok {
s += fmt.Sprintf(" crossorigin=\"\" integrity=\"%s\"", sri)
}
s += "></script>"
return template.HTML(s)
}
func SRIStylesheet(uri string) template.HTML {
s := fmt.Sprintf("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\"", uri)
if sri, ok := sriMap[uri]; ok {
s += fmt.Sprintf(" integrity=\"%s\"", sri)
}
s += ">"
return template.HTML(s)
}
This diff is collapsed.
This diff is collapsed.
@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{{template "header" "container details"}}
{{$domain := .Config.Domain}}
<h1>{{$domain}} / service {{.Service.Name}} / container {{.Container.Name}}</h1>
<pre>{{.Container | yaml}}</pre>
{{template "footer"}}
{{define "footer"}}
<hr>
<div class="container-fluid">
<div class="float-right">
float
</div>
</div>
</div>
{{sri_script "/static/js/jquery.slim.min.js"}}
{{sri_script "/static/js/bootstrap.bundle.min.js"}}