Skip to content

Commit 944b10b

Browse files
committed
Merge branch 'main' into showpanic
2 parents 61713a0 + 06d7e36 commit 944b10b

File tree

115 files changed

+2567
-1214
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+2567
-1214
lines changed

cli/agent_test.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ func TestWorkspaceAgent(t *testing.T) {
6060
ctx := context.WithValue(ctx, "azure-client", metadataClient)
6161
errC <- cmd.ExecuteContext(ctx)
6262
}()
63-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
64-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
63+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
64+
workspace, err := client.Workspace(ctx, workspace.ID)
6565
require.NoError(t, err)
66-
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
66+
resources := workspace.LatestBuild.Resources
67+
if assert.NotEmpty(t, workspace.LatestBuild.Resources) && assert.NotEmpty(t, resources[0].Agents) {
6768
assert.NotEmpty(t, resources[0].Agents[0].Version)
6869
}
6970
dialer, err := client.DialWorkspaceAgentTailnet(ctx, slog.Logger{}, resources[0].Agents[0].ID)
@@ -120,9 +121,10 @@ func TestWorkspaceAgent(t *testing.T) {
120121
ctx := context.WithValue(ctx, "aws-client", metadataClient)
121122
errC <- cmd.ExecuteContext(ctx)
122123
}()
123-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
124-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
124+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
125+
workspace, err := client.Workspace(ctx, workspace.ID)
125126
require.NoError(t, err)
127+
resources := workspace.LatestBuild.Resources
126128
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
127129
assert.NotEmpty(t, resources[0].Agents[0].Version)
128130
}
@@ -180,9 +182,10 @@ func TestWorkspaceAgent(t *testing.T) {
180182
ctx := context.WithValue(ctx, "gcp-client", metadata)
181183
errC <- cmd.ExecuteContext(ctx)
182184
}()
183-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
184-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
185+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
186+
workspace, err := client.Workspace(ctx, workspace.ID)
185187
require.NoError(t, err)
188+
resources := workspace.LatestBuild.Resources
186189
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
187190
assert.NotEmpty(t, resources[0].Agents[0].Version)
188191
}

cli/configssh_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func TestConfigSSH(t *testing.T) {
114114
defer func() {
115115
_ = agentCloser.Close()
116116
}()
117-
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
117+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
118118
agentConn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, resources[0].Agents[0].ID)
119119
require.NoError(t, err)
120120
defer agentConn.Close()

cli/gitssh_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str
8181
errC <- cmd.ExecuteContext(ctx)
8282
}()
8383
t.Cleanup(func() { require.NoError(t, <-errC) })
84-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
84+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
8585
return agentClient, agentToken, pubkey
8686
}
8787

cli/portforward_test.go

+6-8
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ func TestPortForward(t *testing.T) {
114114
// Setup agent once to be shared between test-cases (avoid expensive
115115
// non-parallel setup).
116116
var (
117-
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
118-
user = coderdtest.CreateFirstUser(t, client)
119-
_, workspace = runAgent(t, client, user.UserID)
117+
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
118+
user = coderdtest.CreateFirstUser(t, client)
119+
workspace = runAgent(t, client, user.UserID)
120120
)
121121

122122
for _, c := range cases { //nolint:paralleltest // the `c := c` confuses the linter
@@ -283,7 +283,7 @@ func TestPortForward(t *testing.T) {
283283
// runAgent creates a fake workspace and starts an agent locally for that
284284
// workspace. The agent will be cleaned up on test completion.
285285
// nolint:unused
286-
func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) ([]codersdk.WorkspaceResource, codersdk.Workspace) {
286+
func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) codersdk.Workspace {
287287
ctx := context.Background()
288288
user, err := client.User(ctx, userID.String())
289289
require.NoError(t, err, "specified user does not exist")
@@ -336,11 +336,9 @@ func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) ([]coders
336336
errC <- cmd.ExecuteContext(agentCtx)
337337
}()
338338

339-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
340-
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
341-
require.NoError(t, err)
339+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
342340

343-
return resources, workspace
341+
return workspace
344342
}
345343

346344
// setupTestListener starts accepting connections and echoing a single packet.

cli/root.go

+18
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ const (
4848
varNoFeatureWarning = "no-feature-warning"
4949
varForceTty = "force-tty"
5050
varVerbose = "verbose"
51+
varExperimental = "experimental"
5152
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
5253

5354
envNoVersionCheck = "CODER_NO_VERSION_WARNING"
5455
envNoFeatureWarning = "CODER_NO_FEATURE_WARNING"
56+
envExperimental = "CODER_EXPERIMENTAL"
5557
)
5658

5759
var (
@@ -184,6 +186,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
184186
cmd.PersistentFlags().Bool(varNoOpen, false, "Block automatically opening URLs in the browser.")
185187
_ = cmd.PersistentFlags().MarkHidden(varNoOpen)
186188
cliflag.Bool(cmd.PersistentFlags(), varVerbose, "v", "CODER_VERBOSE", false, "Enable verbose output.")
189+
cliflag.Bool(cmd.PersistentFlags(), varExperimental, "", envExperimental, false, "Enable experimental features. Experimental features are not ready for production.")
187190

188191
return cmd
189192
}
@@ -598,3 +601,18 @@ func (h *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
598601
}
599602
return h.transport.RoundTrip(req)
600603
}
604+
605+
// ExperimentalEnabled returns if the experimental feature flag is enabled.
606+
func ExperimentalEnabled(cmd *cobra.Command) bool {
607+
return cliflag.IsSetBool(cmd, varExperimental)
608+
}
609+
610+
// EnsureExperimental will ensure that the experimental feature flag is set if the given flag is set.
611+
func EnsureExperimental(cmd *cobra.Command, name string) error {
612+
_, set := cliflag.IsSet(cmd, name)
613+
if set && !ExperimentalEnabled(cmd) {
614+
return xerrors.Errorf("flag %s is set but requires flag --experimental or environment variable CODER_EXPERIMENTAL=true.", name)
615+
}
616+
617+
return nil
618+
}

cli/root_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/coder/coder/buildinfo"
1515
"github.com/coder/coder/cli"
16+
"github.com/coder/coder/cli/cliflag"
1617
"github.com/coder/coder/cli/clitest"
1718
"github.com/coder/coder/codersdk"
1819
)
@@ -153,4 +154,19 @@ func TestRoot(t *testing.T) {
153154
// This won't succeed, because we're using the login cmd to assert requests.
154155
_ = cmd.Execute()
155156
})
157+
158+
t.Run("Experimental", func(t *testing.T) {
159+
t.Parallel()
160+
161+
cmd, _ := clitest.New(t, "--experimental")
162+
err := cmd.Execute()
163+
require.NoError(t, err)
164+
require.True(t, cli.ExperimentalEnabled(cmd))
165+
166+
cmd, _ = clitest.New(t, "help", "--verbose")
167+
_ = cmd.Execute()
168+
_, set := cliflag.IsSet(cmd, "verbose")
169+
require.True(t, set)
170+
require.ErrorContains(t, cli.EnsureExperimental(cmd, "verbose"), "--experimental")
171+
})
156172
}

cli/server.go

+56-36
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"crypto/tls"
66
"crypto/x509"
77
"database/sql"
8-
"encoding/pem"
98
"errors"
109
"fmt"
1110
"io"
@@ -106,11 +105,11 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
106105
telemetryEnable bool
107106
telemetryTraceEnable bool
108107
telemetryURL string
109-
tlsCertFile string
108+
tlsCertFiles []string
110109
tlsClientCAFile string
111110
tlsClientAuth string
112111
tlsEnable bool
113-
tlsKeyFile string
112+
tlsKeyFiles []string
114113
tlsMinVersion string
115114
tunnel bool
116115
traceEnable bool
@@ -221,7 +220,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
221220
defer listener.Close()
222221

223222
if tlsEnable {
224-
listener, err = configureTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile)
223+
listener, err = configureServerTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFiles, tlsKeyFiles, tlsClientCAFile)
225224
if err != nil {
226225
return xerrors.Errorf("configure tls: %w", err)
227226
}
@@ -369,6 +368,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
369368
AutoImportTemplates: validatedAutoImportTemplates,
370369
MetricsCacheRefreshInterval: metricsCacheRefreshInterval,
371370
AgentStatsRefreshInterval: agentStatRefreshInterval,
371+
Experimental: ExperimentalEnabled(cmd),
372372
}
373373

374374
if oauth2GithubClientSecret != "" {
@@ -842,17 +842,17 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
842842
_ = root.Flags().MarkHidden("telemetry-url")
843843
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false,
844844
"Whether TLS will be enabled.")
845-
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
846-
"Path to the certificate for TLS. It requires a PEM-encoded file. "+
845+
cliflag.StringArrayVarP(root.Flags(), &tlsCertFiles, "tls-cert-file", "", "CODER_TLS_CERT_FILE", []string{},
846+
"Path to each certificate for TLS. It requires a PEM-encoded file. "+
847847
"To configure the listener to use a CA certificate, concatenate the primary certificate "+
848848
"and the CA certificate together. The primary certificate should appear first in the combined file.")
849849
cliflag.StringVarP(root.Flags(), &tlsClientCAFile, "tls-client-ca-file", "", "CODER_TLS_CLIENT_CA_FILE", "",
850850
"PEM-encoded Certificate Authority file used for checking the authenticity of client")
851851
cliflag.StringVarP(root.Flags(), &tlsClientAuth, "tls-client-auth", "", "CODER_TLS_CLIENT_AUTH", "request",
852852
`Policy the server will follow for TLS Client Authentication. `+
853853
`Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`)
854-
cliflag.StringVarP(root.Flags(), &tlsKeyFile, "tls-key-file", "", "CODER_TLS_KEY_FILE", "",
855-
"Path to the private key for the certificate. It requires a PEM-encoded file")
854+
cliflag.StringArrayVarP(root.Flags(), &tlsKeyFiles, "tls-key-file", "", "CODER_TLS_KEY_FILE", []string{},
855+
"Paths to the private keys for each of the certificates. It requires a PEM-encoded file")
856856
cliflag.StringVarP(root.Flags(), &tlsMinVersion, "tls-min-version", "", "CODER_TLS_MIN_VERSION", "tls12",
857857
`Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
858858
cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_TUNNEL", false,
@@ -1040,7 +1040,32 @@ func printLogo(cmd *cobra.Command, spooky bool) {
10401040
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s - Remote development on your infrastucture\n", cliui.Styles.Bold.Render("Coder "+buildinfo.Version()))
10411041
}
10421042

1043-
func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) {
1043+
func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, error) {
1044+
if len(tlsCertFiles) != len(tlsKeyFiles) {
1045+
return nil, xerrors.New("--tls-cert-file and --tls-key-file must be used the same amount of times")
1046+
}
1047+
if len(tlsCertFiles) == 0 {
1048+
return nil, xerrors.New("--tls-cert-file is required when tls is enabled")
1049+
}
1050+
if len(tlsKeyFiles) == 0 {
1051+
return nil, xerrors.New("--tls-key-file is required when tls is enabled")
1052+
}
1053+
1054+
certs := make([]tls.Certificate, len(tlsCertFiles))
1055+
for i := range tlsCertFiles {
1056+
certFile, keyFile := tlsCertFiles[i], tlsKeyFiles[i]
1057+
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
1058+
if err != nil {
1059+
return nil, xerrors.Errorf("load TLS key pair %d (%q, %q): %w", i, certFile, keyFile, err)
1060+
}
1061+
1062+
certs[i] = cert
1063+
}
1064+
1065+
return certs, nil
1066+
}
1067+
1068+
func configureServerTLS(listener net.Listener, tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string) (net.Listener, error) {
10441069
tlsConfig := &tls.Config{
10451070
MinVersion: tls.VersionTLS12,
10461071
}
@@ -1072,36 +1097,31 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
10721097
return nil, xerrors.Errorf("unrecognized tls client auth: %q", tlsClientAuth)
10731098
}
10741099

1075-
if tlsCertFile == "" {
1076-
return nil, xerrors.New("tls-cert-file is required when tls is enabled")
1077-
}
1078-
if tlsKeyFile == "" {
1079-
return nil, xerrors.New("tls-key-file is required when tls is enabled")
1080-
}
1081-
1082-
certPEMBlock, err := os.ReadFile(tlsCertFile)
1083-
if err != nil {
1084-
return nil, xerrors.Errorf("read file %q: %w", tlsCertFile, err)
1085-
}
1086-
keyPEMBlock, err := os.ReadFile(tlsKeyFile)
1100+
certs, err := loadCertificates(tlsCertFiles, tlsKeyFiles)
10871101
if err != nil {
1088-
return nil, xerrors.Errorf("read file %q: %w", tlsKeyFile, err)
1089-
}
1090-
keyBlock, _ := pem.Decode(keyPEMBlock)
1091-
if keyBlock == nil {
1092-
return nil, xerrors.New("decoded pem is blank")
1093-
}
1094-
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
1095-
if err != nil {
1096-
return nil, xerrors.Errorf("create key pair: %w", err)
1097-
}
1098-
tlsConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
1099-
return &cert, nil
1102+
return nil, xerrors.Errorf("load certificates: %w", err)
11001103
}
1104+
tlsConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
1105+
// If there's only one certificate, return it.
1106+
if len(certs) == 1 {
1107+
return &certs[0], nil
1108+
}
1109+
1110+
// Expensively check which certificate matches the client hello.
1111+
for _, cert := range certs {
1112+
cert := cert
1113+
if err := hi.SupportsCertificate(&cert); err == nil {
1114+
return &cert, nil
1115+
}
1116+
}
11011117

1102-
certPool := x509.NewCertPool()
1103-
certPool.AppendCertsFromPEM(certPEMBlock)
1104-
tlsConfig.RootCAs = certPool
1118+
// Return the first certificate if we have one, or return nil so the
1119+
// server doesn't fail.
1120+
if len(certs) > 0 {
1121+
return &certs[0], nil
1122+
}
1123+
return nil, nil //nolint:nilnil
1124+
}
11051125

11061126
if tlsClientCAFile != "" {
11071127
caPool := x509.NewCertPool()

0 commit comments

Comments
 (0)