Skip to content

Commit 179f92d

Browse files
author
Jonathan Yu
authored
feat: make proxy settings configurable (#186)
* Add coderd proxy settings for outbound connections * Move utility functions into separate file * Extract LoadChart out of TestDeployment tests to avoid repeatedly loading the chart for parallel tests * Refactor Render function to accept a function closure, as with MustRender, to make the function signatures more consistent * Add utility function to convert hard-coded environment variables into a map
1 parent a3b9053 commit 179f92d

File tree

9 files changed

+252
-93
lines changed

9 files changed

+252
-93
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ View [our docs](https://coder.com/docs/setup/installation) for detailed installa
2525
| certs | object | Certificate that will be mounted inside Coder services. | `{"secret":{"key":"","name":""}}` |
2626
| certs.secret.key | string | Key pointing to a certificate in the secret. | `""` |
2727
| certs.secret.name | string | Name of the secret. | `""` |
28-
| coderd | object | Primary service responsible for all things Coder! | `{"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/name","operator":"In","values":["coderd"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":1}]}},"builtinProviderServiceAccount":{"annotations":{},"labels":{}},"devurlsHost":"","extraLabels":{},"image":"","oidc":{"enableRefresh":false,"redirectOptions":{}},"podSecurityContext":{"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000,"seccompProfile":{"type":"RuntimeDefault"}},"replicas":1,"resources":{"limits":{"cpu":"250m","memory":"512Mi"},"requests":{"cpu":"250m","memory":"512Mi"}},"satellite":{"accessURL":"","enable":false,"primaryURL":""},"securityContext":{"allowPrivilegeEscalation":false,"readOnlyRootFilesystem":true,"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000,"seccompProfile":{"type":"RuntimeDefault"}},"serviceAnnotations":{},"serviceNodePorts":{"http":null,"https":null},"serviceSpec":{"externalTrafficPolicy":"Local","loadBalancerIP":"","loadBalancerSourceRanges":[],"type":"LoadBalancer"},"superAdmin":{"passwordSecret":{"key":"password","name":""}},"tls":{"devurlsHostSecretName":"","hostSecretName":""},"trustProxyIP":false}` |
28+
| coderd | object | Primary service responsible for all things Coder! | `{"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/name","operator":"In","values":["coderd"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":1}]}},"builtinProviderServiceAccount":{"annotations":{},"labels":{}},"devurlsHost":"","extraLabels":{},"image":"","oidc":{"enableRefresh":false,"redirectOptions":{}},"podSecurityContext":{"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000,"seccompProfile":{"type":"RuntimeDefault"}},"proxy":{"exempt":"cluster.local","http":"","https":""},"replicas":1,"resources":{"limits":{"cpu":"250m","memory":"512Mi"},"requests":{"cpu":"250m","memory":"512Mi"}},"satellite":{"accessURL":"","enable":false,"primaryURL":""},"securityContext":{"allowPrivilegeEscalation":false,"readOnlyRootFilesystem":true,"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000,"seccompProfile":{"type":"RuntimeDefault"}},"serviceAnnotations":{},"serviceNodePorts":{"http":null,"https":null},"serviceSpec":{"externalTrafficPolicy":"Local","loadBalancerIP":"","loadBalancerSourceRanges":[],"type":"LoadBalancer"},"superAdmin":{"passwordSecret":{"key":"password","name":""}},"tls":{"devurlsHostSecretName":"","hostSecretName":""},"trustProxyIP":false}` |
2929
| coderd.affinity | object | Allows specifying an affinity rule for the `coderd` deployment. The default rule prefers to schedule coderd pods on different nodes, which is only applicable if coderd.replicas is greater than 1. | `{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/name","operator":"In","values":["coderd"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":1}]}}` |
3030
| coderd.builtinProviderServiceAccount | object | Customize the built-in Kubernetes provider service account. | `{"annotations":{},"labels":{}}` |
3131
| coderd.builtinProviderServiceAccount.annotations | object | A KV mapping of annotations. See: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ | `{}` |
@@ -38,6 +38,10 @@ View [our docs](https://coder.com/docs/setup/installation) for detailed installa
3838
| coderd.podSecurityContext.runAsNonRoot | bool | Requires that containers in the pod run as an unprivileged user. If setting runAsUser to 0 (root), this will need to be set to false. | `true` |
3939
| coderd.podSecurityContext.runAsUser | int | Sets the user id of the pod. For security reasons, we recommend using a non-root user. | `1000` |
4040
| coderd.podSecurityContext.seccompProfile | object | Sets the seccomp profile for the pod. If set, the container security context setting will take precedence over this value. | `{"type":"RuntimeDefault"}` |
41+
| coderd.proxy | object | Whether Coder should initiate outbound connections using a proxy. | `{"exempt":"cluster.local","http":"","https":""}` |
42+
| coderd.proxy.exempt | string | Bypass the configured proxy rules for this comma-delimited list of hosts or prefixes. This corresponds to the no_proxy environment variable. | `"cluster.local"` |
43+
| coderd.proxy.http | string | Proxy to use for HTTP connections. If unset, coderd will initiate HTTP connections directly. This corresponds to the http_proxy environment variable. | `""` |
44+
| coderd.proxy.https | string | Proxy to use for HTTPS connections. If this is not set, coderd will use the HTTP proxy (if set), otherwise it will initiate HTTPS connections directly. This corresponds to the https_proxy environment variable. | `""` |
4145
| coderd.replicas | int | The number of Kubernetes Pod replicas. | `1` |
4246
| coderd.resources | object | Kubernetes resource specification for coderd pods. To unset a value, set it to "". To unset all values, set resources to nil. | `{"limits":{"cpu":"250m","memory":"512Mi"},"requests":{"cpu":"250m","memory":"512Mi"}}` |
4347
| coderd.satellite | object | Deploy a satellite to geodistribute access to workspaces for lower latency. | `{"accessURL":"","enable":false,"primaryURL":""}` |

templates/coderd.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ spec:
7171
value: {{ .Values.coderd.devurlsHost | quote }}
7272
- name: VERBOSE
7373
value: "true"
74+
- name: http_proxy
75+
value: {{ .Values.coderd.proxy.http | quote }}
76+
- name: https_proxy
77+
value: {{ .Values.coderd.proxy.https | quote }}
78+
- name: no_proxy
79+
value: {{ .Values.coderd.proxy.exempt | quote }}
7480
{{- include "coder.postgres.env" . | indent 12 }}
7581
{{- include "coder.workspaces.configMapEnv" . | indent 12 }}
7682
command:
@@ -158,13 +164,19 @@ spec:
158164
{{- end }}
159165
- name: OIDC_REDIRECT_OPTIONS
160166
value: {{ toJson .Values.coderd.oidc.redirectOptions | quote }}
161-
{{- if ne .Values.coderd.superAdmin.passwordSecret.name "" }}
167+
{{- if .Values.coderd.superAdmin.passwordSecret.name }}
162168
- name: SUPER_ADMIN_PASSWORD
163169
valueFrom:
164170
secretKeyRef:
165171
name: {{ .Values.coderd.superAdmin.passwordSecret.name | quote }}
166172
key: {{ .Values.coderd.superAdmin.passwordSecret.key | quote }}
167173
{{- end }}
174+
- name: http_proxy
175+
value: {{ .Values.coderd.proxy.http | quote }}
176+
- name: https_proxy
177+
value: {{ .Values.coderd.proxy.https | quote }}
178+
- name: no_proxy
179+
value: {{ .Values.coderd.proxy.exempt | quote }}
168180
{{- include "coder.workspaces.configMapEnv" . | indent 12 }}
169181
{{- include "coder.postgres.env" . | indent 12 }}
170182
command:

tests/defaults_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestNamespace(t *testing.T) {
4141
t.Parallel()
4242

4343
// Render the chart with default values
44-
objs, err := chart.Render(chart.OriginalValues, &opts, nil)
44+
objs, err := chart.Render(nil, &opts, nil)
4545
require.NoError(t, err, "chart render failed")
4646

4747
// Verify that all objects are using the supplied namespace

tests/deployment_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
func TestDeployment(t *testing.T) {
1010
t.Parallel()
1111

12+
chart := LoadChart(t)
13+
1214
t.Run("Labels", func(t *testing.T) {
1315
var (
1416
expectedLabels = map[string]string{
@@ -21,7 +23,7 @@ func TestDeployment(t *testing.T) {
2123
"foo": "bar",
2224
}
2325

24-
objs = LoadChart(t).MustRender(t, func(cv *CoderValues) {
26+
objs = chart.MustRender(t, func(cv *CoderValues) {
2527
cv.Coderd.ExtraLabels = extraLabels
2628
})
2729
coderd = MustFindDeployment(t, objs, "coderd")

tests/ingress_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,7 @@ func TestIngress(t *testing.T) {
106106
copier.Copy(values, chart.OriginalValues)
107107

108108
// Run function to perform test-specific modifications of defaults
109-
test.ValuesFunc(values)
110-
111-
// Verify the results using AssertFunc
112-
objs, err := chart.Render(values, nil, nil)
113-
require.NoError(t, err, "chart render failed")
109+
objs := chart.MustRender(t, test.ValuesFunc)
114110

115111
var found bool
116112
for _, obj := range objs {

tests/proxy_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/utils/pointer"
9+
)
10+
11+
func TestProxy(t *testing.T) {
12+
t.Parallel()
13+
14+
chart := LoadChart(t)
15+
16+
tests := []struct {
17+
Name string
18+
ValuesFunc func(v *CoderValues)
19+
AssertFunc func(t testing.TB, spec *corev1.PodSpec)
20+
}{
21+
{
22+
Name: "default",
23+
ValuesFunc: nil,
24+
AssertFunc: func(t testing.TB, spec *corev1.PodSpec) {
25+
require.Len(t, spec.Containers, 1, "pod spec should have 1 container")
26+
vars := EnvVarsAsMap(spec.Containers[0].Env)
27+
require.Empty(t, vars["https_proxy"], "https_proxy should be empty")
28+
require.Empty(t, vars["http_proxy"], "http_proxy should be empty")
29+
require.Equal(t, "cluster.local", vars["no_proxy"], "no_proxy did not match")
30+
31+
require.Len(t, spec.InitContainers, 1, "pod spec should have 1 init container")
32+
vars = EnvVarsAsMap(spec.InitContainers[0].Env)
33+
require.Empty(t, vars["https_proxy"], "https_proxy should be empty")
34+
require.Empty(t, vars["http_proxy"], "http_proxy should be empty")
35+
require.Equal(t, "cluster.local", vars["no_proxy"], "no_proxy did not match")
36+
},
37+
},
38+
{
39+
Name: "all_proxy",
40+
ValuesFunc: func(v *CoderValues) {
41+
v.Coderd.Proxy.HTTPS = pointer.String("http://proxy.coder.com:3128")
42+
v.Coderd.Proxy.HTTP = pointer.String("https://proxy.coder.com:8888")
43+
v.Coderd.Proxy.Exempt = pointer.String("coder.com,coder.app")
44+
},
45+
AssertFunc: func(t testing.TB, spec *corev1.PodSpec) {
46+
require.Len(t, spec.Containers, 1, "pod spec should have 1 container")
47+
vars := EnvVarsAsMap(spec.Containers[0].Env)
48+
require.Equal(t, "http://proxy.coder.com:3128", vars["https_proxy"], "http_proxy did not match")
49+
require.Equal(t, "https://proxy.coder.com:8888", vars["http_proxy"], "https_proxy did not match")
50+
require.Equal(t, "coder.com,coder.app", vars["no_proxy"], "no_proxy did not match")
51+
52+
require.Len(t, spec.InitContainers, 1, "pod spec should have 1 init container")
53+
vars = EnvVarsAsMap(spec.InitContainers[0].Env)
54+
require.Equal(t, "http://proxy.coder.com:3128", vars["https_proxy"], "http_proxy did not match")
55+
require.Equal(t, "https://proxy.coder.com:8888", vars["http_proxy"], "https_proxy did not match")
56+
require.Equal(t, "coder.com,coder.app", vars["no_proxy"], "no_proxy did not match")
57+
},
58+
},
59+
}
60+
61+
for _, test := range tests {
62+
test := test
63+
64+
t.Run(test.Name, func(t *testing.T) {
65+
t.Parallel()
66+
67+
objs := chart.MustRender(t, test.ValuesFunc)
68+
deployment := MustFindDeployment(t, objs, "coderd")
69+
test.AssertFunc(t, &deployment.Spec.Template.Spec)
70+
})
71+
}
72+
}

tests/utils.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
appsv1 "k8s.io/api/apps/v1"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
)
10+
11+
// MustFindDeployment finds a deployment in the given slice of objects with the
12+
// given name, or fails the test.
13+
func MustFindDeployment(t testing.TB, objs []runtime.Object, name string) *appsv1.Deployment {
14+
names := []string{}
15+
for _, obj := range objs {
16+
if deployment, ok := obj.(*appsv1.Deployment); ok {
17+
if deployment.Name == name {
18+
return deployment
19+
}
20+
names = append(names, deployment.Name)
21+
}
22+
}
23+
24+
t.Fatalf("failed to find deployment %q, found %v", name, names)
25+
return nil
26+
}
27+
28+
// EnvVarsAsMap converts simple key/value environment variable pairs into a
29+
// map, ignoring variables using a ConfigMap or Secret source. If a variable
30+
// is defined multiple times, the last value will be returned.
31+
func EnvVarsAsMap(variables []corev1.EnvVar) map[string]string {
32+
values := map[string]string{}
33+
34+
for _, v := range variables {
35+
if v.ValueFrom != nil {
36+
continue
37+
}
38+
39+
values[v.Name] = v.Value
40+
}
41+
42+
return values
43+
}
44+
45+
// AssertVolume asserts that a volume exists of the given name in the given
46+
// slice of volumes. If it exists, it also runs fn against the named volume.
47+
func AssertVolume(t testing.TB, vols []corev1.Volume, name string, fn func(t testing.TB, v corev1.Volume)) {
48+
names := []string{}
49+
for _, v := range vols {
50+
if v.Name == name {
51+
fn(t, v)
52+
return
53+
}
54+
names = append(names, v.Name)
55+
}
56+
57+
t.Fatalf("failed to find volume %q, found %v", name, names)
58+
}
59+
60+
// AssertVolumeMount asserts that a volume mount exists of the given name in the
61+
// given slice of volume mounts. If it exists, it also runs fn against the named
62+
// volume mount.
63+
func AssertVolumeMount(t testing.TB, vols []corev1.VolumeMount, name string, fn func(t testing.TB, v corev1.VolumeMount)) {
64+
names := []string{}
65+
for _, v := range vols {
66+
if v.Name == name {
67+
fn(t, v)
68+
return
69+
}
70+
names = append(names, v.Name)
71+
}
72+
73+
t.Fatalf("failed to find volume mount %q, found %v", name, names)
74+
}
75+
76+
// AssertContainer asserts that a container exists of the given name in the
77+
// given slice of containers. If it exists, it also runs fn against the named
78+
// container.
79+
func AssertContainer(t testing.TB, cnts []corev1.Container, name string, fn func(t testing.TB, v corev1.Container)) {
80+
names := []string{}
81+
for _, c := range cnts {
82+
if c.Name == name {
83+
fn(t, c)
84+
return
85+
}
86+
names = append(names, c.Name)
87+
}
88+
89+
t.Fatalf("failed to find container %q, found %v", name, names)
90+
}

0 commit comments

Comments
 (0)