Skip to content

Commit 8992378

Browse files
committed
add support for DOCKER_CUSTOM_HEADERS env-var (experimental)
This environment variable allows for setting additional headers to be sent by the client. Headers set through this environment variable are added to headers set through the config-file (through the HttpHeaders field). This environment variable can be used in situations where headers must be set for a specific invocation of the CLI, but should not be set by default, and therefore cannot be set in the config-file. WARNING: If both config and environment-variable are set, the environment variable currently overrides all headers set in the configuration file. This behavior may change in a future update, as we are considering the environment variable to be appending to existing headers (and to only override headers with the same name). Signed-off-by: Sebastiaan van Stijn <[email protected]> (cherry picked from commit 6638deb) Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent f90273c commit 8992378

File tree

5 files changed

+153
-5
lines changed

5 files changed

+153
-5
lines changed

cli/command/cli.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
324324
if len(configFile.HTTPHeaders) > 0 {
325325
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
326326
}
327-
opts = append(opts, client.WithUserAgent(UserAgent()))
327+
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
328328
return client.NewClientWithOpts(opts...)
329329
}
330330

cli/command/cli_options.go

+109
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package command
22

33
import (
44
"context"
5+
"encoding/csv"
56
"io"
7+
"net/http"
68
"os"
79
"strconv"
10+
"strings"
811

912
"github.com/docker/cli/cli/streams"
1013
"github.com/docker/docker/client"
14+
"github.com/docker/docker/errdefs"
1115
"github.com/moby/term"
16+
"github.com/pkg/errors"
1217
)
1318

1419
// CLIOption is a functional argument to apply options to a [DockerCli]. These
@@ -108,3 +113,107 @@ func WithAPIClient(c client.APIClient) CLIOption {
108113
return nil
109114
}
110115
}
116+
117+
// envOverrideHTTPHeaders is the name of the environment-variable that can be
118+
// used to set custom HTTP headers to be sent by the client. This environment
119+
// variable is the equivalent to the HttpHeaders field in the configuration
120+
// file.
121+
//
122+
// WARNING: If both config and environment-variable are set, the environment
123+
// variable currently overrides all headers set in the configuration file.
124+
// This behavior may change in a future update, as we are considering the
125+
// environment variable to be appending to existing headers (and to only
126+
// override headers with the same name).
127+
//
128+
// While this env-var allows for custom headers to be set, it does not allow
129+
// for built-in headers (such as "User-Agent", if set) to be overridden.
130+
// Also see [client.WithHTTPHeaders] and [client.WithUserAgent].
131+
//
132+
// This environment variable can be used in situations where headers must be
133+
// set for a specific invocation of the CLI, but should not be set by default,
134+
// and therefore cannot be set in the config-file.
135+
//
136+
// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs,
137+
// where key must be a non-empty, valid MIME header format. Whitespaces surrounding
138+
// the key are trimmed, and the key is normalised. Whitespaces in values are
139+
// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored.
140+
// Tuples without a "=" produce an error.
141+
//
142+
// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted
143+
// if they must contain commas, which allows for multiple values for a single
144+
// header to be set. If a key is repeated in the list, later values override
145+
// prior values.
146+
//
147+
// For example, the following value:
148+
//
149+
// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two
150+
//
151+
// Produces four headers (four is omitted as it has an empty value set):
152+
//
153+
// - one (value is "one-value")
154+
// - two (value is "two,value")
155+
// - three (value is " a value with whitespace ")
156+
// - five (value is "five-two", the later value has overridden the prior value)
157+
const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
158+
159+
// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the
160+
// client through the [envOverrideHTTPHeaders] environment-variable. This
161+
// environment variable is the equivalent to the HttpHeaders field in the
162+
// configuration file.
163+
//
164+
// WARNING: If both config and environment-variable are set, the environment-
165+
// variable currently overrides all headers set in the configuration file.
166+
// This behavior may change in a future update, as we are considering the
167+
// environment-variable to be appending to existing headers (and to only
168+
// override headers with the same name).
169+
//
170+
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
171+
func withCustomHeadersFromEnv() client.Opt {
172+
return func(apiClient *client.Client) error {
173+
value := os.Getenv(envOverrideHTTPHeaders)
174+
if value == "" {
175+
return nil
176+
}
177+
csvReader := csv.NewReader(strings.NewReader(value))
178+
fields, err := csvReader.Read()
179+
if err != nil {
180+
return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders))
181+
}
182+
if len(fields) == 0 {
183+
return nil
184+
}
185+
186+
env := map[string]string{}
187+
for _, kv := range fields {
188+
k, v, hasValue := strings.Cut(kv, "=")
189+
190+
// Only strip whitespace in keys; preserve whitespace in values.
191+
k = strings.TrimSpace(k)
192+
193+
if k == "" {
194+
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv))
195+
}
196+
197+
// We don't currently allow empty key=value pairs, and produce an error.
198+
// This is something we could allow in future (e.g. to read value
199+
// from an environment variable with the same name). In the meantime,
200+
// produce an error to prevent users from depending on this.
201+
if !hasValue {
202+
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
203+
}
204+
205+
env[http.CanonicalHeaderKey(k)] = v
206+
}
207+
208+
if len(env) == 0 {
209+
// We should probably not hit this case, as we don't skip values
210+
// (only return errors), but we don't want to discard existing
211+
// headers with an empty set.
212+
return nil
213+
}
214+
215+
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
216+
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
217+
return client.WithHTTPHeaders(env)(apiClient)
218+
}
219+
}

cli/command/cli_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,41 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
8787
assert.DeepEqual(t, received, expectedHeaders)
8888
}
8989

90+
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
91+
var received http.Header
92+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93+
received = r.Header.Clone()
94+
_, _ = w.Write([]byte("OK"))
95+
}))
96+
defer ts.Close()
97+
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
98+
opts := &flags.ClientOptions{Hosts: []string{host}}
99+
configFile := &configfile.ConfigFile{
100+
HTTPHeaders: map[string]string{
101+
"My-Header": "Custom-Value from config-file",
102+
},
103+
}
104+
105+
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
106+
// so "My-Header" should not be present.
107+
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
108+
apiClient, err := NewAPIClientFromFlags(opts, configFile)
109+
assert.NilError(t, err)
110+
assert.Equal(t, apiClient.DaemonHost(), host)
111+
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
112+
113+
expectedHeaders := http.Header{
114+
"One": []string{"one-value"},
115+
"Two": []string{"two,value"},
116+
"Three": []string{""},
117+
"Four": []string{"four-value-override"},
118+
"User-Agent": []string{UserAgent()},
119+
}
120+
_, err = apiClient.Ping(context.Background())
121+
assert.NilError(t, err)
122+
assert.DeepEqual(t, received, expectedHeaders)
123+
}
124+
90125
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
91126
customVersion := "v3.3.3"
92127
t.Setenv("DOCKER_API_VERSION", customVersion)

docs/reference/commandline/docker.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,14 @@ The following list of environment variables are supported by the `docker` comman
118118
line:
119119

120120
| Variable | Description |
121-
| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121+
| :---------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
122122
| `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) |
123123
| `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) |
124124
| `DOCKER_CONFIG` | The location of your client configuration files. |
125125
| `DOCKER_CONTENT_TRUST_SERVER` | The URL of the Notary server to use. Defaults to the same URL as the registry. |
126126
| `DOCKER_CONTENT_TRUST` | When set Docker uses notary to sign and verify images. Equates to `--disable-content-trust=false` for build, create, pull, push, run. |
127127
| `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) |
128+
| `DOCKER_CUSTOM_HEADERS` | (Experimental) Configure [custom HTTP headers](#custom-http-headers) to be sent by the client. Headers must be provided as a comma-separated list of `name=value` pairs. This is the equivalent to the `HttpHeaders` field in the configuration file. |
128129
| `DOCKER_DEFAULT_PLATFORM` | Default platform for commands that take the `--platform` flag. |
129130
| `DOCKER_HIDE_LEGACY_COMMANDS` | When set, Docker hides "legacy" top-level commands (such as `docker rm`, and `docker pull`) in `docker help` output, and only `Management commands` per object-type (e.g., `docker container`) are printed. This may become the default in a future release. |
130131
| `DOCKER_HOST` | Daemon socket to connect to. |
@@ -281,6 +282,10 @@ sent from the Docker client to the daemon. Docker doesn't try to interpret or
281282
understand these headers; it simply puts them into the messages. Docker does
282283
not allow these headers to change any headers it sets for itself.
283284

285+
Alternatively, use the `DOCKER_CUSTOM_HEADERS` [environment variable](#environment-variables),
286+
which is available in v27.1 and higher. This environment-variable is experimental,
287+
and its exact behavior may change.
288+
284289
#### Credential store options
285290

286291
The property `credsStore` specifies an external binary to serve as the default

docs/reference/dockerd.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,8 @@ to [the `daemon.json` file](#daemon-configuration-file).
133133

134134
The following list of environment variables are supported by the `dockerd` daemon.
135135
Some of these environment variables are supported both by the Docker Daemon and
136-
the `docker` CLI. Refer to [Environment variables](https://docs.docker.com/reference/cli/docker/#environment-variables)
137-
in the CLI section to learn about environment variables supported by the
138-
`docker` CLI.
136+
the `docker` CLI. Refer to [Environment variables](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
137+
to learn about environment variables supported by the `docker` CLI.
139138

140139
| Variable | Description |
141140
| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

0 commit comments

Comments
 (0)