mirror of https://github.com/coder/coder.git
feat: add switch http(s) button to error page (#12942)
This commit is contained in:
parent
848ea7e9f1
commit
8ba05a9052
3
Makefile
3
Makefile
|
@ -200,7 +200,8 @@ endef
|
||||||
# calling this manually.
|
# calling this manually.
|
||||||
$(CODER_ALL_BINARIES): go.mod go.sum \
|
$(CODER_ALL_BINARIES): go.mod go.sum \
|
||||||
$(GO_SRC_FILES) \
|
$(GO_SRC_FILES) \
|
||||||
$(shell find ./examples/templates)
|
$(shell find ./examples/templates) \
|
||||||
|
site/static/error.html
|
||||||
|
|
||||||
$(get-mode-os-arch-ext)
|
$(get-mode-os-arch-ext)
|
||||||
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
|
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
|
||||||
|
|
|
@ -4,11 +4,14 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
@ -23,6 +26,7 @@ import (
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/v2/coderd/tracing"
|
"github.com/coder/coder/v2/coderd/tracing"
|
||||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||||
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||||
"github.com/coder/coder/v2/site"
|
"github.com/coder/coder/v2/site"
|
||||||
"github.com/coder/coder/v2/tailnet"
|
"github.com/coder/coder/v2/tailnet"
|
||||||
|
@ -341,7 +345,7 @@ type ServerTailnet struct {
|
||||||
totalConns *prometheus.CounterVec
|
totalConns *prometheus.CounterVec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID) *httputil.ReverseProxy {
|
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID, app appurl.ApplicationURL, wildcardHostname string) *httputil.ReverseProxy {
|
||||||
// Rewrite the targetURL's Host to point to the agent's IP. This is
|
// Rewrite the targetURL's Host to point to the agent's IP. This is
|
||||||
// necessary because due to TCP connection caching, each agent needs to be
|
// necessary because due to TCP connection caching, each agent needs to be
|
||||||
// addressed invidivually. Otherwise, all connections get dialed as
|
// addressed invidivually. Otherwise, all connections get dialed as
|
||||||
|
@ -351,13 +355,46 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
|
||||||
tgt.Host = net.JoinHostPort(tailnet.IPFromUUID(agentID).String(), port)
|
tgt.Host = net.JoinHostPort(tailnet.IPFromUUID(agentID).String(), port)
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(&tgt)
|
proxy := httputil.NewSingleHostReverseProxy(&tgt)
|
||||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, theErr error) {
|
||||||
|
var (
|
||||||
|
desc = "Failed to proxy request to application: " + theErr.Error()
|
||||||
|
additionalInfo = ""
|
||||||
|
additionalButtonLink = ""
|
||||||
|
additionalButtonText = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
var tlsError tls.RecordHeaderError
|
||||||
|
if (errors.As(theErr, &tlsError) && tlsError.Msg == "first record does not look like a TLS handshake") ||
|
||||||
|
errors.Is(theErr, http.ErrSchemeMismatch) {
|
||||||
|
// If the error is due to an HTTP/HTTPS mismatch, we can provide a
|
||||||
|
// more helpful error message with redirect buttons.
|
||||||
|
switchURL := url.URL{
|
||||||
|
Scheme: dashboardURL.Scheme,
|
||||||
|
}
|
||||||
|
_, protocol, isPort := app.PortInfo()
|
||||||
|
if isPort {
|
||||||
|
targetProtocol := "https"
|
||||||
|
if protocol == "https" {
|
||||||
|
targetProtocol = "http"
|
||||||
|
}
|
||||||
|
app = app.ChangePortProtocol(targetProtocol)
|
||||||
|
|
||||||
|
switchURL.Host = fmt.Sprintf("%s%s", app.String(), strings.TrimPrefix(wildcardHostname, "*"))
|
||||||
|
additionalButtonLink = switchURL.String()
|
||||||
|
additionalButtonText = fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol))
|
||||||
|
additionalInfo += fmt.Sprintf("This error seems to be due to an app protocol mismatch, try switching to %s.", strings.ToUpper(targetProtocol))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
|
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
|
||||||
Status: http.StatusBadGateway,
|
Status: http.StatusBadGateway,
|
||||||
Title: "Bad Gateway",
|
Title: "Bad Gateway",
|
||||||
Description: "Failed to proxy request to application: " + err.Error(),
|
Description: desc,
|
||||||
RetryEnabled: true,
|
RetryEnabled: true,
|
||||||
DashboardURL: dashboardURL.String(),
|
DashboardURL: dashboardURL.String(),
|
||||||
|
AdditionalInfo: additionalInfo,
|
||||||
|
AdditionalButtonLink: additionalButtonLink,
|
||||||
|
AdditionalButtonText: additionalButtonText,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
proxy.Director = s.director(agentID, proxy.Director)
|
proxy.Director = s.director(agentID, proxy.Director)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/coder/coder/v2/agent/agenttest"
|
"github.com/coder/coder/v2/agent/agenttest"
|
||||||
"github.com/coder/coder/v2/agent/proto"
|
"github.com/coder/coder/v2/agent/proto"
|
||||||
"github.com/coder/coder/v2/coderd"
|
"github.com/coder/coder/v2/coderd"
|
||||||
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||||
"github.com/coder/coder/v2/tailnet"
|
"github.com/coder/coder/v2/tailnet"
|
||||||
|
@ -81,7 +82,7 @@ func TestServerTailnet_ReverseProxy_ProxyEnv(t *testing.T) {
|
||||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
@ -112,7 +113,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
@ -143,7 +144,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
@ -177,7 +178,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -222,7 +223,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
u, err := url.Parse("http://127.0.0.1" + port)
|
u, err := url.Parse("http://127.0.0.1" + port)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
@ -279,7 +280,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, ag := range agents {
|
for i, ag := range agents {
|
||||||
rp := serverTailnet.ReverseProxy(u, u, ag.id)
|
rp := serverTailnet.ReverseProxy(u, u, ag.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
@ -317,7 +318,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
uri, err := url.Parse(s.URL)
|
uri, err := url.Parse(s.URL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(uri, uri, a.id)
|
rp := serverTailnet.ReverseProxy(uri, uri, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
@ -347,7 +348,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
@ -83,6 +84,55 @@ func (a ApplicationURL) Path() string {
|
||||||
return fmt.Sprintf("/@%s/%s.%s/apps/%s", a.Username, a.WorkspaceName, a.AgentName, a.AppSlugOrPort)
|
return fmt.Sprintf("/@%s/%s.%s/apps/%s", a.Username, a.WorkspaceName, a.AgentName, a.AppSlugOrPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PortInfo returns the port, protocol, and whether the AppSlugOrPort is a port or not.
|
||||||
|
func (a ApplicationURL) PortInfo() (uint, string, bool) {
|
||||||
|
var (
|
||||||
|
port uint64
|
||||||
|
protocol string
|
||||||
|
isPort bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if strings.HasSuffix(a.AppSlugOrPort, "s") {
|
||||||
|
trimmed := strings.TrimSuffix(a.AppSlugOrPort, "s")
|
||||||
|
port, err = strconv.ParseUint(trimmed, 10, 16)
|
||||||
|
if err == nil {
|
||||||
|
protocol = "https"
|
||||||
|
isPort = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port, err = strconv.ParseUint(a.AppSlugOrPort, 10, 16)
|
||||||
|
if err == nil {
|
||||||
|
protocol = "http"
|
||||||
|
isPort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(port), protocol, isPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApplicationURL) ChangePortProtocol(target string) ApplicationURL {
|
||||||
|
newAppURL := *a
|
||||||
|
port, protocol, isPort := a.PortInfo()
|
||||||
|
if !isPort {
|
||||||
|
return newAppURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == protocol {
|
||||||
|
return newAppURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == "https" {
|
||||||
|
newAppURL.AppSlugOrPort = fmt.Sprintf("%ds", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == "http" {
|
||||||
|
newAppURL.AppSlugOrPort = fmt.Sprintf("%d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAppURL
|
||||||
|
}
|
||||||
|
|
||||||
// ParseSubdomainAppURL parses an ApplicationURL from the given subdomain. If
|
// ParseSubdomainAppURL parses an ApplicationURL from the given subdomain. If
|
||||||
// the subdomain is not a valid application URL hostname, returns a non-nil
|
// the subdomain is not a valid application URL hostname, returns a non-nil
|
||||||
// error. If the hostname is not a subdomain of the given base hostname, returns
|
// error. If the hostname is not a subdomain of the given base hostname, returns
|
||||||
|
|
|
@ -124,6 +124,16 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||||
Username: "user",
|
Username: "user",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Port--Agent--Workspace--User",
|
||||||
|
Subdomain: "8080s--agent--workspace--user",
|
||||||
|
Expected: appurl.ApplicationURL{
|
||||||
|
AppSlugOrPort: "8080s",
|
||||||
|
AgentName: "agent",
|
||||||
|
WorkspaceName: "workspace",
|
||||||
|
Username: "user",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "HyphenatedNames",
|
Name: "HyphenatedNames",
|
||||||
Subdomain: "app-slug--agent-name--workspace-name--user-name",
|
Subdomain: "app-slug--agent-name--workspace-name--user-name",
|
||||||
|
|
|
@ -66,7 +66,7 @@ var nonCanonicalHeaders = map[string]string{
|
||||||
type AgentProvider interface {
|
type AgentProvider interface {
|
||||||
// ReverseProxy returns an httputil.ReverseProxy for proxying HTTP requests
|
// ReverseProxy returns an httputil.ReverseProxy for proxying HTTP requests
|
||||||
// to the specified agent.
|
// to the specified agent.
|
||||||
ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID) *httputil.ReverseProxy
|
ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID, app appurl.ApplicationURL, wildcardHost string) *httputil.ReverseProxy
|
||||||
|
|
||||||
// AgentConn returns a new connection to the specified agent.
|
// AgentConn returns a new connection to the specified agent.
|
||||||
AgentConn(ctx context.Context, agentID uuid.UUID) (_ *workspacesdk.AgentConn, release func(), _ error)
|
AgentConn(ctx context.Context, agentID uuid.UUID) (_ *workspacesdk.AgentConn, release func(), _ error)
|
||||||
|
@ -314,7 +314,7 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.proxyWorkspaceApp(rw, r, *token, chiPath)
|
s.proxyWorkspaceApp(rw, r, *token, chiPath, appurl.ApplicationURL{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSubdomain handles subdomain-based application proxy requests (aka.
|
// HandleSubdomain handles subdomain-based application proxy requests (aka.
|
||||||
|
@ -417,7 +417,7 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.proxyWorkspaceApp(rw, r, *token, r.URL.Path)
|
s.proxyWorkspaceApp(rw, r, *token, r.URL.Path, app)
|
||||||
})).ServeHTTP(rw, r.WithContext(ctx))
|
})).ServeHTTP(rw, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -476,7 +476,7 @@ func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next htt
|
||||||
return app, true
|
return app, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appToken SignedToken, path string) {
|
func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appToken SignedToken, path string, app appurl.ApplicationURL) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
// Filter IP headers from untrusted origins.
|
// Filter IP headers from untrusted origins.
|
||||||
|
@ -545,8 +545,12 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
|
||||||
|
|
||||||
r.URL.Path = path
|
r.URL.Path = path
|
||||||
appURL.RawQuery = ""
|
appURL.RawQuery = ""
|
||||||
|
_, protocol, isPort := app.PortInfo()
|
||||||
|
if isPort {
|
||||||
|
appURL.Scheme = protocol
|
||||||
|
}
|
||||||
|
|
||||||
proxy := s.AgentProvider.ReverseProxy(appURL, s.DashboardURL, appToken.AgentID)
|
proxy := s.AgentProvider.ReverseProxy(appURL, s.DashboardURL, appToken.AgentID, app, s.Hostname)
|
||||||
|
|
||||||
proxy.ModifyResponse = func(r *http.Response) error {
|
proxy.ModifyResponse = func(r *http.Response) error {
|
||||||
r.Header.Del(httpmw.AccessControlAllowOriginHeader)
|
r.Header.Del(httpmw.AccessControlAllowOriginHeader)
|
||||||
|
|
15
site/site.go
15
site/site.go
|
@ -786,12 +786,15 @@ func extractBin(dest string, r io.Reader) (numExtracted int, err error) {
|
||||||
type ErrorPageData struct {
|
type ErrorPageData struct {
|
||||||
Status int
|
Status int
|
||||||
// HideStatus will remove the status code from the page.
|
// HideStatus will remove the status code from the page.
|
||||||
HideStatus bool
|
HideStatus bool
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
RetryEnabled bool
|
RetryEnabled bool
|
||||||
DashboardURL string
|
DashboardURL string
|
||||||
Warnings []string
|
Warnings []string
|
||||||
|
AdditionalInfo string
|
||||||
|
AdditionalButtonLink string
|
||||||
|
AdditionalButtonText string
|
||||||
|
|
||||||
RenderDescriptionMarkdown bool
|
RenderDescriptionMarkdown bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ running). */}}
|
||||||
.container {
|
.container {
|
||||||
--side-padding: 24px;
|
--side-padding: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc(320px + var(--side-padding) * 2);
|
max-width: calc(500px + var(--side-padding) * 2);
|
||||||
padding: 0 var(--side-padding);
|
padding: 0 var(--side-padding);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,9 @@ running). */}}
|
||||||
{{- if .Error.RenderDescriptionMarkdown }} {{ .ErrorDescriptionHTML }} {{
|
{{- if .Error.RenderDescriptionMarkdown }} {{ .ErrorDescriptionHTML }} {{
|
||||||
else }}
|
else }}
|
||||||
<p>{{ .Error.Description }}</p>
|
<p>{{ .Error.Description }}</p>
|
||||||
|
{{ end }} {{- if .Error.AdditionalInfo }}
|
||||||
|
<br />
|
||||||
|
<p>{{ .Error.AdditionalInfo }}</p>
|
||||||
{{ end }} {{- if .Error.Warnings }}
|
{{ end }} {{- if .Error.Warnings }}
|
||||||
<div class="warning">
|
<div class="warning">
|
||||||
<div class="warning-title">
|
<div class="warning-title">
|
||||||
|
@ -195,7 +198,11 @@ running). */}}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
{{- if .Error.RetryEnabled }}
|
{{- if and .Error.AdditionalButtonText .Error.AdditionalButtonLink }}
|
||||||
|
<a href="{{ .Error.AdditionalButtonLink }}"
|
||||||
|
>{{ .Error.AdditionalButtonText }}</a
|
||||||
|
>
|
||||||
|
{{ end }} {{- if .Error.RetryEnabled }}
|
||||||
<button onclick="window.location.reload()">Retry</button>
|
<button onclick="window.location.reload()">Retry</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="{{ .Error.DashboardURL }}">Back to site</a>
|
<a href="{{ .Error.DashboardURL }}">Back to site</a>
|
||||||
|
|
Loading…
Reference in New Issue