feat: Add deployment side config-ssh options (#6613)

* feat: Allow setting deployment wide ssh config settings
* feat: config-ssh respects deployment ssh config
* The '.' is now configurable
* Move buildinfo into deployment.go
This commit is contained in:
Steven Masley
2023-03-16 13:03:37 -05:00
committed by GitHub
parent 25e8abd63e
commit fe247c86eb
18 changed files with 642 additions and 49 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"flag"
"fmt"
"math"
"net/http"
"os"
@ -160,6 +161,7 @@ type DeploymentValues struct {
DisablePasswordAuth clibase.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"`
Support SupportConfig `json:"support,omitempty" typescript:",notnull"`
GitAuthProviders clibase.Struct[[]GitAuthConfig] `json:"git_auth,omitempty" typescript:",notnull"`
SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"`
Config clibase.String `json:"config,omitempty" typescript:",notnull"`
WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"`
@ -168,6 +170,40 @@ type DeploymentValues struct {
Address clibase.HostPort `json:"address,omitempty" typescript:",notnull"`
}
// SSHConfig is configuration the cli & vscode extension use for configuring
// ssh connections.
type SSHConfig struct {
// DeploymentName is the config-ssh Hostname prefix
DeploymentName clibase.String
// SSHConfigOptions are additional options to add to the ssh config file.
// This will override defaults.
SSHConfigOptions clibase.Strings
}
func (c SSHConfig) ParseOptions() (map[string]string, error) {
m := make(map[string]string)
for _, opt := range c.SSHConfigOptions {
key, value, err := ParseSSHConfigOption(opt)
if err != nil {
return nil, err
}
m[key] = value
}
return m, nil
}
// ParseSSHConfigOption parses a single ssh config option into it's key/value pair.
func ParseSSHConfigOption(opt string) (key string, value string, err error) {
// An equal sign or whitespace is the separator between the key and value.
idx := strings.IndexFunc(opt, func(r rune) bool {
return r == ' ' || r == '='
})
if idx == -1 {
return "", "", fmt.Errorf("invalid config-ssh option %q", opt)
}
return opt[:idx], opt[idx+1:], nil
}
type DERP struct {
Server DERPServerConfig `json:"server" typescript:",notnull"`
Config DERPConfig `json:"config" typescript:",notnull"`
@ -390,6 +426,11 @@ when required by your organization's security policy.`,
deploymentGroupDangerous = clibase.Group{
Name: "⚠️ Dangerous",
}
deploymentGroupClient = clibase.Group{
Name: "Client",
Description: "These options change the behavior of how clients interact with the Coder. " +
"Clients include the coder cli, vs code extension, and the web UI.",
}
deploymentGroupConfig = clibase.Group{
Name: "Config",
Description: `Use a YAML configuration file when your server launch become unwieldy.`,
@ -1265,6 +1306,29 @@ when required by your organization's security policy.`,
Group: &deploymentGroupConfig,
Value: &c.Config,
},
{
Name: "SSH Host Prefix",
Description: "The SSH deployment prefix is used in the Host of the ssh config.",
Flag: "ssh-hostname-prefix",
Env: "SSH_HOSTNAME_PREFIX",
YAML: "sshHostnamePrefix",
Group: &deploymentGroupClient,
Value: &c.SSHConfig.DeploymentName,
Hidden: false,
Default: "coder.",
},
{
Name: "SSH Config Options",
Description: "These SSH config options will override the default SSH config options. " +
"Provide options in \"key=value\" or \"key value\" format separated by commas." +
"Using this incorrectly can break SSH to your deployment, use cautiously.",
Flag: "ssh-config-options",
Env: "SSH_CONFIG_OPTIONS",
YAML: "sshConfigOptions",
Group: &deploymentGroupClient,
Value: &c.SSHConfig.SSHConfigOptions,
Hidden: false,
},
{
Name: "Write Config",
Description: `
@ -1580,3 +1644,25 @@ type DeploymentStats struct {
Workspaces WorkspaceDeploymentStats `json:"workspaces"`
SessionCount SessionCountDeploymentStats `json:"session_count"`
}
type SSHConfigResponse struct {
HostnamePrefix string `json:"hostname_prefix"`
SSHConfigOptions map[string]string `json:"ssh_config_options"`
}
// SSHConfiguration returns information about the SSH configuration for the
// Coder instance.
func (c *Client) SSHConfiguration(ctx context.Context) (SSHConfigResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/deployment/ssh", nil)
if err != nil {
return SSHConfigResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return SSHConfigResponse{}, ReadBodyAsError(res)
}
var sshConfig SSHConfigResponse
return sshConfig, json.NewDecoder(res.Body).Decode(&sshConfig)
}

View File

@ -3,6 +3,9 @@ package codersdk_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/codersdk"
)
@ -105,3 +108,82 @@ func TestDeploymentValues_HighlyConfigurable(t *testing.T) {
t.Errorf("Excluded option %q is not in the deployment config. Remove it?", opt)
}
}
func TestSSHConfig_ParseOptions(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
ConfigOptions clibase.Strings
ExpectError bool
Expect map[string]string
}{
{
Name: "Empty",
ConfigOptions: []string{},
Expect: map[string]string{},
},
{
Name: "Whitespace",
ConfigOptions: []string{
"test value",
},
Expect: map[string]string{
"test": "value",
},
},
{
Name: "SimpleValueEqual",
ConfigOptions: []string{
"test=value",
},
Expect: map[string]string{
"test": "value",
},
},
{
Name: "SimpleValues",
ConfigOptions: []string{
"test=value",
"foo=bar",
},
Expect: map[string]string{
"test": "value",
"foo": "bar",
},
},
{
Name: "ValueWithQuote",
ConfigOptions: []string{
"bar=buzz=bazz",
},
Expect: map[string]string{
"bar": "buzz=bazz",
},
},
{
Name: "NoEquals",
ConfigOptions: []string{
"foobar",
},
ExpectError: true,
},
}
for _, tt := range testCases {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
c := codersdk.SSHConfig{
SSHConfigOptions: tt.ConfigOptions,
}
got, err := c.ParseOptions()
if tt.ExpectError {
require.Error(t, err, tt.ConfigOptions.String())
} else {
require.NoError(t, err, tt.ConfigOptions.String())
require.Equalf(t, tt.Expect, got, tt.ConfigOptions.String())
}
})
}
}