mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-27 15:41:41 +01:00
Add 'plugins/traefik/' from commit 'aef1f9e0dd205ea9cdea9e3ccf11900c5fe79b1f'
git-subtree-dir: plugins/traefik git-subtree-mainline:1a14070131git-subtree-split:aef1f9e0dd
This commit is contained in:
201
plugins/traefik/pkg/pages/error.go
Normal file
201
plugins/traefik/pkg/pages/error.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"path"
|
||||
)
|
||||
|
||||
var errorPage = `<!doctype html>
|
||||
<html lang="en-US">
|
||||
|
||||
<head>
|
||||
<title>Ondemand - Error</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
|
||||
|
||||
<link rel="shortcut icon"
|
||||
href="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" />
|
||||
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: 'Inter', 'system-ui', sans-serif;
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
background-color: #c7d0d9;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 10rem 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #c7d0d9;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
align-items: center;
|
||||
background-color: #212124;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 6rem 0 0 0;
|
||||
max-width: 100vw;
|
||||
padding: 3.5rem 6.2rem;
|
||||
width: 56rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin: 0 0 4rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: #ffffff;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 5rem;
|
||||
margin: 0 0 1.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
bottom: 1rem;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@media (max-width: 56rem) {
|
||||
body {
|
||||
background-image: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<img
|
||||
src="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg">
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="headline" id="headline">Error loading {{ .Name }}.</h2>
|
||||
|
||||
<p class="message text" id="message">There was an error loading your instance.</p>
|
||||
|
||||
|
||||
<div class="support text">
|
||||
{{ .Error }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer text">
|
||||
<a href="https://github.com/acouvreur/traefik-ondemand-plugin"
|
||||
target="_blank">acouvreur/traefik-ondemand-plugin</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
|
||||
type ErrorData struct {
|
||||
Name string
|
||||
Error string
|
||||
}
|
||||
|
||||
func GetErrorPage(template_path string, name string, e string) string {
|
||||
var tpl *template.Template
|
||||
var err error
|
||||
if template_path != "" {
|
||||
tpl, err = template.New(path.Base(template_path)).ParseFiles(template_path)
|
||||
} else {
|
||||
tpl, err = template.New("error").Parse(errorPage)
|
||||
}
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
b := bytes.Buffer{}
|
||||
err = tpl.Execute(&b, ErrorData{
|
||||
Name: name,
|
||||
Error: e,
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
164
plugins/traefik/pkg/pages/error.html
Normal file
164
plugins/traefik/pkg/pages/error.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
|
||||
<head>
|
||||
<title>Ondemand - Error</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
|
||||
|
||||
<link rel="shortcut icon"
|
||||
href="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" />
|
||||
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: 'Inter', 'system-ui', sans-serif;
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
background-color: #c7d0d9;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 10rem 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #c7d0d9;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
align-items: center;
|
||||
background-color: #212124;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 6rem 0 0 0;
|
||||
max-width: 100vw;
|
||||
padding: 3.5rem 6.2rem;
|
||||
width: 56rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin: 0 0 4rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: #ffffff;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 5rem;
|
||||
margin: 0 0 1.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
bottom: 1rem;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@media (max-width: 56rem) {
|
||||
body {
|
||||
background-image: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<img
|
||||
src="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg">
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="headline" id="headline">Error loading {{ .Name }}.</h2>
|
||||
|
||||
<p class="message text" id="message">There was an error loading your instance.</p>
|
||||
|
||||
|
||||
<div class="support text">
|
||||
{{ .Error }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer text">
|
||||
<a href="https://github.com/acouvreur/traefik-ondemand-plugin"
|
||||
target="_blank">acouvreur/traefik-ondemand-plugin</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
310
plugins/traefik/pkg/pages/loading.go
Normal file
310
plugins/traefik/pkg/pages/loading.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
var loadingPage = `<!doctype html>
|
||||
<html lang="en-US">
|
||||
|
||||
<head>
|
||||
<title>Ondemand - Loading</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
|
||||
|
||||
<link rel="shortcut icon"
|
||||
href="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" />
|
||||
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: 'Inter', 'system-ui', sans-serif;
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
background-color: #c7d0d9;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 10rem 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #c7d0d9;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
align-items: center;
|
||||
background-color: #212124;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 6rem 0 0 0;
|
||||
max-width: 100vw;
|
||||
padding: 3.5rem 6.2rem;
|
||||
width: 56rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin: 0 0 4rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: #ffffff;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 5rem;
|
||||
margin: 0 0 1.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
bottom: 1rem;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@media (max-width: 56rem) {
|
||||
body {
|
||||
background-image: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<img
|
||||
src="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg">
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="headline" id="headline">{{ .Name }} is loading...</h2>
|
||||
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
||||
|
||||
<p class="message text" id="message">Your instance is loading, and will be
|
||||
ready shortly.</p>
|
||||
|
||||
|
||||
<div class="support text">
|
||||
Your instance will shutdown automatically after {{ .Timeout }} of
|
||||
inactivity.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer text">
|
||||
<a href="https://github.com/acouvreur/traefik-ondemand-plugin"
|
||||
target="_blank">acouvreur/traefik-ondemand-plugin</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>`
|
||||
|
||||
type LoadingData struct {
|
||||
Name string
|
||||
Timeout string
|
||||
}
|
||||
|
||||
func GetLoadingPage(template_path string, name string, timeout time.Duration) string {
|
||||
var tpl *template.Template
|
||||
var err error
|
||||
if template_path != "" {
|
||||
tpl, err = template.New(path.Base(template_path)).ParseFiles(template_path)
|
||||
} else {
|
||||
tpl, err = template.New("loading").Parse(loadingPage)
|
||||
}
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
b := bytes.Buffer{}
|
||||
err = tpl.Execute(&b, LoadingData{
|
||||
Name: name,
|
||||
Timeout: humanizeDuration(timeout),
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// humanizeDuration humanizes time.Duration output to a meaningful value,
|
||||
// golang's default ``time.Duration`` output is badly formatted and unreadable.
|
||||
func humanizeDuration(duration time.Duration) string {
|
||||
if duration.Seconds() < 60.0 {
|
||||
return fmt.Sprintf("%d seconds", int64(duration.Seconds()))
|
||||
}
|
||||
if duration.Minutes() < 60.0 {
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
if remainingSeconds > 0 {
|
||||
return fmt.Sprintf("%d minutes %d seconds", int64(duration.Minutes()), int64(remainingSeconds))
|
||||
}
|
||||
return fmt.Sprintf("%d minutes", int64(duration.Minutes()))
|
||||
}
|
||||
if duration.Hours() < 24.0 {
|
||||
remainingMinutes := math.Mod(duration.Minutes(), 60)
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
|
||||
if remainingMinutes > 0 {
|
||||
if remainingSeconds > 0 {
|
||||
return fmt.Sprintf("%d hours %d minutes %d seconds", int64(duration.Hours()), int64(remainingMinutes), int64(remainingSeconds))
|
||||
}
|
||||
return fmt.Sprintf("%d hours %d minutes", int64(duration.Hours()), int64(remainingMinutes))
|
||||
}
|
||||
return fmt.Sprintf("%d hours", int64(duration.Hours()))
|
||||
}
|
||||
remainingHours := math.Mod(duration.Hours(), 24)
|
||||
remainingMinutes := math.Mod(duration.Minutes(), 60)
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
return fmt.Sprintf("%d days %d hours %d minutes %d seconds",
|
||||
int64(duration.Hours()/24), int64(remainingHours),
|
||||
int64(remainingMinutes), int64(remainingSeconds))
|
||||
}
|
||||
236
plugins/traefik/pkg/pages/loading.html
Normal file
236
plugins/traefik/pkg/pages/loading.html
Normal file
@@ -0,0 +1,236 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
|
||||
<head>
|
||||
<title>Ondemand - Loading</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
|
||||
|
||||
<link rel="shortcut icon"
|
||||
href="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" />
|
||||
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: 'Inter', 'system-ui', sans-serif;
|
||||
font-size: 62.5%;
|
||||
height: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
background-color: #c7d0d9;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 10rem 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #c7d0d9;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
align-items: center;
|
||||
background-color: #212124;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 6rem 0 0 0;
|
||||
max-width: 100vw;
|
||||
padding: 3.5rem 6.2rem;
|
||||
width: 56rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin: 0 0 4rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: #ffffff;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 5rem;
|
||||
margin: 0 0 1.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
bottom: 1rem;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@media (max-width: 56rem) {
|
||||
body {
|
||||
background-image: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.panel>*:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<img
|
||||
src="https://docs.traefik.io/assets/images/logo-traefik-proxy-logo.svg">
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="headline" id="headline">{{ .Name }} is loading...</h2>
|
||||
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
||||
|
||||
<p class="message text" id="message">Your instance is loading, and will be
|
||||
ready shortly.</p>
|
||||
|
||||
|
||||
<div class="support text">
|
||||
Your instance will shutdown automatically after {{ .Timeout }} of
|
||||
inactivity.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer text">
|
||||
<a href="https://github.com/acouvreur/traefik-ondemand-plugin"
|
||||
target="_blank">acouvreur/traefik-ondemand-plugin</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
59
plugins/traefik/pkg/strategy/blocking_strategy.go
Normal file
59
plugins/traefik/pkg/strategy/blocking_strategy.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BlockingStrategy struct {
|
||||
Requests []string
|
||||
Name string
|
||||
Next http.Handler
|
||||
Timeout time.Duration
|
||||
BlockDelay time.Duration
|
||||
BlockCheckInterval time.Duration
|
||||
}
|
||||
|
||||
type InternalServerError struct {
|
||||
ServiceName string `json:"serviceName"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// ServeHTTP retrieve the service status
|
||||
func (e *BlockingStrategy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
for start := time.Now(); time.Since(start) < e.BlockDelay; {
|
||||
notReadyCount := 0
|
||||
for _, request := range e.Requests {
|
||||
|
||||
log.Printf("Sending request: %s", request)
|
||||
status, err := getServiceStatus(request)
|
||||
log.Printf("Status: %s", status)
|
||||
|
||||
if err != nil {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(rw).Encode(InternalServerError{ServiceName: e.Name, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if status != "started" {
|
||||
notReadyCount++
|
||||
}
|
||||
}
|
||||
if notReadyCount == 0 {
|
||||
// Services all started forward request
|
||||
e.Next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(e.BlockCheckInterval)
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
json.NewEncoder(rw).Encode(InternalServerError{ServiceName: e.Name, Error: fmt.Sprintf("Service was unreachable within %s", e.BlockDelay)})
|
||||
}
|
||||
88
plugins/traefik/pkg/strategy/blocking_strategy_test.go
Normal file
88
plugins/traefik/pkg/strategy/blocking_strategy_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSingleBlockingStrategy_ServeHTTP(t *testing.T) {
|
||||
for _, test := range SingleServiceTestCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.onDemandServiceResponses[0].status)
|
||||
fmt.Fprint(w, test.onDemandServiceResponses[0].body)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
blockingStrategy := &BlockingStrategy{
|
||||
Name: "whoami",
|
||||
Requests: []string{mockServer.URL},
|
||||
Next: next,
|
||||
BlockDelay: 1 * time.Second,
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
|
||||
|
||||
blockingStrategy.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expected.blocking, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleBlockingStrategy_ServeHTTP(t *testing.T) {
|
||||
|
||||
for _, test := range MultipleServicesTestCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
urls := make([]string, len(test.onDemandServiceResponses))
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
for responseIndex, response := range test.onDemandServiceResponses {
|
||||
response := response
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(response.status)
|
||||
fmt.Fprint(w, response.body)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
urls[responseIndex] = mockServer.URL
|
||||
}
|
||||
fmt.Println(urls)
|
||||
blockingStrategy := &BlockingStrategy{
|
||||
Name: "whoami",
|
||||
Requests: urls,
|
||||
Next: next,
|
||||
BlockDelay: 1 * time.Second,
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
|
||||
|
||||
blockingStrategy.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expected.blocking, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
62
plugins/traefik/pkg/strategy/dynamic_strategy.go
Normal file
62
plugins/traefik/pkg/strategy/dynamic_strategy.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/acouvreur/traefik-ondemand-plugin/pkg/pages"
|
||||
)
|
||||
|
||||
type DynamicStrategy struct {
|
||||
Requests []string
|
||||
Name string
|
||||
Next http.Handler
|
||||
Timeout time.Duration
|
||||
DisplayName string
|
||||
LoadingPage string
|
||||
ErrorPage string
|
||||
}
|
||||
|
||||
// ServeHTTP retrieve the service status
|
||||
func (e *DynamicStrategy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
started := make([]bool, len(e.Requests))
|
||||
|
||||
displayName := e.Name
|
||||
if len(e.DisplayName) > 0 {
|
||||
displayName = e.DisplayName
|
||||
}
|
||||
|
||||
notReadyCount := 0
|
||||
for requestIndex, request := range e.Requests {
|
||||
log.Printf("Sending request: %s", request)
|
||||
status, err := getServiceStatus(request)
|
||||
log.Printf("Status: %s", status)
|
||||
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, displayName, err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
if status == "started" {
|
||||
started[requestIndex] = true
|
||||
} else if status == "starting" {
|
||||
started[requestIndex] = false
|
||||
notReadyCount++
|
||||
} else {
|
||||
// Error
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(pages.GetErrorPage(e.ErrorPage, displayName, status)))
|
||||
return
|
||||
}
|
||||
}
|
||||
if notReadyCount == 0 {
|
||||
// All services are ready, forward request
|
||||
e.Next.ServeHTTP(rw, req)
|
||||
} else {
|
||||
// Services still starting, notify client
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
rw.Write([]byte(pages.GetLoadingPage(e.LoadingPage, displayName, e.Timeout)))
|
||||
}
|
||||
}
|
||||
77
plugins/traefik/pkg/strategy/dynamic_strategy_test.go
Normal file
77
plugins/traefik/pkg/strategy/dynamic_strategy_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSingleDynamicStrategy_ServeHTTP(t *testing.T) {
|
||||
|
||||
for _, test := range SingleServiceTestCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, test.onDemandServiceResponses[0].body)
|
||||
}))
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
dynamicStrategy := &DynamicStrategy{
|
||||
Name: "whoami",
|
||||
Requests: []string{mockServer.URL},
|
||||
Next: next,
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
|
||||
|
||||
dynamicStrategy.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expected.dynamic, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleDynamicStrategy_ServeHTTP(t *testing.T) {
|
||||
for _, test := range MultipleServicesTestCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
urls := make([]string, len(test.onDemandServiceResponses))
|
||||
for responseIndex, response := range test.onDemandServiceResponses {
|
||||
response := response
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, response.body)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
urls[responseIndex] = mockServer.URL
|
||||
}
|
||||
dynamicStrategy := &DynamicStrategy{
|
||||
Name: "whoami",
|
||||
Requests: urls,
|
||||
Next: next,
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://mydomain/whoami", nil)
|
||||
|
||||
dynamicStrategy.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expected.dynamic, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
39
plugins/traefik/pkg/strategy/strategy.go
Normal file
39
plugins/traefik/pkg/strategy/strategy.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Net client is a custom client to timeout after 2 seconds if the service is not ready
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 2,
|
||||
}
|
||||
|
||||
type Strategy interface {
|
||||
ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
func getServiceStatus(request string) (string, error) {
|
||||
|
||||
// This request wakes up the service if he's scaled to 0
|
||||
resp, err := netClient.Get(request)
|
||||
if err != nil {
|
||||
return "error", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "parsing error", err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return "error from ondemand service", errors.New(string(body))
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(string(body), "\n"), nil
|
||||
}
|
||||
135
plugins/traefik/pkg/strategy/strategy_test_cases.go
Normal file
135
plugins/traefik/pkg/strategy/strategy_test_cases.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package strategy
|
||||
|
||||
type OnDemandServiceResponses struct {
|
||||
body string
|
||||
status int
|
||||
}
|
||||
type ExpectedStatusForStrategy struct {
|
||||
dynamic int
|
||||
blocking int
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
desc string
|
||||
onDemandServiceResponses []OnDemandServiceResponses
|
||||
expected ExpectedStatusForStrategy
|
||||
}
|
||||
|
||||
var SingleServiceTestCases = []TestCase{
|
||||
{
|
||||
desc: "service is / keeps on starting",
|
||||
onDemandServiceResponses: GenerateServicesResponses(1, "starting"),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 202,
|
||||
blocking: 503,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "service is started",
|
||||
onDemandServiceResponses: GenerateServicesResponses(1, "started"),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 200,
|
||||
blocking: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ondemand service is in error",
|
||||
onDemandServiceResponses: GenerateServicesResponses(1, "error"),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 500,
|
||||
blocking: 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func GenerateServicesResponses(count int, serviceBody string) []OnDemandServiceResponses {
|
||||
responses := make([]OnDemandServiceResponses, count)
|
||||
for i := 0; i < count; i++ {
|
||||
if serviceBody == "starting" || serviceBody == "started" {
|
||||
responses[i] = OnDemandServiceResponses{
|
||||
body: serviceBody,
|
||||
status: 200,
|
||||
}
|
||||
} else {
|
||||
responses[i] = OnDemandServiceResponses{
|
||||
body: serviceBody,
|
||||
status: 503,
|
||||
}
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
var MultipleServicesTestCases = []TestCase{
|
||||
{
|
||||
desc: "all services are starting",
|
||||
onDemandServiceResponses: GenerateServicesResponses(5, "starting"),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 202,
|
||||
blocking: 503,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "one started others are starting",
|
||||
onDemandServiceResponses: append(GenerateServicesResponses(1, "starting"), GenerateServicesResponses(4, "started")...),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 202,
|
||||
blocking: 503,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "one starting others are started",
|
||||
onDemandServiceResponses: append(GenerateServicesResponses(4, "starting"), GenerateServicesResponses(1, "started")...),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 202,
|
||||
blocking: 503,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "one errored others are starting",
|
||||
onDemandServiceResponses: append(
|
||||
GenerateServicesResponses(2, "starting"),
|
||||
append(
|
||||
GenerateServicesResponses(1, "error"),
|
||||
GenerateServicesResponses(2, "starting")...,
|
||||
)...,
|
||||
),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 500,
|
||||
blocking: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "one errored others are started",
|
||||
onDemandServiceResponses: append(
|
||||
GenerateServicesResponses(1, "error"),
|
||||
GenerateServicesResponses(4, "started")...,
|
||||
),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 500,
|
||||
blocking: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "one errored others are mix of starting / started",
|
||||
onDemandServiceResponses: append(
|
||||
GenerateServicesResponses(2, "started"),
|
||||
append(
|
||||
GenerateServicesResponses(1, "error"),
|
||||
GenerateServicesResponses(2, "starting")...,
|
||||
)...,
|
||||
),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 500,
|
||||
blocking: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "all are started",
|
||||
onDemandServiceResponses: GenerateServicesResponses(5, "started"),
|
||||
expected: ExpectedStatusForStrategy{
|
||||
dynamic: 200,
|
||||
blocking: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user