Skip to content

Commit c1ecc91

Browse files
authored
feat: Add fallback troubleshooting URL for coder agents (#5005)
1 parent 1f4f0ce commit c1ecc91

File tree

11 files changed

+161
-52
lines changed

11 files changed

+161
-52
lines changed

cli/cliui/agent_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/coder/coder/cli/cliui"
1313
"github.com/coder/coder/codersdk"
1414
"github.com/coder/coder/pty/ptytest"
15+
"github.com/coder/coder/testutil"
1516
)
1617

1718
func TestAgent(t *testing.T) {
@@ -49,3 +50,50 @@ func TestAgent(t *testing.T) {
4950
disconnected.Store(true)
5051
<-done
5152
}
53+
54+
func TestAgentTimeoutWithTroubleshootingURL(t *testing.T) {
55+
t.Parallel()
56+
57+
ctx, _ := testutil.Context(t)
58+
59+
wantURL := "https://coder.com/troubleshoot"
60+
61+
var connected, timeout atomic.Bool
62+
cmd := &cobra.Command{
63+
RunE: func(cmd *cobra.Command, args []string) error {
64+
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
65+
WorkspaceName: "example",
66+
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
67+
agent := codersdk.WorkspaceAgent{
68+
Status: codersdk.WorkspaceAgentConnecting,
69+
TroubleshootingURL: "https://coder.com/troubleshoot",
70+
}
71+
switch {
72+
case connected.Load():
73+
agent.Status = codersdk.WorkspaceAgentConnected
74+
case timeout.Load():
75+
agent.Status = codersdk.WorkspaceAgentTimeout
76+
}
77+
return agent, nil
78+
},
79+
FetchInterval: time.Millisecond,
80+
WarnInterval: 5 * time.Millisecond,
81+
})
82+
return err
83+
},
84+
}
85+
ptty := ptytest.New(t)
86+
cmd.SetOutput(ptty.Output())
87+
cmd.SetIn(ptty.Input())
88+
done := make(chan struct{})
89+
go func() {
90+
defer close(done)
91+
err := cmd.ExecuteContext(ctx)
92+
assert.NoError(t, err)
93+
}()
94+
ptty.ExpectMatch("Don't panic")
95+
timeout.Store(true)
96+
ptty.ExpectMatch(wantURL)
97+
connected.Store(true)
98+
<-done
99+
}

cli/deployment/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,13 @@ func newConfig() *codersdk.DeploymentConfig {
347347
Hidden: true,
348348
Default: 10 * time.Minute,
349349
},
350+
AgentFallbackTroubleshootingURL: &codersdk.DeploymentConfigField[string]{
351+
Name: "Agent Fallback Troubleshooting URL",
352+
Usage: "URL to use for agent troubleshooting when not set in the template",
353+
Flag: "agent-fallback-troubleshooting-url",
354+
Hidden: true,
355+
Default: "https://coder.com/docs/coder-oss/latest/templates#troubleshooting-templates",
356+
},
350357
AuditLogging: &codersdk.DeploymentConfigField[bool]{
351358
Name: "Audit Logging",
352359
Usage: "Specifies whether audit logging is enabled.",

coderd/provisionerjobs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
262262
}
263263
}
264264

265-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout)
265+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
266266
if err != nil {
267267
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
268268
Message: "Internal error reading job agent.",

coderd/workspaceagents.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
5252
})
5353
return
5454
}
55-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout)
55+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
5656
if err != nil {
5757
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
5858
Message: "Internal error reading workspace agent.",
@@ -67,7 +67,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
6767
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
6868
ctx := r.Context()
6969
workspaceAgent := httpmw.WorkspaceAgent(r)
70-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout)
70+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
7171
if err != nil {
7272
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
7373
Message: "Internal error reading workspace agent.",
@@ -138,7 +138,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
138138
func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Request) {
139139
ctx := r.Context()
140140
workspaceAgent := httpmw.WorkspaceAgent(r)
141-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout)
141+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
142142
if err != nil {
143143
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
144144
Message: "Internal error reading workspace agent.",
@@ -192,7 +192,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
192192
httpapi.ResourceNotFound(rw)
193193
return
194194
}
195-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout)
195+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
196196
if err != nil {
197197
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
198198
Message: "Internal error reading workspace agent.",
@@ -269,7 +269,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
269269
return
270270
}
271271

272-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout)
272+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
273273
if err != nil {
274274
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
275275
Message: "Internal error reading workspace agent.",
@@ -660,14 +660,18 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
660660
return apps
661661
}
662662

663-
func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator, dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentInactiveDisconnectTimeout time.Duration) (codersdk.WorkspaceAgent, error) {
663+
func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator, dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string) (codersdk.WorkspaceAgent, error) {
664664
var envs map[string]string
665665
if dbAgent.EnvironmentVariables.Valid {
666666
err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs)
667667
if err != nil {
668668
return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal env vars: %w", err)
669669
}
670670
}
671+
troubleshootingURL := agentFallbackTroubleshootingURL
672+
if dbAgent.TroubleshootingURL != "" {
673+
troubleshootingURL = dbAgent.TroubleshootingURL
674+
}
671675
workspaceAgent := codersdk.WorkspaceAgent{
672676
ID: dbAgent.ID,
673677
CreatedAt: dbAgent.CreatedAt,
@@ -683,7 +687,7 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
683687
Directory: dbAgent.Directory,
684688
Apps: apps,
685689
ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds,
686-
TroubleshootingURL: dbAgent.TroubleshootingURL,
690+
TroubleshootingURL: troubleshootingURL,
687691
}
688692
node := coordinator.Node(dbAgent.ID)
689693
if node != nil {

coderd/workspaceagents_test.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,48 @@ func TestWorkspaceAgent(t *testing.T) {
7676
_, err = client.WorkspaceAgent(ctx, workspace.LatestBuild.Resources[0].Agents[0].ID)
7777
require.NoError(t, err)
7878
})
79+
t.Run("HasFallbackTroubleshootingURL", func(t *testing.T) {
80+
t.Parallel()
81+
client := coderdtest.New(t, &coderdtest.Options{
82+
IncludeProvisionerDaemon: true,
83+
})
84+
user := coderdtest.CreateFirstUser(t, client)
85+
authToken := uuid.NewString()
86+
tmpDir := t.TempDir()
87+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
88+
Parse: echo.ParseComplete,
89+
ProvisionPlan: echo.ProvisionComplete,
90+
ProvisionApply: []*proto.Provision_Response{{
91+
Type: &proto.Provision_Response_Complete{
92+
Complete: &proto.Provision_Complete{
93+
Resources: []*proto.Resource{{
94+
Name: "example",
95+
Type: "aws_instance",
96+
Agents: []*proto.Agent{{
97+
Id: uuid.NewString(),
98+
Directory: tmpDir,
99+
Auth: &proto.Agent_Token{
100+
Token: authToken,
101+
},
102+
}},
103+
}},
104+
},
105+
},
106+
}},
107+
})
108+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
109+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
110+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
111+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
112+
113+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
114+
defer cancel()
115+
116+
workspace, err := client.Workspace(ctx, workspace.ID)
117+
require.NoError(t, err)
118+
require.NotEmpty(t, workspace.LatestBuild.Resources[0].Agents[0].TroubleshootingURL)
119+
t.Log(workspace.LatestBuild.Resources[0].Agents[0].TroubleshootingURL)
120+
})
79121
t.Run("Timeout", func(t *testing.T) {
80122
t.Parallel()
81123
client := coderdtest.New(t, &coderdtest.Options{
@@ -84,6 +126,9 @@ func TestWorkspaceAgent(t *testing.T) {
84126
user := coderdtest.CreateFirstUser(t, client)
85127
authToken := uuid.NewString()
86128
tmpDir := t.TempDir()
129+
130+
wantTroubleshootingURL := "https://example.com/troubleshoot"
131+
87132
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
88133
Parse: echo.ParseComplete,
89134
ProvisionPlan: echo.ProvisionComplete,
@@ -100,7 +145,7 @@ func TestWorkspaceAgent(t *testing.T) {
100145
Token: authToken,
101146
},
102147
ConnectionTimeoutSeconds: 1,
103-
TroubleshootingUrl: "https://example.com/troubleshoot",
148+
TroubleshootingUrl: wantTroubleshootingURL,
104149
}},
105150
}},
106151
},
@@ -115,13 +160,16 @@ func TestWorkspaceAgent(t *testing.T) {
115160
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
116161
defer cancel()
117162

163+
var err error
118164
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
119-
workspace, err := client.Workspace(ctx, workspace.ID)
165+
workspace, err = client.Workspace(ctx, workspace.ID)
120166
if !assert.NoError(t, err) {
121167
return false
122168
}
123169
return workspace.LatestBuild.Resources[0].Agents[0].Status == codersdk.WorkspaceAgentTimeout
124170
}, testutil.IntervalMedium, "agent status timeout")
171+
172+
require.Equal(t, wantTroubleshootingURL, workspace.LatestBuild.Resources[0].Agents[0].TroubleshootingURL)
125173
})
126174
}
127175

coderd/workspacebuilds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ func (api *API) convertWorkspaceBuild(
902902
apiAgents := make([]codersdk.WorkspaceAgent, 0)
903903
for _, agent := range agents {
904904
apps := appsByAgentID[agent.ID]
905-
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout)
905+
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
906906
if err != nil {
907907
return codersdk.WorkspaceBuild{}, xerrors.Errorf("converting workspace agent: %w", err)
908908
}

codersdk/deploymentconfig.go

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,36 @@ import (
1111

1212
// DeploymentConfig is the central configuration for the coder server.
1313
type DeploymentConfig struct {
14-
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
15-
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
16-
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
17-
AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"`
18-
DERP *DERP `json:"derp" typescript:",notnull"`
19-
GitAuth *DeploymentConfigField[[]GitAuthConfig] `json:"gitauth" typescript:",notnull"`
20-
Prometheus *PrometheusConfig `json:"prometheus" typescript:",notnull"`
21-
Pprof *PprofConfig `json:"pprof" typescript:",notnull"`
22-
ProxyTrustedHeaders *DeploymentConfigField[[]string] `json:"proxy_trusted_headers" typescript:",notnull"`
23-
ProxyTrustedOrigins *DeploymentConfigField[[]string] `json:"proxy_trusted_origins" typescript:",notnull"`
24-
CacheDirectory *DeploymentConfigField[string] `json:"cache_directory" typescript:",notnull"`
25-
InMemoryDatabase *DeploymentConfigField[bool] `json:"in_memory_database" typescript:",notnull"`
26-
PostgresURL *DeploymentConfigField[string] `json:"pg_connection_url" typescript:",notnull"`
27-
OAuth2 *OAuth2Config `json:"oauth2" typescript:",notnull"`
28-
OIDC *OIDCConfig `json:"oidc" typescript:",notnull"`
29-
Telemetry *TelemetryConfig `json:"telemetry" typescript:",notnull"`
30-
TLS *TLSConfig `json:"tls" typescript:",notnull"`
31-
Trace *TraceConfig `json:"trace" typescript:",notnull"`
32-
SecureAuthCookie *DeploymentConfigField[bool] `json:"secure_auth_cookie" typescript:",notnull"`
33-
SSHKeygenAlgorithm *DeploymentConfigField[string] `json:"ssh_keygen_algorithm" typescript:",notnull"`
34-
AutoImportTemplates *DeploymentConfigField[[]string] `json:"auto_import_templates" typescript:",notnull"`
35-
MetricsCacheRefreshInterval *DeploymentConfigField[time.Duration] `json:"metrics_cache_refresh_interval" typescript:",notnull"`
36-
AgentStatRefreshInterval *DeploymentConfigField[time.Duration] `json:"agent_stat_refresh_interval" typescript:",notnull"`
37-
AuditLogging *DeploymentConfigField[bool] `json:"audit_logging" typescript:",notnull"`
38-
BrowserOnly *DeploymentConfigField[bool] `json:"browser_only" typescript:",notnull"`
39-
SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"`
40-
Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"`
41-
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
42-
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
14+
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
15+
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
16+
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
17+
AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"`
18+
DERP *DERP `json:"derp" typescript:",notnull"`
19+
GitAuth *DeploymentConfigField[[]GitAuthConfig] `json:"gitauth" typescript:",notnull"`
20+
Prometheus *PrometheusConfig `json:"prometheus" typescript:",notnull"`
21+
Pprof *PprofConfig `json:"pprof" typescript:",notnull"`
22+
ProxyTrustedHeaders *DeploymentConfigField[[]string] `json:"proxy_trusted_headers" typescript:",notnull"`
23+
ProxyTrustedOrigins *DeploymentConfigField[[]string] `json:"proxy_trusted_origins" typescript:",notnull"`
24+
CacheDirectory *DeploymentConfigField[string] `json:"cache_directory" typescript:",notnull"`
25+
InMemoryDatabase *DeploymentConfigField[bool] `json:"in_memory_database" typescript:",notnull"`
26+
PostgresURL *DeploymentConfigField[string] `json:"pg_connection_url" typescript:",notnull"`
27+
OAuth2 *OAuth2Config `json:"oauth2" typescript:",notnull"`
28+
OIDC *OIDCConfig `json:"oidc" typescript:",notnull"`
29+
Telemetry *TelemetryConfig `json:"telemetry" typescript:",notnull"`
30+
TLS *TLSConfig `json:"tls" typescript:",notnull"`
31+
Trace *TraceConfig `json:"trace" typescript:",notnull"`
32+
SecureAuthCookie *DeploymentConfigField[bool] `json:"secure_auth_cookie" typescript:",notnull"`
33+
SSHKeygenAlgorithm *DeploymentConfigField[string] `json:"ssh_keygen_algorithm" typescript:",notnull"`
34+
AutoImportTemplates *DeploymentConfigField[[]string] `json:"auto_import_templates" typescript:",notnull"`
35+
MetricsCacheRefreshInterval *DeploymentConfigField[time.Duration] `json:"metrics_cache_refresh_interval" typescript:",notnull"`
36+
AgentStatRefreshInterval *DeploymentConfigField[time.Duration] `json:"agent_stat_refresh_interval" typescript:",notnull"`
37+
AgentFallbackTroubleshootingURL *DeploymentConfigField[string] `json:"agent_fallback_troubleshooting_url" typescript:",notnull"`
38+
AuditLogging *DeploymentConfigField[bool] `json:"audit_logging" typescript:",notnull"`
39+
BrowserOnly *DeploymentConfigField[bool] `json:"browser_only" typescript:",notnull"`
40+
SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"`
41+
Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"`
42+
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
43+
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
4344
}
4445

4546
type DERP struct {

codersdk/workspaceagents.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type WorkspaceAgent struct {
5656
// DERPLatency is mapped by region name (e.g. "New York City", "Seattle").
5757
DERPLatency map[string]DERPRegion `json:"latency,omitempty"`
5858
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
59-
TroubleshootingURL string `json:"troubleshooting_url,omitempty"`
59+
TroubleshootingURL string `json:"troubleshooting_url"`
6060
}
6161

6262
type WorkspaceAgentResourceMetadata struct {

site/src/api/typesGenerated.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ export interface DeploymentConfig {
299299
readonly auto_import_templates: DeploymentConfigField<string[]>
300300
readonly metrics_cache_refresh_interval: DeploymentConfigField<number>
301301
readonly agent_stat_refresh_interval: DeploymentConfigField<number>
302+
readonly agent_fallback_troubleshooting_url: DeploymentConfigField<string>
302303
readonly audit_logging: DeploymentConfigField<boolean>
303304
readonly browser_only: DeploymentConfigField<boolean>
304305
readonly scim_api_key: DeploymentConfigField<string>
@@ -817,7 +818,7 @@ export interface WorkspaceAgent {
817818
readonly apps: WorkspaceApp[]
818819
readonly latency?: Record<string, DERPRegion>
819820
readonly connection_timeout_seconds: number
820-
readonly troubleshooting_url?: string
821+
readonly troubleshooting_url: string
821822
}
822823

823824
// From codersdk/workspaceagents.go

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy