From 7271e8fa4ca2daeb24aa770a5adbd19ba95427e7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 11:15:19 -0500 Subject: [PATCH 01/22] feat: Add workspace proxy enterprise cli commands --- enterprise/cli/root.go | 1 + enterprise/cli/workspaceproxy.go | 362 ++++++++++++++++++++++++++ enterprise/cli/workspaceproxy_slim.go | 37 +++ 3 files changed, 400 insertions(+) create mode 100644 enterprise/cli/workspaceproxy.go create mode 100644 enterprise/cli/workspaceproxy_slim.go diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go index b70c91ead5728..a89a8008fc977 100644 --- a/enterprise/cli/root.go +++ b/enterprise/cli/root.go @@ -12,6 +12,7 @@ type RootCmd struct { func (r *RootCmd) enterpriseOnly() []*clibase.Cmd { return []*clibase.Cmd{ r.server(), + r.workspaceProxy(), r.features(), r.licenses(), r.groups(), diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go new file mode 100644 index 0000000000000..0c0f68ab115d4 --- /dev/null +++ b/enterprise/cli/workspaceproxy.go @@ -0,0 +1,362 @@ +//go:build !slim + +package cli + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "net/http/pprof" + "net/url" + "os/signal" + "regexp" + rpprof "runtime/pprof" + "time" + + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/coreos/go-systemd/daemon" + + "github.com/coder/coder/cli/cliui" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli" + "github.com/coder/coder/coderd/workspaceapps" + "github.com/coder/coder/enterprise/wsproxy" + + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/codersdk" +) + +func (r *RootCmd) workspaceProxy() *clibase.Cmd { + cmd := &clibase.Cmd{ + Use: "workspace-proxy", + Short: "Manage workspace proxies", + Aliases: []string{"proxy"}, + Hidden: true, + Handler: func(inv *clibase.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + Children: []*clibase.Cmd{ + r.proxyServer(), + r.registerProxy(), + }, + } + + return cmd +} + +func (r *RootCmd) registerProxy() *clibase.Cmd { + client := new(codersdk.Client) + cmd := &clibase.Cmd{ + Use: "register", + Short: "Register a workspace proxy", + Middleware: clibase.Chain( + clibase.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(i *clibase.Invocation) error { + ctx := i.Context() + name := i.Args[0] + // TODO: Fix all this + resp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{ + Name: name, + DisplayName: name, + Icon: "whocares.png", + URL: "http://localhost:6005", + WildcardHostname: "", + }) + if err != nil { + return xerrors.Errorf("create workspace proxy: %w", err) + } + + fmt.Println(resp.ProxyToken) + return nil + }, + } + return cmd +} + +type closers []func() + +func (c closers) Close() { + for _, closeF := range c { + closeF() + } +} + +func (c *closers) Add(f func()) { + *c = append(*c, f) +} + +func (r *RootCmd) proxyServer() *clibase.Cmd { + var ( + // TODO: Remove options that we do not need + cfg = new(codersdk.DeploymentValues) + opts = cfg.Options() + ) + var _ = opts + + client := new(codersdk.Client) + cmd := &clibase.Cmd{ + Use: "server", + Short: "Start a workspace proxy server", + Options: opts, + Middleware: clibase.Chain( + cli.WriteConfigMW(cfg), + cli.PrintDeprecatedOptions(), + clibase.RequireNArgs(0), + // We need a client to connect with the primary coderd instance. + r.InitClient(client), + ), + Handler: func(inv *clibase.Invocation) error { + var closers closers + // Main command context for managing cancellation of running + // services. + ctx, topCancel := context.WithCancel(inv.Context()) + defer topCancel() + closers.Add(topCancel) + + go cli.DumpHandler(ctx) + + cli.PrintLogo(inv) + logger, logCloser, err := cli.BuildLogger(inv, cfg) + if err != nil { + return xerrors.Errorf("make logger: %w", err) + } + defer logCloser() + closers.Add(logCloser) + + logger.Debug(ctx, "started debug logging") + logger.Sync() + + // Register signals early on so that graceful shutdown can't + // be interrupted by additional signals. Note that we avoid + // shadowing cancel() (from above) here because notifyStop() + // restores default behavior for the signals. This protects + // the shutdown sequence from abruptly terminating things + // like: database migrations, provisioner work, workspace + // cleanup in dev-mode, etc. + // + // To get out of a graceful shutdown, the user can send + // SIGQUIT with ctrl+\ or SIGKILL with `kill -9`. + notifyCtx, notifyStop := signal.NotifyContext(ctx, cli.InterruptSignals...) + defer notifyStop() + + // Clean up idle connections at the end, e.g. + // embedded-postgres can leave an idle connection + // which is caught by goleaks. + defer http.DefaultClient.CloseIdleConnections() + closers.Add(http.DefaultClient.CloseIdleConnections) + + tracer, _ := cli.ConfigureTraceProvider(ctx, logger, inv, cfg) + + httpServers, err := cli.ConfigureHTTPServers(inv, cfg) + if err != nil { + return xerrors.Errorf("configure http(s): %w", err) + } + defer httpServers.Close() + closers.Add(httpServers.Close) + + // TODO: @emyrk I find this strange that we add this to the context + // at the root here. + ctx, httpClient, err := cli.ConfigureHTTPClient( + ctx, + cfg.TLS.ClientCertFile.String(), + cfg.TLS.ClientKeyFile.String(), + cfg.TLS.ClientCAFile.String(), + ) + if err != nil { + return xerrors.Errorf("configure http client: %w", err) + } + defer httpClient.CloseIdleConnections() + closers.Add(httpClient.CloseIdleConnections) + + // Warn the user if the access URL appears to be a loopback address. + isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value()) + if isLocal || err != nil { + reason := "could not be resolved" + if isLocal { + reason = "isn't externally reachable" + } + cliui.Warnf( + inv.Stderr, + "The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", + cliui.Styles.Field.Render(cfg.AccessURL.String()), reason, + ) + } + + // A newline is added before for visibility in terminal output. + cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String()) + + var appHostnameRegex *regexp.Regexp + appHostname := cfg.WildcardAccessURL.String() + if appHostname != "" { + appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) + if err != nil { + return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err) + } + } + + realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins) + if err != nil { + return xerrors.Errorf("parse real ip config: %w", err) + } + + if cfg.Pprof.Enable { + // This prevents the pprof import from being accidentally deleted. + // pprof has an init function that attaches itself to the default handler. + // By passing a nil handler to 'serverHandler', it will automatically use + // the default, which has pprof attached. + _ = pprof.Handler + //nolint:revive + closeFunc := cli.ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof") + defer closeFunc() + closers.Add(closeFunc) + } + + prometheusRegistry := prometheus.NewRegistry() + if cfg.Prometheus.Enable { + prometheusRegistry.MustRegister(collectors.NewGoCollector()) + prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + + //nolint:revive + closeFunc := cli.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( + prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}), + ), cfg.Prometheus.Address.String(), "prometheus") + defer closeFunc() + closers.Add(closeFunc) + } + + pu, _ := url.Parse("http://localhost:3000") + proxy, err := wsproxy.New(&wsproxy.Options{ + Logger: logger, + // TODO: PrimaryAccessURL + PrimaryAccessURL: pu, + AccessURL: cfg.AccessURL.Value(), + AppHostname: appHostname, + AppHostnameRegex: appHostnameRegex, + RealIPConfig: realIPConfig, + // TODO: AppSecurityKey + AppSecurityKey: workspaceapps.SecurityKey{}, + Tracing: tracer, + PrometheusRegistry: prometheusRegistry, + APIRateLimit: int(cfg.RateLimit.API.Value()), + SecureAuthCookie: cfg.SecureAuthCookie.Value(), + // TODO: DisablePathApps + DisablePathApps: false, + // TODO: ProxySessionToken + ProxySessionToken: "", + }) + if err != nil { + return xerrors.Errorf("create workspace proxy: %w", err) + } + + shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) + defer shutdownConns() + closers.Add(shutdownConns) + // ReadHeaderTimeout is purposefully not enabled. It caused some + // issues with websockets over the dev tunnel. + // See: https://github.com/coder/coder/pull/3730 + //nolint:gosec + httpServer := &http.Server{ + // These errors are typically noise like "TLS: EOF". Vault does + // similar: + // https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714 + ErrorLog: log.New(io.Discard, "", 0), + Handler: proxy.Handler, + BaseContext: func(_ net.Listener) context.Context { + return shutdownConnsCtx + }, + } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = httpServer.Shutdown(ctx) + }() + + // TODO: So this obviously is not going to work well. + errCh := make(chan error, 1) + go rpprof.Do(ctx, rpprof.Labels("service", "workspace-proxy"), func(ctx context.Context) { + errCh <- httpServers.Serve(httpServer) + }) + + cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") + + // Updates the systemd status from activating to activated. + _, err = daemon.SdNotify(false, daemon.SdNotifyReady) + if err != nil { + return xerrors.Errorf("notify systemd: %w", err) + } + + // Currently there is no way to ask the server to shut + // itself down, so any exit signal will result in a non-zero + // exit of the server. + var exitErr error + select { + case exitErr = <-errCh: + case <-notifyCtx.Done(): + exitErr = notifyCtx.Err() + _, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Bold.Render( + "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit", + )) + } + + if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) { + cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr) + } + + // Begin clean shut down stage, we try to shut down services + // gracefully in an order that gives the best experience. + // This procedure should not differ greatly from the order + // of `defer`s in this function, but allows us to inform + // the user about what's going on and handle errors more + // explicitly. + + _, err = daemon.SdNotify(false, daemon.SdNotifyStopping) + if err != nil { + cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err) + } + + // Stop accepting new connections without interrupting + // in-flight requests, give in-flight requests 5 seconds to + // complete. + cliui.Info(inv.Stdout, "Shutting down API server..."+"\n") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err = httpServer.Shutdown(shutdownCtx) + if err != nil { + cliui.Errorf(inv.Stderr, "API server shutdown took longer than 3s: %s\n", err) + } else { + cliui.Info(inv.Stdout, "Gracefully shut down API server\n") + } + // Cancel any remaining in-flight requests. + shutdownConns() + + // Trigger context cancellation for any remaining services. + closers.Close() + + switch { + case xerrors.Is(exitErr, context.DeadlineExceeded): + cliui.Warnf(inv.Stderr, "Graceful shutdown timed out") + // Errors here cause a significant number of benign CI failures. + return nil + case xerrors.Is(exitErr, context.Canceled): + return nil + case exitErr != nil: + return xerrors.Errorf("graceful shutdown: %w", exitErr) + default: + return nil + } + }, + } + + return cmd +} diff --git a/enterprise/cli/workspaceproxy_slim.go b/enterprise/cli/workspaceproxy_slim.go new file mode 100644 index 0000000000000..bb296a2cb73a3 --- /dev/null +++ b/enterprise/cli/workspaceproxy_slim.go @@ -0,0 +1,37 @@ +//go:build slim + +package cli + +import ( + "fmt" + "io" + "os" + + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/cli/cliui" +) + +func (r *RootCmd) workspaceProxy() *clibase.Cmd { + root := &clibase.Cmd{ + Use: "workspace-proxy", + Short: "Manage workspace proxies", + Aliases: []string{"proxy"}, + // We accept RawArgs so all commands and flags are accepted. + RawArgs: true, + Hidden: true, + Handler: func(inv *clibase.Invocation) error { + serverUnsupported(inv.Stderr) + return nil + }, + } + + return root +} + +func serverUnsupported(w io.Writer) { + _, _ = fmt.Fprintf(w, "You are using a 'slim' build of Coder, which does not support the %s subcommand.\n", cliui.Styles.Code.Render("server")) + _, _ = fmt.Fprintln(w, "") + _, _ = fmt.Fprintln(w, "Please use a build of Coder from GitHub releases:") + _, _ = fmt.Fprintln(w, " https://github.com/coder/coder/releases") + os.Exit(1) +} From 0a1af727f91a93361ab0bbc4149a1fa203da311c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 11:34:40 -0500 Subject: [PATCH 02/22] chore: Handle custom workspace proxy options. Remove excess --- cli/clibase/option.go | 10 +++ codersdk/deployment.go | 101 ++++++++++++++++++++++--------- enterprise/cli/workspaceproxy.go | 34 ++++++++--- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/cli/clibase/option.go b/cli/clibase/option.go index 076346e004ce7..390e9e073f2ca 100644 --- a/cli/clibase/option.go +++ b/cli/clibase/option.go @@ -80,6 +80,16 @@ func (s *OptionSet) Add(opts ...Option) { *s = append(*s, opts...) } +func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet { + cpy := make(OptionSet, 0) + for _, opt := range s { + if filter(opt) { + cpy = append(cpy, opt) + } + } + return cpy +} + // FlagSet returns a pflag.FlagSet for the OptionSet. func (s *OptionSet) FlagSet() *pflag.FlagSet { if s == nil { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 2e153d02e462a..b0623a56729ca 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -333,10 +333,17 @@ type DangerousConfig struct { } const ( - flagEnterpriseKey = "enterprise" - flagSecretKey = "secret" + flagEnterpriseKey = "enterprise" + flagSecretKey = "secret" + flagExternalProxies = "external_workspace_proxies" ) +func IsExternalProxies(opt clibase.Option) bool { + // If it is a bool, use the bool value. + b, _ := strconv.ParseBool(opt.Annotations[flagExternalProxies]) + return b +} + func IsSecretDeploymentOption(opt clibase.Option) bool { return opt.Annotations.IsSet(flagSecretKey) } @@ -470,6 +477,7 @@ when required by your organization's security policy.`, Value: &c.HTTPAddress, Group: &deploymentGroupNetworkingHTTP, YAML: "httpAddress", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), } tlsBindAddress := clibase.Option{ Name: "TLS Address", @@ -480,6 +488,7 @@ when required by your organization's security policy.`, Value: &c.TLS.Address, Group: &deploymentGroupNetworkingTLS, YAML: "address", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), } redirectToAccessURL := clibase.Option{ Name: "Redirect to Access URL", @@ -499,6 +508,7 @@ when required by your organization's security policy.`, Env: "CODER_ACCESS_URL", Group: &deploymentGroupNetworking, YAML: "accessURL", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Wildcard Access URL", @@ -508,6 +518,7 @@ when required by your organization's security policy.`, Value: &c.WildcardAccessURL, Group: &deploymentGroupNetworking, YAML: "wildcardAccessURL", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, redirectToAccessURL, { @@ -534,7 +545,8 @@ when required by your organization's security policy.`, httpAddress, tlsBindAddress, }, - Group: &deploymentGroupNetworking, + Group: &deploymentGroupNetworking, + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // TLS settings { @@ -545,6 +557,7 @@ when required by your organization's security policy.`, Value: &c.TLS.Enable, Group: &deploymentGroupNetworkingTLS, YAML: "enable", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Redirect HTTP to HTTPS", @@ -557,6 +570,7 @@ when required by your organization's security policy.`, UseInstead: clibase.OptionSet{redirectToAccessURL}, Group: &deploymentGroupNetworkingTLS, YAML: "redirectHTTP", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Certificate Files", @@ -566,6 +580,7 @@ when required by your organization's security policy.`, Value: &c.TLS.CertFiles, Group: &deploymentGroupNetworkingTLS, YAML: "certFiles", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Client CA Files", @@ -575,6 +590,7 @@ when required by your organization's security policy.`, Value: &c.TLS.ClientCAFile, Group: &deploymentGroupNetworkingTLS, YAML: "clientCAFile", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Client Auth", @@ -585,6 +601,7 @@ when required by your organization's security policy.`, Value: &c.TLS.ClientAuth, Group: &deploymentGroupNetworkingTLS, YAML: "clientAuth", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Key Files", @@ -594,6 +611,7 @@ when required by your organization's security policy.`, Value: &c.TLS.KeyFiles, Group: &deploymentGroupNetworkingTLS, YAML: "keyFiles", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Minimum Version", @@ -604,6 +622,7 @@ when required by your organization's security policy.`, Value: &c.TLS.MinVersion, Group: &deploymentGroupNetworkingTLS, YAML: "minVersion", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Client Cert File", @@ -613,6 +632,7 @@ when required by your organization's security policy.`, Value: &c.TLS.ClientCertFile, Group: &deploymentGroupNetworkingTLS, YAML: "clientCertFile", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "TLS Client Key File", @@ -622,6 +642,7 @@ when required by your organization's security policy.`, Value: &c.TLS.ClientKeyFile, Group: &deploymentGroupNetworkingTLS, YAML: "clientKeyFile", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // Derp settings { @@ -712,6 +733,7 @@ when required by your organization's security policy.`, Value: &c.Prometheus.Enable, Group: &deploymentGroupIntrospectionPrometheus, YAML: "enable", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Prometheus Address", @@ -722,6 +744,7 @@ when required by your organization's security policy.`, Value: &c.Prometheus.Address, Group: &deploymentGroupIntrospectionPrometheus, YAML: "address", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Prometheus Collect Agent Stats", @@ -741,6 +764,7 @@ when required by your organization's security policy.`, Value: &c.Pprof.Enable, Group: &deploymentGroupIntrospectionPPROF, YAML: "enable", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "pprof Address", @@ -751,6 +775,7 @@ when required by your organization's security policy.`, Value: &c.Pprof.Address, Group: &deploymentGroupIntrospectionPPROF, YAML: "address", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // oAuth settings { @@ -1007,13 +1032,14 @@ when required by your organization's security policy.`, Value: &c.Trace.Enable, Group: &deploymentGroupIntrospectionTracing, YAML: "enable", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Trace Honeycomb API Key", Description: "Enables trace exporting to Honeycomb.io using the provided API Key.", Flag: "trace-honeycomb-api-key", Env: "CODER_TRACE_HONEYCOMB_API_KEY", - Annotations: clibase.Annotations{}.Mark(flagSecretKey, "true"), + Annotations: clibase.Annotations{}.Mark(flagSecretKey, "true").Mark(flagExternalProxies, "true"), Value: &c.Trace.HoneycombAPIKey, Group: &deploymentGroupIntrospectionTracing, }, @@ -1025,6 +1051,7 @@ when required by your organization's security policy.`, Value: &c.Trace.CaptureLogs, Group: &deploymentGroupIntrospectionTracing, YAML: "captureLogs", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // Provisioner settings { @@ -1074,19 +1101,21 @@ when required by your organization's security policy.`, Flag: "dangerous-disable-rate-limits", Env: "CODER_DANGEROUS_DISABLE_RATE_LIMITS", - Value: &c.RateLimit.DisableAll, - Hidden: true, + Value: &c.RateLimit.DisableAll, + Hidden: true, + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "API Rate Limit", Description: "Maximum number of requests per minute allowed to the API per user, or per IP address for unauthenticated users. Negative values mean no rate limit. Some API endpoints have separate strict rate limits regardless of this value to prevent denial-of-service or brute force attacks.", // Change the env from the auto-generated CODER_RATE_LIMIT_API to the // old value to avoid breaking existing deployments. - Env: "CODER_API_RATE_LIMIT", - Flag: "api-rate-limit", - Default: "512", - Value: &c.RateLimit.API, - Hidden: true, + Env: "CODER_API_RATE_LIMIT", + Flag: "api-rate-limit", + Default: "512", + Value: &c.RateLimit.API, + Hidden: true, + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // Logging settings { @@ -1096,9 +1125,10 @@ when required by your organization's security policy.`, Env: "CODER_VERBOSE", FlagShorthand: "v", - Value: &c.Verbose, - Group: &deploymentGroupIntrospectionLogging, - YAML: "verbose", + Value: &c.Verbose, + Group: &deploymentGroupIntrospectionLogging, + YAML: "verbose", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Human Log Location", @@ -1109,6 +1139,7 @@ when required by your organization's security policy.`, Value: &c.Logging.Human, Group: &deploymentGroupIntrospectionLogging, YAML: "humanPath", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "JSON Log Location", @@ -1119,6 +1150,7 @@ when required by your organization's security policy.`, Value: &c.Logging.JSON, Group: &deploymentGroupIntrospectionLogging, YAML: "jsonPath", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Stackdriver Log Location", @@ -1129,6 +1161,7 @@ when required by your organization's security policy.`, Value: &c.Logging.Stackdriver, Group: &deploymentGroupIntrospectionLogging, YAML: "stackdriverPath", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, // ☢️ Dangerous settings { @@ -1157,6 +1190,7 @@ when required by your organization's security policy.`, Env: "CODER_EXPERIMENTS", Value: &c.Experiments, YAML: "experiments", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Update Check", @@ -1199,6 +1233,7 @@ when required by your organization's security policy.`, Value: &c.ProxyTrustedHeaders, Group: &deploymentGroupNetworking, YAML: "proxyTrustedHeaders", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Proxy Trusted Origins", @@ -1208,6 +1243,7 @@ when required by your organization's security policy.`, Value: &c.ProxyTrustedOrigins, Group: &deploymentGroupNetworking, YAML: "proxyTrustedOrigins", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Cache Directory", @@ -1243,28 +1279,31 @@ when required by your organization's security policy.`, Value: &c.SecureAuthCookie, Group: &deploymentGroupNetworking, YAML: "secureAuthCookie", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Strict-Transport-Security", Description: "Controls if the 'Strict-Transport-Security' header is set on all static file responses. " + "This header should only be set if the server is accessed via HTTPS. This value is the MaxAge in seconds of " + "the header.", - Default: "0", - Flag: "strict-transport-security", - Env: "CODER_STRICT_TRANSPORT_SECURITY", - Value: &c.StrictTransportSecurity, - Group: &deploymentGroupNetworkingTLS, - YAML: "strictTransportSecurity", + Default: "0", + Flag: "strict-transport-security", + Env: "CODER_STRICT_TRANSPORT_SECURITY", + Value: &c.StrictTransportSecurity, + Group: &deploymentGroupNetworkingTLS, + YAML: "strictTransportSecurity", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Strict-Transport-Security Options", Description: "Two optional fields can be set in the Strict-Transport-Security header; 'includeSubDomains' and 'preload'. " + "The 'strict-transport-security' flag must be set to a non-zero value for these options to be used.", - Flag: "strict-transport-security-options", - Env: "CODER_STRICT_TRANSPORT_SECURITY_OPTIONS", - Value: &c.StrictTransportSecurityOptions, - Group: &deploymentGroupNetworkingTLS, - YAML: "strictTransportSecurityOptions", + Flag: "strict-transport-security-options", + Env: "CODER_STRICT_TRANSPORT_SECURITY_OPTIONS", + Value: &c.StrictTransportSecurityOptions, + Group: &deploymentGroupNetworkingTLS, + YAML: "strictTransportSecurityOptions", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "SSH Keygen Algorithm", @@ -1308,7 +1347,7 @@ when required by your organization's security policy.`, Description: "Whether Coder only allows connections to workspaces via the browser.", Flag: "browser-only", Env: "CODER_BROWSER_ONLY", - Annotations: clibase.Annotations{}.Mark(flagEnterpriseKey, "true"), + Annotations: clibase.Annotations{}.Mark(flagEnterpriseKey, "true").Mark(flagExternalProxies, "true"), Value: &c.BrowserOnly, Group: &deploymentGroupNetworking, YAML: "browserOnly", @@ -1328,8 +1367,9 @@ when required by your organization's security policy.`, Flag: "disable-path-apps", Env: "CODER_DISABLE_PATH_APPS", - Value: &c.DisablePathApps, - YAML: "disablePathApps", + Value: &c.DisablePathApps, + YAML: "disablePathApps", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Disable Owner Workspace Access", @@ -1337,8 +1377,9 @@ when required by your organization's security policy.`, Flag: "disable-owner-workspace-access", Env: "CODER_DISABLE_OWNER_WORKSPACE_ACCESS", - Value: &c.DisableOwnerWorkspaceExec, - YAML: "disableOwnerWorkspaceAccess", + Value: &c.DisableOwnerWorkspaceExec, + YAML: "disableOwnerWorkspaceAccess", + Annotations: clibase.Annotations{}.Mark(flagExternalProxies, "true"), }, { Name: "Session Duration", diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 0c0f68ab115d4..e101900ab7d7c 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -98,11 +98,31 @@ func (c *closers) Add(f func()) { func (r *RootCmd) proxyServer() *clibase.Cmd { var ( - // TODO: Remove options that we do not need - cfg = new(codersdk.DeploymentValues) - opts = cfg.Options() + cfg = new(codersdk.DeploymentValues) + // Filter options for only relevant ones. + opts = cfg.Options().Filter(codersdk.IsExternalProxies) + + externalProxyOptionGroup = clibase.Group{ + Name: "External Workspace Proxy", + YAML: "externalWorkspaceProxy", + } + proxySessionToken clibase.String + ) + opts.Add( + // Options only for external workspace proxies + + clibase.Option{ + Name: "Proxy Session Token", + Description: "Authentication token for the workspace proxy to communicate with coderd.", + Flag: "proxy-session-token", + Env: "CODER_PROXY_SESSION_TOKEN", + YAML: "proxySessionToken", + Default: "", + Value: &proxySessionToken, + Group: &externalProxyOptionGroup, + Hidden: false, + }, ) - var _ = opts client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -250,10 +270,8 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { PrometheusRegistry: prometheusRegistry, APIRateLimit: int(cfg.RateLimit.API.Value()), SecureAuthCookie: cfg.SecureAuthCookie.Value(), - // TODO: DisablePathApps - DisablePathApps: false, - // TODO: ProxySessionToken - ProxySessionToken: "", + DisablePathApps: cfg.DisablePathApps.Value(), + ProxySessionToken: proxySessionToken.Value(), }) if err != nil { return xerrors.Errorf("create workspace proxy: %w", err) From 9fc8078e5ddc6952ff36c7f33c4eb78efc9a208e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 11:38:04 -0500 Subject: [PATCH 03/22] Add primary access url --- enterprise/cli/workspaceproxy.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index e101900ab7d7c..7c5fd2a272ec2 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -10,7 +10,6 @@ import ( "net" "net/http" "net/http/pprof" - "net/url" "os/signal" "regexp" rpprof "runtime/pprof" @@ -107,6 +106,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { YAML: "externalWorkspaceProxy", } proxySessionToken clibase.String + primaryAccessURL clibase.URL ) opts.Add( // Options only for external workspace proxies @@ -122,6 +122,18 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { Group: &externalProxyOptionGroup, Hidden: false, }, + + clibase.Option{ + Name: "Coderd (Primary) Access URL", + Description: "URL to communicate with coderd. This should match the access URL of the Coder deployment.", + Flag: "primary-access-url", + Env: "CODER_PRIMARY_ACCESS_URL", + YAML: "primaryAccessURL", + Default: "", + Value: &primaryAccessURL, + Group: &externalProxyOptionGroup, + Hidden: false, + }, ) client := new(codersdk.Client) @@ -137,6 +149,10 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { + if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") { + return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL) + } + var closers closers // Main command context for managing cancellation of running // services. @@ -255,11 +271,10 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { closers.Add(closeFunc) } - pu, _ := url.Parse("http://localhost:3000") proxy, err := wsproxy.New(&wsproxy.Options{ Logger: logger, // TODO: PrimaryAccessURL - PrimaryAccessURL: pu, + PrimaryAccessURL: primaryAccessURL.Value(), AccessURL: cfg.AccessURL.Value(), AppHostname: appHostname, AppHostnameRegex: appHostnameRegex, From 56d0ad7020b175ec7e9a1e1c4a55d4f484b59e75 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 11:40:38 -0500 Subject: [PATCH 04/22] Include app security key --- enterprise/cli/workspaceproxy.go | 36 ++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 7c5fd2a272ec2..db774506dba8d 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -107,6 +107,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { } proxySessionToken clibase.String primaryAccessURL clibase.URL + appSecuritYKey clibase.String ) opts.Add( // Options only for external workspace proxies @@ -134,6 +135,20 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { Group: &externalProxyOptionGroup, Hidden: false, }, + + // TODO: Make sure this is kept secret. Idk if a flag is the best option + clibase.Option{ + Name: "App Security Key", + Description: "App security key used for decrypting/verifying app tokens sent from coderd.", + Flag: "app-security-key", + Env: "CODER_APP_SECURITY_KEY", + YAML: "appSecurityKey", + Default: "", + Value: &appSecuritYKey, + Group: &externalProxyOptionGroup, + Hidden: false, + Annotations: clibase.Annotations{}.Mark("secret", "true"), + }, ) client := new(codersdk.Client) @@ -153,6 +168,11 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL) } + secKey, err := workspaceapps.KeyFromString(appSecuritYKey.Value()) + if err != nil { + return xerrors.Errorf("app security key: %w", err) + } + var closers closers // Main command context for managing cancellation of running // services. @@ -272,15 +292,13 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { } proxy, err := wsproxy.New(&wsproxy.Options{ - Logger: logger, - // TODO: PrimaryAccessURL - PrimaryAccessURL: primaryAccessURL.Value(), - AccessURL: cfg.AccessURL.Value(), - AppHostname: appHostname, - AppHostnameRegex: appHostnameRegex, - RealIPConfig: realIPConfig, - // TODO: AppSecurityKey - AppSecurityKey: workspaceapps.SecurityKey{}, + Logger: logger, + PrimaryAccessURL: primaryAccessURL.Value(), + AccessURL: cfg.AccessURL.Value(), + AppHostname: appHostname, + AppHostnameRegex: appHostnameRegex, + RealIPConfig: realIPConfig, + AppSecurityKey: secKey, Tracing: tracer, PrometheusRegistry: prometheusRegistry, APIRateLimit: int(cfg.RateLimit.API.Value()), From 4d7daedb58521458132a72fa50d5d7e2019b3800 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 14:16:24 -0500 Subject: [PATCH 05/22] Add output formats for registering a proxy --- codersdk/workspaceproxy.go | 18 +++--- enterprise/cli/workspaceproxy.go | 94 ++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/codersdk/workspaceproxy.go b/codersdk/workspaceproxy.go index 675eecd65217b..2605129aea9ba 100644 --- a/codersdk/workspaceproxy.go +++ b/codersdk/workspaceproxy.go @@ -12,16 +12,16 @@ import ( ) type WorkspaceProxy struct { - ID uuid.UUID `db:"id" json:"id" format:"uuid"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` + ID uuid.UUID `db:"id" json:"id" format:"uuid" table:"id"` + Name string `db:"name" json:"name" table:"name,default_sort"` + Icon string `db:"icon" json:"icon" table:"icon"` // Full url including scheme of the proxy api url: https://us.example.com - URL string `db:"url" json:"url"` + URL string `db:"url" json:"url" table:"url"` // WildcardHostname with the wildcard for subdomain based app hosting: *.us.example.com - WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"` - CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time"` - Deleted bool `db:"deleted" json:"deleted"` + WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname" table:"wildcard_hostname"` + CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time" table:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time" table:"updated_at"` + Deleted bool `db:"deleted" json:"deleted" table:"deleted"` } type CreateWorkspaceProxyRequest struct { @@ -33,7 +33,7 @@ type CreateWorkspaceProxyRequest struct { } type CreateWorkspaceProxyResponse struct { - Proxy WorkspaceProxy `json:"proxy"` + Proxy WorkspaceProxy `json:"proxy" table:"proxy,recursive,default_sort"` ProxyToken string `json:"proxy_token"` } diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index db774506dba8d..9d986c8412b22 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -53,33 +53,103 @@ func (r *RootCmd) workspaceProxy() *clibase.Cmd { } func (r *RootCmd) registerProxy() *clibase.Cmd { + var ( + proxyName string + displayName string + proxyIcon string + proxyURL string + proxyWildcardHostname string + onlyToken bool + formatter = cliui.NewOutputFormatter( + // Text formatter should be human readable. + cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) { + response, ok := data.(codersdk.CreateWorkspaceProxyResponse) + if !ok { + return nil, xerrors.Errorf("unexpected type %T", data) + } + return fmt.Sprintf("Workspace Proxy %q registered successfully\nToken: %s", response.Proxy.Name, response.ProxyToken), nil + }), + cliui.JSONFormat(), + cliui.ChangeFormatterData(cliui.TableFormat([]codersdk.CreateWorkspaceProxyResponse{}, []string{"proxy name", "proxy url", "proxy token"}), + func(data any) (any, error) { + response, ok := data.(codersdk.CreateWorkspaceProxyResponse) + if !ok { + return nil, xerrors.Errorf("unexpected type %T", data) + } + return []codersdk.CreateWorkspaceProxyResponse{response}, nil + }), + ) + ) + client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "register", Short: "Register a workspace proxy", Middleware: clibase.Chain( - clibase.RequireNArgs(1), + clibase.RequireNArgs(0), r.InitClient(client), ), - Handler: func(i *clibase.Invocation) error { - ctx := i.Context() - name := i.Args[0] - // TODO: Fix all this + Handler: func(inv *clibase.Invocation) error { + ctx := inv.Context() resp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{ - Name: name, - DisplayName: name, - Icon: "whocares.png", - URL: "http://localhost:6005", - WildcardHostname: "", + Name: proxyName, + DisplayName: displayName, + Icon: proxyIcon, + URL: proxyURL, + WildcardHostname: proxyWildcardHostname, }) if err != nil { return xerrors.Errorf("create workspace proxy: %w", err) } - fmt.Println(resp.ProxyToken) - return nil + var output string + if onlyToken { + output = resp.ProxyToken + } else { + output, err = formatter.Format(ctx, resp) + if err != nil { + return err + } + } + + _, err = fmt.Fprintln(inv.Stdout, output) + return err }, } + + formatter.AttachOptions(&cmd.Options) + cmd.Options.Add( + clibase.Option{ + Flag: "name", + Description: "Name of the proxy. This is used to identify the proxy.", + Value: clibase.StringOf(&proxyName), + }, + clibase.Option{ + Flag: "display-name", + Description: "Display of the proxy. If omitted, the name is reused as the display name.", + Value: clibase.StringOf(&displayName), + }, + clibase.Option{ + Flag: "icon", + Description: "Display icon of the proxy.", + Value: clibase.StringOf(&proxyIcon), + }, + clibase.Option{ + Flag: "access-url", + Description: "Access URL of the proxy.", + Value: clibase.StringOf(&proxyURL), + }, + clibase.Option{ + Flag: "wildcard-access-url", + Description: "(Optional) Access url of the proxy for subdomain based apps.", + Value: clibase.StringOf(&proxyWildcardHostname), + }, + clibase.Option{ + Flag: "only-token", + Description: "Only print the token. This is useful for scripting.", + Value: clibase.BoolOf(&onlyToken), + }, + ) return cmd } From 62f676b0416d4003c25d27ee33500605f2b7ec8a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 14:19:21 -0500 Subject: [PATCH 06/22] Fix formats --- cli/cliui/output.go | 27 +++++++++++++++++++++++++++ codersdk/workspaceproxy.go | 5 +++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cli/cliui/output.go b/cli/cliui/output.go index c9ed34677971c..b090d6795cdf5 100644 --- a/cli/cliui/output.go +++ b/cli/cliui/output.go @@ -192,3 +192,30 @@ func (textFormat) AttachOptions(_ *clibase.OptionSet) {} func (textFormat) Format(_ context.Context, data any) (string, error) { return fmt.Sprintf("%s", data), nil } + +type DataChangeFormat struct { + format OutputFormat + change func(data any) (any, error) +} + +// ChangeFormatterData allows manipulating the data passed to an output +// format. +func ChangeFormatterData(format OutputFormat, change func(data any) (any, error)) *DataChangeFormat { + return &DataChangeFormat{format: format, change: change} +} + +func (d *DataChangeFormat) ID() string { + return d.format.ID() +} + +func (d *DataChangeFormat) AttachOptions(opts *clibase.OptionSet) { + d.format.AttachOptions(opts) +} + +func (d *DataChangeFormat) Format(ctx context.Context, data any) (string, error) { + newData, err := d.change(data) + if err != nil { + return "", err + } + return d.format.Format(ctx, newData) +} diff --git a/codersdk/workspaceproxy.go b/codersdk/workspaceproxy.go index 2605129aea9ba..22fe0ecdab560 100644 --- a/codersdk/workspaceproxy.go +++ b/codersdk/workspaceproxy.go @@ -33,8 +33,9 @@ type CreateWorkspaceProxyRequest struct { } type CreateWorkspaceProxyResponse struct { - Proxy WorkspaceProxy `json:"proxy" table:"proxy,recursive,default_sort"` - ProxyToken string `json:"proxy_token"` + Proxy WorkspaceProxy `json:"proxy" table:"proxy,recursive"` + // The recursive table sort is not working very well. + ProxyToken string `json:"proxy_token" table:"proxy token,default_sort"` } func (c *Client) CreateWorkspaceProxy(ctx context.Context, req CreateWorkspaceProxyRequest) (CreateWorkspaceProxyResponse, error) { From 913aef1402d815d5307791ba88b452021d14eda1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 14:26:04 -0500 Subject: [PATCH 07/22] Add cli cmd to get app security key --- cli/appsecuritykey.go | 59 +++++++++++++++++++++++++++++++++++++++++++ cli/root.go | 1 + 2 files changed, 60 insertions(+) create mode 100644 cli/appsecuritykey.go diff --git a/cli/appsecuritykey.go b/cli/appsecuritykey.go new file mode 100644 index 0000000000000..ed48733d49857 --- /dev/null +++ b/cli/appsecuritykey.go @@ -0,0 +1,59 @@ +package cli + +import ( + "database/sql" + "fmt" + + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/migrations" + "golang.org/x/xerrors" +) + +func (*RootCmd) appSecurityKey() *clibase.Cmd { + var postgresURL string + + root := &clibase.Cmd{ + Use: "app-security-key", + Short: "Directly connect to the database to print the app security key", + // We can unhide this if we decide to keep it this way. + Hidden: true, + Middleware: clibase.RequireNArgs(0), + Handler: func(inv *clibase.Invocation) error { + sqlDB, err := sql.Open("postgres", postgresURL) + if err != nil { + return xerrors.Errorf("dial postgres: %w", err) + } + defer sqlDB.Close() + err = sqlDB.Ping() + if err != nil { + return xerrors.Errorf("ping postgres: %w", err) + } + + err = migrations.EnsureClean(sqlDB) + if err != nil { + return xerrors.Errorf("database needs migration: %w", err) + } + db := database.New(sqlDB) + + key, err := db.GetAppSecurityKey(inv.Context()) + if err != nil { + return xerrors.Errorf("retrieving key: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stdout, key) + return nil + }, + } + + root.Options = clibase.OptionSet{ + { + Flag: "postgres-url", + Description: "URL of a PostgreSQL database to connect to.", + Env: "CODER_PG_CONNECTION_URL", + Value: clibase.StringOf(&postgresURL), + }, + } + + return root +} diff --git a/cli/root.go b/cli/root.go index a71626ccdf3ef..2d47908f67ecd 100644 --- a/cli/root.go +++ b/cli/root.go @@ -79,6 +79,7 @@ func (r *RootCmd) Core() []*clibase.Cmd { r.portForward(), r.publickey(), r.resetPassword(), + r.appSecurityKey(), r.state(), r.templates(), r.users(), From 9c37e16bca494757d9387fb07e019813a437730e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Apr 2023 15:06:00 -0500 Subject: [PATCH 08/22] Add deleting proxies --- coderd/database/dbauthz/querier.go | 4 ++ coderd/database/dbfake/databasefake.go | 12 ++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 29 ++++++++++++++ coderd/database/queries/proxies.sql | 10 +++++ coderd/httpmw/workspaceproxy.go | 52 ++++++++++++++++++++++++++ codersdk/workspaceproxy.go | 22 +++++++++++ enterprise/cli/workspaceproxy.go | 25 +++++++++++++ enterprise/coderd/coderd.go | 14 +++---- enterprise/coderd/workspaceproxy.go | 45 ++++++++++++++++++++++ scripts/develop.sh | 18 ++++++++- 11 files changed, 224 insertions(+), 8 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 2b9810405e5a9..d3a86da0c02dc 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1697,6 +1697,10 @@ func (q *querier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (data return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByID)(ctx, id) } +func (q *querier) GetWorkspaceProxyByName(ctx context.Context, name string) (database.WorkspaceProxy, error) { + return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByName)(ctx, name) +} + func (q *querier) GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (database.WorkspaceProxy, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByHostname)(ctx, hostname) } diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 9d37f195dd01b..f7995ad10ab64 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -5097,6 +5097,18 @@ func (q *fakeQuerier) GetWorkspaceProxyByID(_ context.Context, id uuid.UUID) (da return database.WorkspaceProxy{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceProxyByName(_ context.Context, name string) (database.WorkspaceProxy, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, proxy := range q.workspaceProxies { + if proxy.Name == name { + return proxy, nil + } + } + return database.WorkspaceProxy{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, hostname string) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7feb2e8b78b88..47e574ef52e04 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -158,6 +158,7 @@ type sqlcQuerier interface { // GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (WorkspaceProxy, error) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) + GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5dd8577d3c18a..fdf2fe2020fea 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2937,6 +2937,35 @@ func (q *sqlQuerier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (W return i, err } +const getWorkspaceProxyByName = `-- name: GetWorkspaceProxyByName :one +SELECT + id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret +FROM + workspace_proxies +WHERE + name = $1 +LIMIT + 1 +` + +func (q *sqlQuerier) GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceProxyByName, name) + var i WorkspaceProxy + err := row.Scan( + &i.ID, + &i.Name, + &i.DisplayName, + &i.Icon, + &i.Url, + &i.WildcardHostname, + &i.CreatedAt, + &i.UpdatedAt, + &i.Deleted, + &i.TokenHashedSecret, + ) + return i, err +} + const insertWorkspaceProxy = `-- name: InsertWorkspaceProxy :one INSERT INTO workspace_proxies ( diff --git a/coderd/database/queries/proxies.sql b/coderd/database/queries/proxies.sql index 807105238bc93..d384b0daebb71 100644 --- a/coderd/database/queries/proxies.sql +++ b/coderd/database/queries/proxies.sql @@ -49,6 +49,16 @@ WHERE LIMIT 1; +-- name: GetWorkspaceProxyByName :one +SELECT + * +FROM + workspace_proxies +WHERE + name = $1 +LIMIT + 1; + -- Finds a workspace proxy that has an access URL or app hostname that matches -- the provided hostname. This is to check if a hostname matches any workspace -- proxy. diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go index 28961ea19c08b..540ccc2a24b21 100644 --- a/coderd/httpmw/workspaceproxy.go +++ b/coderd/httpmw/workspaceproxy.go @@ -8,6 +8,8 @@ import ( "net/http" "strings" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" "golang.org/x/xerrors" @@ -156,3 +158,53 @@ func ExtractWorkspaceProxy(opts ExtractWorkspaceProxyConfig) func(http.Handler) }) } } + +type workspaceProxyParamContextKey struct{} + +// WorkspaceProxy returns the worksace proxy from the ExtractWorkspaceProxyParam handler. +func WorkspaceProxy(r *http.Request) database.WorkspaceProxy { + user, ok := r.Context().Value(workspaceProxyParamContextKey{}).(database.WorkspaceProxy) + if !ok { + panic("developer error: workspace proxy parameter middleware not provided") + } + return user +} + +// ExtractWorkspaceProxyParam extracts a workspace proxy from an ID/name in the {workspaceproxy} URL +// parameter. +// +//nolint:revive +func ExtractWorkspaceProxyParam(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + proxyQuery := chi.URLParam(r, "workspaceproxy") + if proxyQuery == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "\"workspaceproxy\" must be provided.", + }) + return + } + + var proxy database.WorkspaceProxy + var dbErr error + if proxyID, err := uuid.Parse(proxyQuery); err == nil { + proxy, dbErr = db.GetWorkspaceProxyByID(ctx, proxyID) + } else { + proxy, dbErr = db.GetWorkspaceProxyByName(ctx, proxyQuery) + } + if httpapi.Is404Error(dbErr) { + httpapi.ResourceNotFound(rw) + return + } + if dbErr != nil { + httpapi.InternalServerError(rw, dbErr) + return + } + + ctx = context.WithValue(ctx, workspaceProxyParamContextKey{}, proxy) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/codersdk/workspaceproxy.go b/codersdk/workspaceproxy.go index 22fe0ecdab560..9b3521bcb4098 100644 --- a/codersdk/workspaceproxy.go +++ b/codersdk/workspaceproxy.go @@ -3,6 +3,7 @@ package codersdk import ( "context" "encoding/json" + "fmt" "net/http" "time" @@ -72,3 +73,24 @@ func (c *Client) WorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) var proxies []WorkspaceProxy return proxies, json.NewDecoder(res.Body).Decode(&proxies) } + +func (c *Client) DeleteWorkspaceProxyByName(ctx context.Context, name string) error { + res, err := c.Request(ctx, http.MethodDelete, + fmt.Sprintf("/api/v2/workspaceproxies/%s", name), + nil, + ) + if err != nil { + return xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + + return nil +} + +func (c *Client) DeleteWorkspaceProxyByID(ctx context.Context, id uuid.UUID) error { + return c.DeleteWorkspaceProxyByName(ctx, id.String()) +} diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 9d986c8412b22..0229c430c4e2d 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -46,6 +46,31 @@ func (r *RootCmd) workspaceProxy() *clibase.Cmd { Children: []*clibase.Cmd{ r.proxyServer(), r.registerProxy(), + r.deleteProxy(), + }, + } + + return cmd +} + +func (r *RootCmd) deleteProxy() *clibase.Cmd { + client := new(codersdk.Client) + cmd := &clibase.Cmd{ + Use: "delete", + Short: "Delete a workspace proxy", + Middleware: clibase.Chain( + clibase.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *clibase.Invocation) error { + ctx := inv.Context() + err := client.DeleteWorkspaceProxyByName(ctx, inv.Args[0]) + if err != nil { + return xerrors.Errorf("delete workspace proxy %q: %w", inv.Args[0], err) + } + + _, _ = fmt.Fprintln(inv.Stdout, "Workspace proxy %q deleted successfully", inv.Args[0]) + return nil }, } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 0a79176ba7cda..5baef82e19b8c 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -102,13 +102,13 @@ func New(ctx context.Context, options *Options) (*API, error) { r.Post("/issue-signed-app-token", api.workspaceProxyIssueSignedAppToken) }) // TODO: Add specific workspace proxy endpoints. - // r.Route("/{proxyName}", func(r chi.Router) { - // r.Use( - // httpmw.ExtractWorkspaceProxyByNameParam(api.Database), - // ) - // - // r.Get("/", api.workspaceProxyByName) - // }) + r.Route("/{workspaceproxy}", func(r chi.Router) { + r.Use( + httpmw.ExtractWorkspaceProxyParam(api.Database), + ) + + r.Delete("/", api.deleteWorkspaceProxy) + }) }) r.Route("/organizations/{organization}/groups", func(r chi.Router) { r.Use( diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 65499d3167f69..3642aeb177f2d 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -7,6 +7,8 @@ import ( "net/http" "net/url" + "github.com/coder/coder/coderd/httpmw" + "github.com/google/uuid" "golang.org/x/xerrors" @@ -19,6 +21,49 @@ import ( "github.com/coder/coder/enterprise/wsproxy/wsproxysdk" ) +// @Summary Delete workspace proxy +// @ID delete-workspace-proxy +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Enterprise +// @Param workspaceproxy path string true "Proxy ID or name" format(uuid) +// @Success 200 {object} codersdk.Response +// @Router /workspaceproxies/{workspaceproxy} [delete] +func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + proxy = httpmw.WorkspaceProxy(r) + auditor = api.AGPL.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + }) + ) + aReq.Old = proxy + defer commitAudit() + + err := api.Database.UpdateWorkspaceProxyDeleted(ctx, database.UpdateWorkspaceProxyDeletedParams{ + ID: proxy.ID, + Deleted: true, + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = database.WorkspaceProxy{} + httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ + Message: "Proxy has been deleted!", + }) +} + // @Summary Create workspace proxy // @ID create-workspace-proxy // @Security CoderSessionToken diff --git a/scripts/develop.sh b/scripts/develop.sh index cfeed0b76fbe7..45c7f0c53a718 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -15,8 +15,9 @@ set -euo pipefail DEFAULT_PASSWORD="SomeSecurePassword!" password="${CODER_DEV_ADMIN_PASSWORD:-${DEFAULT_PASSWORD}}" +use_proxy=0 -args="$(getopt -o "" -l agpl,password: -- "$@")" +args="$(getopt -o "" -l use-proxy,agpl,password: -- "$@")" eval set -- "$args" while true; do case "$1" in @@ -28,6 +29,10 @@ while true; do password="$2" shift 2 ;; + --use-proxy) + use_proxy=1 + shift + ;; --) shift break @@ -38,6 +43,10 @@ while true; do esac done +if [ "${CODER_BUILD_AGPL:-0}" -gt "0" ] && [ "${use_proxy}" -gt "0" ]; then + echo '== ERROR: cannot use both external proxies and APGL build.' && exit 1 +fi + # Preflight checks: ensure we have our required dependencies, and make sure nothing is listening on port 3000 or 8080 dependencies curl git go make yarn curl --fail http://127.0.0.1:3000 >/dev/null 2>&1 && echo '== ERROR: something is listening on port 3000. Kill it and re-run this script.' && exit 1 @@ -168,6 +177,13 @@ fatal() { ) || echo "Failed to create a template. The template files are in ${temp_template_dir}" fi + if [ "${use_proxy}" -gt "0" ]; then + # Create the proxy + "${CODER_DEV_SHIM}" proxy register --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --access-url=http://localhost:3010 --only-token + # Start the proxy + start_cmd PROXY proxy "" "${CODER_DEV_SHIM}" proxy --listen-addr + fi + # Start the frontend once we have a template up and running CODER_HOST=http://127.0.0.1:3000 start_cmd SITE date yarn --cwd=./site dev --host From 9d4591c8e94ec6d90561a3d00a69225dcf2a511d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 09:21:52 -0500 Subject: [PATCH 09/22] feat: Allow workspace proxy spawn in develop.sh --- cli/server.go | 24 ++++++++++++++++++++++++ enterprise/cli/workspaceproxy.go | 2 +- scripts/develop.sh | 22 +++++++++++++++++----- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/cli/server.go b/cli/server.go index a3b19b88e5788..02ec852d95681 100644 --- a/cli/server.go +++ b/cli/server.go @@ -163,6 +163,19 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. var ( cfg = new(codersdk.DeploymentValues) opts = cfg.Options() + // For the develop.sh script, it is helpful to make this key deterministic. + devAppSecurityKey string + ) + opts.Add( + clibase.Option{ + Name: "App Security Key (Development Only)", + Description: "Used to override the app security key stored in the database. This should never be used in production.", + Flag: "dangerous-dev-app-security-key", + Default: "", + Value: clibase.StringOf(&devAppSecurityKey), + Annotations: clibase.Annotations{}.Mark("secret", "true"), + Hidden: true, + }, ) serverCmd := &clibase.Cmd{ Use: "server", @@ -621,6 +634,17 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } + if devAppSecurityKey != "" { + _, err := workspaceapps.KeyFromString(devAppSecurityKey) + if err != nil { + return xerrors.Errorf("invalid dev app security key: %w", err) + } + err = tx.UpsertAppSecurityKey(ctx, devAppSecurityKey) + if err != nil { + return xerrors.Errorf("Insert dev app security key: %w", err) + } + } + // Read the app signing key from the DB. We store it hex encoded // since the config table uses strings for the value and we // don't want to deal with automatic encoding issues. diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 0229c430c4e2d..90e9ed53d9cbc 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -69,7 +69,7 @@ func (r *RootCmd) deleteProxy() *clibase.Cmd { return xerrors.Errorf("delete workspace proxy %q: %w", inv.Args[0], err) } - _, _ = fmt.Fprintln(inv.Stdout, "Workspace proxy %q deleted successfully", inv.Args[0]) + _, _ = fmt.Fprintf(inv.Stdout, "Workspace proxy %q deleted successfully\n", inv.Args[0]) return nil }, } diff --git a/scripts/develop.sh b/scripts/develop.sh index 45c7f0c53a718..ed9f4c091db54 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -16,6 +16,8 @@ set -euo pipefail DEFAULT_PASSWORD="SomeSecurePassword!" password="${CODER_DEV_ADMIN_PASSWORD:-${DEFAULT_PASSWORD}}" use_proxy=0 +# Hard coded app security key for the proxy to also use. +app_security_key=bea458bcfb31990fb70f14a7003c0aad4f371f19653f3a632bc9d3492004cd95e0e542c30e5f158f26728373190bfc802b1c1549f52c149af8ad7f5b12ea1ee0995b95de3b86ae78f021c17437649224cd2dcf1b298d180811cf36f4dcb8f33e args="$(getopt -o "" -l use-proxy,agpl,password: -- "$@")" eval set -- "$args" @@ -131,7 +133,7 @@ fatal() { trap 'fatal "Script encountered an error"' ERR cdroot - start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --experiments "*" "$@" + start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-dev-app-security-key ${app_security_key} --experiments "*" "$@" echo '== Waiting for Coder to become ready' # Start the timeout in the background so interrupting this script @@ -178,10 +180,15 @@ fatal() { fi if [ "${use_proxy}" -gt "0" ]; then - # Create the proxy - "${CODER_DEV_SHIM}" proxy register --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --access-url=http://localhost:3010 --only-token - # Start the proxy - start_cmd PROXY proxy "" "${CODER_DEV_SHIM}" proxy --listen-addr + log "Using external workspace proxy" + ( + # Attempt to delete the proxy first, in case it already exists. + "${CODER_DEV_SHIM}" proxy delete name=local-proxy + # Create the proxy + proxy_session_token=$("${CODER_DEV_SHIM}" proxy register --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --access-url=http://localhost:3010 --only-token) + # Start the proxy + start_cmd PROXY "" "${CODER_DEV_SHIM}" proxy server --http-address=localhost:3010 --proxy-session-token=${proxy_session_token} --primary-access-url=http://localhost:3000 --app-security-key=${app_security_key} + ) || echo "Failed to create workspace proxy. No workspace proxy created." fi # Start the frontend once we have a template up and running @@ -208,6 +215,11 @@ fatal() { for iface in "${interfaces[@]}"; do log "$(printf "== Web UI: http://%s:8080%$((space_padding - ${#iface}))s==" "$iface" "")" done + if [ "${use_proxy}" -gt "0" ]; then + for iface in "${interfaces[@]}"; do + log "$(printf "== Proxy: http://%s:3010%$((space_padding - ${#iface}))s==" "$iface" "")" + done + fi log "== ==" log "== Use ./scripts/coder-dev.sh to talk to this instance! ==" log "$(printf "== alias cdr=%s/scripts/coder-dev.sh%$((space_padding - ${#PWD}))s==" "$PWD" "")" From cc29ffcd671ed80c8ddac3169c54fb251ec3b654 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 09:28:27 -0500 Subject: [PATCH 10/22] Make gen --- coderd/apidoc/docs.go | 50 +++++++++++++++- coderd/apidoc/swagger.json | 9 ++- docs/api/workspaceproxies.md | 102 +++++++++++++++++++++++++++++++++ docs/manifest.json | 4 ++ site/src/api/typesGenerated.ts | 1 - 5 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 docs/api/workspaceproxies.md diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 135cdf1f7e9b6..607a83de4d531 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1685,6 +1685,48 @@ const docTemplate = `{ } } }, + "/proxy-internal/issue-signed-app-token": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Issue signed workspace app token", + "operationId": "issue-signed-workspace-app-token", + "parameters": [ + { + "description": "Issue signed app token request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/workspaceapps.IssueTokenRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/wsproxysdk.IssueSignedAppTokenResponse" + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/replicas": { "get": { "security": [ @@ -6371,12 +6413,12 @@ const docTemplate = `{ "description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.", "type": "string" }, + "is_workspace_proxy": { + "type": "boolean" + }, "version": { "description": "Version returns the semantic version of the build.", "type": "string" - }, - "workspace_proxy": { - "type": "boolean" } } }, @@ -7214,9 +7256,11 @@ const docTemplate = `{ "codersdk.Experiment": { "type": "string", "enum": [ + "template_editor", "moons" ], "x-enum-varnames": [ + "ExperimentTemplateEditor", "ExperimentMoons" ] }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 31acf01b313b3..7b92b3036c4e2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5683,12 +5683,15 @@ "description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.", "type": "string" }, + "is_workspace_proxy": { + "type": "boolean" + }, "version": { "description": "Version returns the semantic version of the build.", "type": "string" }, "workspace_proxy": { - "type": "boolean" + "$ref": "#/definitions/codersdk.WorkspaceProxyBuildInfo" } } }, @@ -6463,8 +6466,8 @@ }, "codersdk.Experiment": { "type": "string", - "enum": ["moons"], - "x-enum-varnames": ["ExperimentMoons"] + "enum": ["template_editor", "moons"], + "x-enum-varnames": ["ExperimentTemplateEditor", "ExperimentMoons"] }, "codersdk.Feature": { "type": "object", diff --git a/docs/api/workspaceproxies.md b/docs/api/workspaceproxies.md new file mode 100644 index 0000000000000..e3311950560de --- /dev/null +++ b/docs/api/workspaceproxies.md @@ -0,0 +1,102 @@ +# WorkspaceProxies + +## Create workspace proxy + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/workspaceproxies \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /workspaceproxies` + +> Body parameter + +```json +{ + "display_name": "string", + "icon": "string", + "name": "string", + "url": "string", + "wildcard_hostname": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | -------------------------------------------------------------------------------------- | -------- | ------------------------------ | +| `body` | body | [codersdk.CreateWorkspaceProxyRequest](schemas.md#codersdkcreateworkspaceproxyrequest) | true | Create workspace proxy request | + +### Example responses + +> 201 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "deleted": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "updated_at": "2019-08-24T14:15:22Z", + "url": "string", + "wildcard_hostname": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------------ | ----------- | ------------------------------------------------------------ | +| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.WorkspaceProxy](schemas.md#codersdkworkspaceproxy) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Delete workspace proxy + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy} \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /workspaceproxies/{workspaceproxy}` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------------ | -------- | ---------------- | +| `workspaceproxy` | path | string(uuid) | true | Proxy ID or name | + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/manifest.json b/docs/manifest.json index 4c4fb7d1879b5..d4949510d45e2 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -462,6 +462,10 @@ "title": "Users", "path": "./api/users.md" }, + { + "title": "WorkspaceProxies", + "path": "./api/workspaceproxies.md" + }, { "title": "Workspaces", "path": "./api/workspaces.md" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6917b89456bab..669656dbfbfa7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1,4 +1,3 @@ -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. // From codersdk/apikey.go export interface APIKey { From 54a6fc5e724781ec613483ce97ba90d8e7f686c3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 11:09:38 -0500 Subject: [PATCH 11/22] Import ordeR --- enterprise/cli/workspaceproxy.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 90e9ed53d9cbc..4b1d208ab9885 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -15,23 +15,20 @@ import ( rpprof "runtime/pprof" "time" - "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/coderd/httpmw" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/coreos/go-systemd/daemon" - - "github.com/coder/coder/cli/cliui" "golang.org/x/xerrors" "github.com/coder/coder/cli" - "github.com/coder/coder/coderd/workspaceapps" - "github.com/coder/coder/enterprise/wsproxy" - "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/workspaceapps" "github.com/coder/coder/codersdk" + "github.com/coder/coder/enterprise/wsproxy" + "github.com/coreos/go-systemd/daemon" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" ) func (r *RootCmd) workspaceProxy() *clibase.Cmd { From 139c309838357f02f849146302e02e22c7251784 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 11:19:47 -0500 Subject: [PATCH 12/22] Quote shell var --- scripts/develop.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/develop.sh b/scripts/develop.sh index ed9f4c091db54..2d63b88e35211 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -187,7 +187,7 @@ fatal() { # Create the proxy proxy_session_token=$("${CODER_DEV_SHIM}" proxy register --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --access-url=http://localhost:3010 --only-token) # Start the proxy - start_cmd PROXY "" "${CODER_DEV_SHIM}" proxy server --http-address=localhost:3010 --proxy-session-token=${proxy_session_token} --primary-access-url=http://localhost:3000 --app-security-key=${app_security_key} + start_cmd PROXY "" "${CODER_DEV_SHIM}" proxy server --http-address=localhost:3010 --proxy-session-token="${proxy_session_token}" --primary-access-url=http://localhost:3000 --app-security-key="${app_security_key}" ) || echo "Failed to create workspace proxy. No workspace proxy created." fi From 6ce0dec7042c3f28f372f7b082dec711474cbcc0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 16:22:16 +0000 Subject: [PATCH 13/22] Imports --- cli/appsecuritykey.go | 3 ++- enterprise/cli/workspaceproxy.go | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cli/appsecuritykey.go b/cli/appsecuritykey.go index ed48733d49857..adda0111903e0 100644 --- a/cli/appsecuritykey.go +++ b/cli/appsecuritykey.go @@ -4,10 +4,11 @@ import ( "database/sql" "fmt" + "golang.org/x/xerrors" + "github.com/coder/coder/cli/clibase" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/migrations" - "golang.org/x/xerrors" ) func (*RootCmd) appSecurityKey() *clibase.Cmd { diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 4b1d208ab9885..b317da27ccf15 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -17,6 +17,11 @@ import ( "golang.org/x/xerrors" + "github.com/coreos/go-systemd/daemon" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/coder/coder/cli" "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" @@ -25,10 +30,6 @@ import ( "github.com/coder/coder/coderd/workspaceapps" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/wsproxy" - "github.com/coreos/go-systemd/daemon" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" ) func (r *RootCmd) workspaceProxy() *clibase.Cmd { From ffc3877fdd3f91a8df1ad9090f5701fa808d82f0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 11:22:31 -0500 Subject: [PATCH 14/22] Fix lint --- enterprise/cli/workspaceproxy.go | 2 +- scripts/develop.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index b317da27ccf15..4353d9dbe6542 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -258,7 +258,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { ), Handler: func(inv *clibase.Invocation) error { if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") { - return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL) + return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL.String()) } secKey, err := workspaceapps.KeyFromString(appSecuritYKey.Value()) diff --git a/scripts/develop.sh b/scripts/develop.sh index 2d63b88e35211..888afc7cf0f4f 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -183,7 +183,7 @@ fatal() { log "Using external workspace proxy" ( # Attempt to delete the proxy first, in case it already exists. - "${CODER_DEV_SHIM}" proxy delete name=local-proxy + "${CODER_DEV_SHIM}" proxy delete name=local-proxy || true # Create the proxy proxy_session_token=$("${CODER_DEV_SHIM}" proxy register --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --access-url=http://localhost:3010 --only-token) # Start the proxy From db78701937729667bdc6f3425a1b0b2d9c90cb8b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Apr 2023 16:25:46 +0000 Subject: [PATCH 15/22] Tabs vs spaces --- scripts/develop.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/develop.sh b/scripts/develop.sh index 888afc7cf0f4f..d79b1af9d453b 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -31,10 +31,10 @@ while true; do password="$2" shift 2 ;; - --use-proxy) - use_proxy=1 - shift - ;; + --use-proxy) + use_proxy=1 + shift + ;; --) shift break From e49f96cad80507247922ea69ecd21193b74a47b0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 12:15:08 -0500 Subject: [PATCH 16/22] remove unused command --- cli/appsecuritykey.go | 60 ------------------------------------------- cli/root.go | 1 - 2 files changed, 61 deletions(-) delete mode 100644 cli/appsecuritykey.go diff --git a/cli/appsecuritykey.go b/cli/appsecuritykey.go deleted file mode 100644 index adda0111903e0..0000000000000 --- a/cli/appsecuritykey.go +++ /dev/null @@ -1,60 +0,0 @@ -package cli - -import ( - "database/sql" - "fmt" - - "golang.org/x/xerrors" - - "github.com/coder/coder/cli/clibase" - "github.com/coder/coder/coderd/database" - "github.com/coder/coder/coderd/database/migrations" -) - -func (*RootCmd) appSecurityKey() *clibase.Cmd { - var postgresURL string - - root := &clibase.Cmd{ - Use: "app-security-key", - Short: "Directly connect to the database to print the app security key", - // We can unhide this if we decide to keep it this way. - Hidden: true, - Middleware: clibase.RequireNArgs(0), - Handler: func(inv *clibase.Invocation) error { - sqlDB, err := sql.Open("postgres", postgresURL) - if err != nil { - return xerrors.Errorf("dial postgres: %w", err) - } - defer sqlDB.Close() - err = sqlDB.Ping() - if err != nil { - return xerrors.Errorf("ping postgres: %w", err) - } - - err = migrations.EnsureClean(sqlDB) - if err != nil { - return xerrors.Errorf("database needs migration: %w", err) - } - db := database.New(sqlDB) - - key, err := db.GetAppSecurityKey(inv.Context()) - if err != nil { - return xerrors.Errorf("retrieving key: %w", err) - } - - _, _ = fmt.Fprintln(inv.Stdout, key) - return nil - }, - } - - root.Options = clibase.OptionSet{ - { - Flag: "postgres-url", - Description: "URL of a PostgreSQL database to connect to.", - Env: "CODER_PG_CONNECTION_URL", - Value: clibase.StringOf(&postgresURL), - }, - } - - return root -} diff --git a/cli/root.go b/cli/root.go index 2d47908f67ecd..a71626ccdf3ef 100644 --- a/cli/root.go +++ b/cli/root.go @@ -79,7 +79,6 @@ func (r *RootCmd) Core() []*clibase.Cmd { r.portForward(), r.publickey(), r.resetPassword(), - r.appSecurityKey(), r.state(), r.templates(), r.users(), From 5f05cbff36de5c91d6a0bfa0ac3662a478667e45 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 13:30:17 -0500 Subject: [PATCH 17/22] Fix compile and make gen --- coderd/apidoc/docs.go | 80 +++++++++++----------- coderd/apidoc/swagger.json | 35 +++++++++- coderd/httpmw/workspaceproxy.go | 4 +- docs/api/enterprise.md | 44 ++++++++++++ docs/api/workspaceproxies.md | 102 ---------------------------- docs/manifest.json | 4 -- enterprise/coderd/workspaceproxy.go | 6 +- site/src/api/typesGenerated.ts | 1 - 8 files changed, 119 insertions(+), 157 deletions(-) delete mode 100644 docs/api/workspaceproxies.md diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 607a83de4d531..ac7207ca474af 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1685,48 +1685,6 @@ const docTemplate = `{ } } }, - "/proxy-internal/issue-signed-app-token": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Enterprise" - ], - "summary": "Issue signed workspace app token", - "operationId": "issue-signed-workspace-app-token", - "parameters": [ - { - "description": "Issue signed app token request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/workspaceapps.IssueTokenRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/wsproxysdk.IssueSignedAppTokenResponse" - } - } - }, - "x-apidocgen": { - "skip": true - } - } - }, "/replicas": { "get": { "security": [ @@ -5109,6 +5067,44 @@ const docTemplate = `{ } } }, + "/workspaceproxies/{workspaceproxy}": { + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Delete workspace proxy", + "operationId": "delete-workspace-proxy", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Proxy ID or name", + "name": "workspaceproxy", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/workspaces": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7b92b3036c4e2..fbd9e852c19fd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4459,6 +4459,38 @@ } } }, + "/workspaceproxies/{workspaceproxy}": { + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Delete workspace proxy", + "operationId": "delete-workspace-proxy", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Proxy ID or name", + "name": "workspaceproxy", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/workspaces": { "get": { "security": [ @@ -5689,9 +5721,6 @@ "version": { "description": "Version returns the semantic version of the build.", "type": "string" - }, - "workspace_proxy": { - "$ref": "#/definitions/codersdk.WorkspaceProxyBuildInfo" } } }, diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go index 540ccc2a24b21..a4d90d66c3dd3 100644 --- a/coderd/httpmw/workspaceproxy.go +++ b/coderd/httpmw/workspaceproxy.go @@ -161,8 +161,8 @@ func ExtractWorkspaceProxy(opts ExtractWorkspaceProxyConfig) func(http.Handler) type workspaceProxyParamContextKey struct{} -// WorkspaceProxy returns the worksace proxy from the ExtractWorkspaceProxyParam handler. -func WorkspaceProxy(r *http.Request) database.WorkspaceProxy { +// WorkspaceProxyParam returns the worksace proxy from the ExtractWorkspaceProxyParam handler. +func WorkspaceProxyParam(r *http.Request) database.WorkspaceProxy { user, ok := r.Context().Value(workspaceProxyParamContextKey{}).(database.WorkspaceProxy) if !ok { panic("developer error: workspace proxy parameter middleware not provided") diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index f82e4f153b75a..a6bc536bb27e6 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -1272,3 +1272,47 @@ curl -X POST http://coder-server:8080/api/v2/workspaceproxies \ | 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.WorkspaceProxy](schemas.md#codersdkworkspaceproxy) | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Delete workspace proxy + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy} \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /workspaceproxies/{workspaceproxy}` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------------ | -------- | ---------------- | +| `workspaceproxy` | path | string(uuid) | true | Proxy ID or name | + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/workspaceproxies.md b/docs/api/workspaceproxies.md deleted file mode 100644 index e3311950560de..0000000000000 --- a/docs/api/workspaceproxies.md +++ /dev/null @@ -1,102 +0,0 @@ -# WorkspaceProxies - -## Create workspace proxy - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/workspaceproxies \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /workspaceproxies` - -> Body parameter - -```json -{ - "display_name": "string", - "icon": "string", - "name": "string", - "url": "string", - "wildcard_hostname": "string" -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | -------------------------------------------------------------------------------------- | -------- | ------------------------------ | -| `body` | body | [codersdk.CreateWorkspaceProxyRequest](schemas.md#codersdkcreateworkspaceproxyrequest) | true | Create workspace proxy request | - -### Example responses - -> 201 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "deleted": true, - "icon": "string", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "name": "string", - "updated_at": "2019-08-24T14:15:22Z", - "url": "string", - "wildcard_hostname": "string" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------------ | ----------- | ------------------------------------------------------------ | -| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.WorkspaceProxy](schemas.md#codersdkworkspaceproxy) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Delete workspace proxy - -### Code samples - -```shell -# Example request using curl -curl -X DELETE http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy} \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`DELETE /workspaceproxies/{workspaceproxy}` - -### Parameters - -| Name | In | Type | Required | Description | -| ---------------- | ---- | ------------ | -------- | ---------------- | -| `workspaceproxy` | path | string(uuid) | true | Proxy ID or name | - -### Example responses - -> 200 Response - -```json -{ - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/manifest.json b/docs/manifest.json index d4949510d45e2..4c4fb7d1879b5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -462,10 +462,6 @@ "title": "Users", "path": "./api/users.md" }, - { - "title": "WorkspaceProxies", - "path": "./api/workspaceproxies.md" - }, { "title": "Workspaces", "path": "./api/workspaces.md" diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 3642aeb177f2d..3068c23002020 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -23,8 +23,8 @@ import ( // @Summary Delete workspace proxy // @ID delete-workspace-proxy -// @Security CoderSessionToken -// @Accept json +// @Security CoderSessionTokeny + // @Produce json // @Tags Enterprise // @Param workspaceproxy path string true "Proxy ID or name" format(uuid) @@ -33,7 +33,7 @@ import ( func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() - proxy = httpmw.WorkspaceProxy(r) + proxy = httpmw.WorkspaceProxyParam(r) auditor = api.AGPL.Auditor.Load() aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{ Audit: *auditor, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 669656dbfbfa7..2580c165230d1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1,4 +1,3 @@ - // From codersdk/apikey.go export interface APIKey { readonly id: string From 0d9da63a6cb4ba6c07cf51665a91dedae9390510 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 13:38:38 -0500 Subject: [PATCH 18/22] Fix slim cmd to include extra proxy cmds --- enterprise/cli/proxyserver.go | 361 ++++++++++++++++++ ...spaceproxy_slim.go => proxyserver_slim.go} | 8 +- enterprise/cli/workspaceproxy.go | 355 +---------------- 3 files changed, 367 insertions(+), 357 deletions(-) create mode 100644 enterprise/cli/proxyserver.go rename enterprise/cli/{workspaceproxy_slim.go => proxyserver_slim.go} (83%) diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go new file mode 100644 index 0000000000000..937fe87b3f1cb --- /dev/null +++ b/enterprise/cli/proxyserver.go @@ -0,0 +1,361 @@ +//go:build !slim + +package cli + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "net/http/pprof" + "os/signal" + "regexp" + rpprof "runtime/pprof" + "time" + + "github.com/coreos/go-systemd/daemon" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli" + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/workspaceapps" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/enterprise/wsproxy" +) + +type closers []func() + +func (c closers) Close() { + for _, closeF := range c { + closeF() + } +} + +func (c *closers) Add(f func()) { + *c = append(*c, f) +} + +func (r *RootCmd) proxyServer() *clibase.Cmd { + var ( + cfg = new(codersdk.DeploymentValues) + // Filter options for only relevant ones. + opts = cfg.Options().Filter(codersdk.IsExternalProxies) + + externalProxyOptionGroup = clibase.Group{ + Name: "External Workspace Proxy", + YAML: "externalWorkspaceProxy", + } + proxySessionToken clibase.String + primaryAccessURL clibase.URL + appSecuritYKey clibase.String + ) + opts.Add( + // Options only for external workspace proxies + + clibase.Option{ + Name: "Proxy Session Token", + Description: "Authentication token for the workspace proxy to communicate with coderd.", + Flag: "proxy-session-token", + Env: "CODER_PROXY_SESSION_TOKEN", + YAML: "proxySessionToken", + Default: "", + Value: &proxySessionToken, + Group: &externalProxyOptionGroup, + Hidden: false, + }, + + clibase.Option{ + Name: "Coderd (Primary) Access URL", + Description: "URL to communicate with coderd. This should match the access URL of the Coder deployment.", + Flag: "primary-access-url", + Env: "CODER_PRIMARY_ACCESS_URL", + YAML: "primaryAccessURL", + Default: "", + Value: &primaryAccessURL, + Group: &externalProxyOptionGroup, + Hidden: false, + }, + + // TODO: Make sure this is kept secret. Idk if a flag is the best option + clibase.Option{ + Name: "App Security Key", + Description: "App security key used for decrypting/verifying app tokens sent from coderd.", + Flag: "app-security-key", + Env: "CODER_APP_SECURITY_KEY", + YAML: "appSecurityKey", + Default: "", + Value: &appSecuritYKey, + Group: &externalProxyOptionGroup, + Hidden: false, + Annotations: clibase.Annotations{}.Mark("secret", "true"), + }, + ) + + client := new(codersdk.Client) + cmd := &clibase.Cmd{ + Use: "server", + Short: "Start a workspace proxy server", + Options: opts, + Middleware: clibase.Chain( + cli.WriteConfigMW(cfg), + cli.PrintDeprecatedOptions(), + clibase.RequireNArgs(0), + // We need a client to connect with the primary coderd instance. + r.InitClient(client), + ), + Handler: func(inv *clibase.Invocation) error { + if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") { + return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL.String()) + } + + secKey, err := workspaceapps.KeyFromString(appSecuritYKey.Value()) + if err != nil { + return xerrors.Errorf("app security key: %w", err) + } + + var closers closers + // Main command context for managing cancellation of running + // services. + ctx, topCancel := context.WithCancel(inv.Context()) + defer topCancel() + closers.Add(topCancel) + + go cli.DumpHandler(ctx) + + cli.PrintLogo(inv) + logger, logCloser, err := cli.BuildLogger(inv, cfg) + if err != nil { + return xerrors.Errorf("make logger: %w", err) + } + defer logCloser() + closers.Add(logCloser) + + logger.Debug(ctx, "started debug logging") + logger.Sync() + + // Register signals early on so that graceful shutdown can't + // be interrupted by additional signals. Note that we avoid + // shadowing cancel() (from above) here because notifyStop() + // restores default behavior for the signals. This protects + // the shutdown sequence from abruptly terminating things + // like: database migrations, provisioner work, workspace + // cleanup in dev-mode, etc. + // + // To get out of a graceful shutdown, the user can send + // SIGQUIT with ctrl+\ or SIGKILL with `kill -9`. + notifyCtx, notifyStop := signal.NotifyContext(ctx, cli.InterruptSignals...) + defer notifyStop() + + // Clean up idle connections at the end, e.g. + // embedded-postgres can leave an idle connection + // which is caught by goleaks. + defer http.DefaultClient.CloseIdleConnections() + closers.Add(http.DefaultClient.CloseIdleConnections) + + tracer, _ := cli.ConfigureTraceProvider(ctx, logger, inv, cfg) + + httpServers, err := cli.ConfigureHTTPServers(inv, cfg) + if err != nil { + return xerrors.Errorf("configure http(s): %w", err) + } + defer httpServers.Close() + closers.Add(httpServers.Close) + + // TODO: @emyrk I find this strange that we add this to the context + // at the root here. + ctx, httpClient, err := cli.ConfigureHTTPClient( + ctx, + cfg.TLS.ClientCertFile.String(), + cfg.TLS.ClientKeyFile.String(), + cfg.TLS.ClientCAFile.String(), + ) + if err != nil { + return xerrors.Errorf("configure http client: %w", err) + } + defer httpClient.CloseIdleConnections() + closers.Add(httpClient.CloseIdleConnections) + + // Warn the user if the access URL appears to be a loopback address. + isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value()) + if isLocal || err != nil { + reason := "could not be resolved" + if isLocal { + reason = "isn't externally reachable" + } + cliui.Warnf( + inv.Stderr, + "The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", + cliui.Styles.Field.Render(cfg.AccessURL.String()), reason, + ) + } + + // A newline is added before for visibility in terminal output. + cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String()) + + var appHostnameRegex *regexp.Regexp + appHostname := cfg.WildcardAccessURL.String() + if appHostname != "" { + appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) + if err != nil { + return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err) + } + } + + realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins) + if err != nil { + return xerrors.Errorf("parse real ip config: %w", err) + } + + if cfg.Pprof.Enable { + // This prevents the pprof import from being accidentally deleted. + // pprof has an init function that attaches itself to the default handler. + // By passing a nil handler to 'serverHandler', it will automatically use + // the default, which has pprof attached. + _ = pprof.Handler + //nolint:revive + closeFunc := cli.ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof") + defer closeFunc() + closers.Add(closeFunc) + } + + prometheusRegistry := prometheus.NewRegistry() + if cfg.Prometheus.Enable { + prometheusRegistry.MustRegister(collectors.NewGoCollector()) + prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + + //nolint:revive + closeFunc := cli.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( + prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}), + ), cfg.Prometheus.Address.String(), "prometheus") + defer closeFunc() + closers.Add(closeFunc) + } + + proxy, err := wsproxy.New(&wsproxy.Options{ + Logger: logger, + PrimaryAccessURL: primaryAccessURL.Value(), + AccessURL: cfg.AccessURL.Value(), + AppHostname: appHostname, + AppHostnameRegex: appHostnameRegex, + RealIPConfig: realIPConfig, + AppSecurityKey: secKey, + Tracing: tracer, + PrometheusRegistry: prometheusRegistry, + APIRateLimit: int(cfg.RateLimit.API.Value()), + SecureAuthCookie: cfg.SecureAuthCookie.Value(), + DisablePathApps: cfg.DisablePathApps.Value(), + ProxySessionToken: proxySessionToken.Value(), + }) + if err != nil { + return xerrors.Errorf("create workspace proxy: %w", err) + } + + shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) + defer shutdownConns() + closers.Add(shutdownConns) + // ReadHeaderTimeout is purposefully not enabled. It caused some + // issues with websockets over the dev tunnel. + // See: https://github.com/coder/coder/pull/3730 + //nolint:gosec + httpServer := &http.Server{ + // These errors are typically noise like "TLS: EOF". Vault does + // similar: + // https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714 + ErrorLog: log.New(io.Discard, "", 0), + Handler: proxy.Handler, + BaseContext: func(_ net.Listener) context.Context { + return shutdownConnsCtx + }, + } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = httpServer.Shutdown(ctx) + }() + + // TODO: So this obviously is not going to work well. + errCh := make(chan error, 1) + go rpprof.Do(ctx, rpprof.Labels("service", "workspace-proxy"), func(ctx context.Context) { + errCh <- httpServers.Serve(httpServer) + }) + + cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") + + // Updates the systemd status from activating to activated. + _, err = daemon.SdNotify(false, daemon.SdNotifyReady) + if err != nil { + return xerrors.Errorf("notify systemd: %w", err) + } + + // Currently there is no way to ask the server to shut + // itself down, so any exit signal will result in a non-zero + // exit of the server. + var exitErr error + select { + case exitErr = <-errCh: + case <-notifyCtx.Done(): + exitErr = notifyCtx.Err() + _, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Bold.Render( + "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit", + )) + } + + if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) { + cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr) + } + + // Begin clean shut down stage, we try to shut down services + // gracefully in an order that gives the best experience. + // This procedure should not differ greatly from the order + // of `defer`s in this function, but allows us to inform + // the user about what's going on and handle errors more + // explicitly. + + _, err = daemon.SdNotify(false, daemon.SdNotifyStopping) + if err != nil { + cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err) + } + + // Stop accepting new connections without interrupting + // in-flight requests, give in-flight requests 5 seconds to + // complete. + cliui.Info(inv.Stdout, "Shutting down API server..."+"\n") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err = httpServer.Shutdown(shutdownCtx) + if err != nil { + cliui.Errorf(inv.Stderr, "API server shutdown took longer than 3s: %s\n", err) + } else { + cliui.Info(inv.Stdout, "Gracefully shut down API server\n") + } + // Cancel any remaining in-flight requests. + shutdownConns() + + // Trigger context cancellation for any remaining services. + closers.Close() + + switch { + case xerrors.Is(exitErr, context.DeadlineExceeded): + cliui.Warnf(inv.Stderr, "Graceful shutdown timed out") + // Errors here cause a significant number of benign CI failures. + return nil + case xerrors.Is(exitErr, context.Canceled): + return nil + case exitErr != nil: + return xerrors.Errorf("graceful shutdown: %w", exitErr) + default: + return nil + } + }, + } + + return cmd +} diff --git a/enterprise/cli/workspaceproxy_slim.go b/enterprise/cli/proxyserver_slim.go similarity index 83% rename from enterprise/cli/workspaceproxy_slim.go rename to enterprise/cli/proxyserver_slim.go index bb296a2cb73a3..d484c43bde298 100644 --- a/enterprise/cli/workspaceproxy_slim.go +++ b/enterprise/cli/proxyserver_slim.go @@ -11,11 +11,11 @@ import ( "github.com/coder/coder/cli/cliui" ) -func (r *RootCmd) workspaceProxy() *clibase.Cmd { +func (r *RootCmd) proxyServer() *clibase.Cmd { root := &clibase.Cmd{ - Use: "workspace-proxy", - Short: "Manage workspace proxies", - Aliases: []string{"proxy"}, + Use: "server", + Short: "Start a workspace proxy server", + Aliases: []string{}, // We accept RawArgs so all commands and flags are accepted. RawArgs: true, Hidden: true, diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 4353d9dbe6542..c62d4420086e4 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -1,35 +1,13 @@ -//go:build !slim - package cli import ( - "context" "fmt" - "io" - "log" - "net" - "net/http" - "net/http/pprof" - "os/signal" - "regexp" - rpprof "runtime/pprof" - "time" "golang.org/x/xerrors" - "github.com/coreos/go-systemd/daemon" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/coder/coder/cli" "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" - "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/coderd/httpmw" - "github.com/coder/coder/coderd/workspaceapps" "github.com/coder/coder/codersdk" - "github.com/coder/coder/enterprise/wsproxy" ) func (r *RootCmd) workspaceProxy() *clibase.Cmd { @@ -43,7 +21,7 @@ func (r *RootCmd) workspaceProxy() *clibase.Cmd { }, Children: []*clibase.Cmd{ r.proxyServer(), - r.registerProxy(), + r.createProxy(), r.deleteProxy(), }, } @@ -75,7 +53,7 @@ func (r *RootCmd) deleteProxy() *clibase.Cmd { return cmd } -func (r *RootCmd) registerProxy() *clibase.Cmd { +func (r *RootCmd) createProxy() *clibase.Cmd { var ( proxyName string displayName string @@ -175,332 +153,3 @@ func (r *RootCmd) registerProxy() *clibase.Cmd { ) return cmd } - -type closers []func() - -func (c closers) Close() { - for _, closeF := range c { - closeF() - } -} - -func (c *closers) Add(f func()) { - *c = append(*c, f) -} - -func (r *RootCmd) proxyServer() *clibase.Cmd { - var ( - cfg = new(codersdk.DeploymentValues) - // Filter options for only relevant ones. - opts = cfg.Options().Filter(codersdk.IsExternalProxies) - - externalProxyOptionGroup = clibase.Group{ - Name: "External Workspace Proxy", - YAML: "externalWorkspaceProxy", - } - proxySessionToken clibase.String - primaryAccessURL clibase.URL - appSecuritYKey clibase.String - ) - opts.Add( - // Options only for external workspace proxies - - clibase.Option{ - Name: "Proxy Session Token", - Description: "Authentication token for the workspace proxy to communicate with coderd.", - Flag: "proxy-session-token", - Env: "CODER_PROXY_SESSION_TOKEN", - YAML: "proxySessionToken", - Default: "", - Value: &proxySessionToken, - Group: &externalProxyOptionGroup, - Hidden: false, - }, - - clibase.Option{ - Name: "Coderd (Primary) Access URL", - Description: "URL to communicate with coderd. This should match the access URL of the Coder deployment.", - Flag: "primary-access-url", - Env: "CODER_PRIMARY_ACCESS_URL", - YAML: "primaryAccessURL", - Default: "", - Value: &primaryAccessURL, - Group: &externalProxyOptionGroup, - Hidden: false, - }, - - // TODO: Make sure this is kept secret. Idk if a flag is the best option - clibase.Option{ - Name: "App Security Key", - Description: "App security key used for decrypting/verifying app tokens sent from coderd.", - Flag: "app-security-key", - Env: "CODER_APP_SECURITY_KEY", - YAML: "appSecurityKey", - Default: "", - Value: &appSecuritYKey, - Group: &externalProxyOptionGroup, - Hidden: false, - Annotations: clibase.Annotations{}.Mark("secret", "true"), - }, - ) - - client := new(codersdk.Client) - cmd := &clibase.Cmd{ - Use: "server", - Short: "Start a workspace proxy server", - Options: opts, - Middleware: clibase.Chain( - cli.WriteConfigMW(cfg), - cli.PrintDeprecatedOptions(), - clibase.RequireNArgs(0), - // We need a client to connect with the primary coderd instance. - r.InitClient(client), - ), - Handler: func(inv *clibase.Invocation) error { - if !(primaryAccessURL.Scheme == "http" || primaryAccessURL.Scheme == "https") { - return xerrors.Errorf("primary access URL must be http or https: url=%s", primaryAccessURL.String()) - } - - secKey, err := workspaceapps.KeyFromString(appSecuritYKey.Value()) - if err != nil { - return xerrors.Errorf("app security key: %w", err) - } - - var closers closers - // Main command context for managing cancellation of running - // services. - ctx, topCancel := context.WithCancel(inv.Context()) - defer topCancel() - closers.Add(topCancel) - - go cli.DumpHandler(ctx) - - cli.PrintLogo(inv) - logger, logCloser, err := cli.BuildLogger(inv, cfg) - if err != nil { - return xerrors.Errorf("make logger: %w", err) - } - defer logCloser() - closers.Add(logCloser) - - logger.Debug(ctx, "started debug logging") - logger.Sync() - - // Register signals early on so that graceful shutdown can't - // be interrupted by additional signals. Note that we avoid - // shadowing cancel() (from above) here because notifyStop() - // restores default behavior for the signals. This protects - // the shutdown sequence from abruptly terminating things - // like: database migrations, provisioner work, workspace - // cleanup in dev-mode, etc. - // - // To get out of a graceful shutdown, the user can send - // SIGQUIT with ctrl+\ or SIGKILL with `kill -9`. - notifyCtx, notifyStop := signal.NotifyContext(ctx, cli.InterruptSignals...) - defer notifyStop() - - // Clean up idle connections at the end, e.g. - // embedded-postgres can leave an idle connection - // which is caught by goleaks. - defer http.DefaultClient.CloseIdleConnections() - closers.Add(http.DefaultClient.CloseIdleConnections) - - tracer, _ := cli.ConfigureTraceProvider(ctx, logger, inv, cfg) - - httpServers, err := cli.ConfigureHTTPServers(inv, cfg) - if err != nil { - return xerrors.Errorf("configure http(s): %w", err) - } - defer httpServers.Close() - closers.Add(httpServers.Close) - - // TODO: @emyrk I find this strange that we add this to the context - // at the root here. - ctx, httpClient, err := cli.ConfigureHTTPClient( - ctx, - cfg.TLS.ClientCertFile.String(), - cfg.TLS.ClientKeyFile.String(), - cfg.TLS.ClientCAFile.String(), - ) - if err != nil { - return xerrors.Errorf("configure http client: %w", err) - } - defer httpClient.CloseIdleConnections() - closers.Add(httpClient.CloseIdleConnections) - - // Warn the user if the access URL appears to be a loopback address. - isLocal, err := cli.IsLocalURL(ctx, cfg.AccessURL.Value()) - if isLocal || err != nil { - reason := "could not be resolved" - if isLocal { - reason = "isn't externally reachable" - } - cliui.Warnf( - inv.Stderr, - "The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", - cliui.Styles.Field.Render(cfg.AccessURL.String()), reason, - ) - } - - // A newline is added before for visibility in terminal output. - cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String()) - - var appHostnameRegex *regexp.Regexp - appHostname := cfg.WildcardAccessURL.String() - if appHostname != "" { - appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) - if err != nil { - return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err) - } - } - - realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins) - if err != nil { - return xerrors.Errorf("parse real ip config: %w", err) - } - - if cfg.Pprof.Enable { - // This prevents the pprof import from being accidentally deleted. - // pprof has an init function that attaches itself to the default handler. - // By passing a nil handler to 'serverHandler', it will automatically use - // the default, which has pprof attached. - _ = pprof.Handler - //nolint:revive - closeFunc := cli.ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof") - defer closeFunc() - closers.Add(closeFunc) - } - - prometheusRegistry := prometheus.NewRegistry() - if cfg.Prometheus.Enable { - prometheusRegistry.MustRegister(collectors.NewGoCollector()) - prometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - - //nolint:revive - closeFunc := cli.ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( - prometheusRegistry, promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}), - ), cfg.Prometheus.Address.String(), "prometheus") - defer closeFunc() - closers.Add(closeFunc) - } - - proxy, err := wsproxy.New(&wsproxy.Options{ - Logger: logger, - PrimaryAccessURL: primaryAccessURL.Value(), - AccessURL: cfg.AccessURL.Value(), - AppHostname: appHostname, - AppHostnameRegex: appHostnameRegex, - RealIPConfig: realIPConfig, - AppSecurityKey: secKey, - Tracing: tracer, - PrometheusRegistry: prometheusRegistry, - APIRateLimit: int(cfg.RateLimit.API.Value()), - SecureAuthCookie: cfg.SecureAuthCookie.Value(), - DisablePathApps: cfg.DisablePathApps.Value(), - ProxySessionToken: proxySessionToken.Value(), - }) - if err != nil { - return xerrors.Errorf("create workspace proxy: %w", err) - } - - shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) - defer shutdownConns() - closers.Add(shutdownConns) - // ReadHeaderTimeout is purposefully not enabled. It caused some - // issues with websockets over the dev tunnel. - // See: https://github.com/coder/coder/pull/3730 - //nolint:gosec - httpServer := &http.Server{ - // These errors are typically noise like "TLS: EOF". Vault does - // similar: - // https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714 - ErrorLog: log.New(io.Discard, "", 0), - Handler: proxy.Handler, - BaseContext: func(_ net.Listener) context.Context { - return shutdownConnsCtx - }, - } - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _ = httpServer.Shutdown(ctx) - }() - - // TODO: So this obviously is not going to work well. - errCh := make(chan error, 1) - go rpprof.Do(ctx, rpprof.Labels("service", "workspace-proxy"), func(ctx context.Context) { - errCh <- httpServers.Serve(httpServer) - }) - - cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") - - // Updates the systemd status from activating to activated. - _, err = daemon.SdNotify(false, daemon.SdNotifyReady) - if err != nil { - return xerrors.Errorf("notify systemd: %w", err) - } - - // Currently there is no way to ask the server to shut - // itself down, so any exit signal will result in a non-zero - // exit of the server. - var exitErr error - select { - case exitErr = <-errCh: - case <-notifyCtx.Done(): - exitErr = notifyCtx.Err() - _, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Bold.Render( - "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit", - )) - } - - if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) { - cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr) - } - - // Begin clean shut down stage, we try to shut down services - // gracefully in an order that gives the best experience. - // This procedure should not differ greatly from the order - // of `defer`s in this function, but allows us to inform - // the user about what's going on and handle errors more - // explicitly. - - _, err = daemon.SdNotify(false, daemon.SdNotifyStopping) - if err != nil { - cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err) - } - - // Stop accepting new connections without interrupting - // in-flight requests, give in-flight requests 5 seconds to - // complete. - cliui.Info(inv.Stdout, "Shutting down API server..."+"\n") - shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - err = httpServer.Shutdown(shutdownCtx) - if err != nil { - cliui.Errorf(inv.Stderr, "API server shutdown took longer than 3s: %s\n", err) - } else { - cliui.Info(inv.Stdout, "Gracefully shut down API server\n") - } - // Cancel any remaining in-flight requests. - shutdownConns() - - // Trigger context cancellation for any remaining services. - closers.Close() - - switch { - case xerrors.Is(exitErr, context.DeadlineExceeded): - cliui.Warnf(inv.Stderr, "Graceful shutdown timed out") - // Errors here cause a significant number of benign CI failures. - return nil - case xerrors.Is(exitErr, context.Canceled): - return nil - case exitErr != nil: - return xerrors.Errorf("graceful shutdown: %w", exitErr) - default: - return nil - } - }, - } - - return cmd -} From 60fb59eb08594e8f23fc4b0e5186d1d7916fe8d2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 13:44:23 -0500 Subject: [PATCH 19/22] Import order --- enterprise/coderd/workspaceproxy.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 3068c23002020..6537a749d209f 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -7,14 +7,13 @@ import ( "net/http" "net/url" - "github.com/coder/coder/coderd/httpmw" - "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/workspaceapps" "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" From b9283d8a8c20fe28f729645d03cc73f043aa3d4c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 13:50:22 -0500 Subject: [PATCH 20/22] Import order --- coderd/httpmw/workspaceproxy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go index a4d90d66c3dd3..692f51b83d2b4 100644 --- a/coderd/httpmw/workspaceproxy.go +++ b/coderd/httpmw/workspaceproxy.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/go-chi/chi/v5" - "github.com/google/uuid" "golang.org/x/xerrors" From dab84e3b0c871b98862042e7718091b7157b2639 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 14:36:32 -0500 Subject: [PATCH 21/22] Fix comment --- enterprise/coderd/workspaceproxy.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 6537a749d209f..6d679e0391d67 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -22,8 +22,7 @@ import ( // @Summary Delete workspace proxy // @ID delete-workspace-proxy -// @Security CoderSessionTokeny - +// @Security CoderSessionToken // @Produce json // @Tags Enterprise // @Param workspaceproxy path string true "Proxy ID or name" format(uuid) From ea3593d5aae9e7d2cdf9492fe0e96e8d5977ea2f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Apr 2023 14:53:42 -0500 Subject: [PATCH 22/22] Fix compile with name --- enterprise/cli/proxyserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index 937fe87b3f1cb..3340d53241555 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -241,7 +241,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { proxy, err := wsproxy.New(&wsproxy.Options{ Logger: logger, - PrimaryAccessURL: primaryAccessURL.Value(), + DashboardURL: primaryAccessURL.Value(), AccessURL: cfg.AccessURL.Value(), AppHostname: appHostname, AppHostnameRegex: appHostnameRegex,