diff --git a/api/common_unix.go b/api/common_unix.go deleted file mode 100644 index 0f77352d7e..0000000000 --- a/api/common_unix.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !windows - -package api // import "github.com/docker/docker/api" - -// MinVersion represents Minimum REST API version supported -const MinVersion = "1.12" diff --git a/api/common_windows.go b/api/common_windows.go deleted file mode 100644 index 590ba5479b..0000000000 --- a/api/common_windows.go +++ /dev/null @@ -1,8 +0,0 @@ -package api // import "github.com/docker/docker/api" - -// MinVersion represents Minimum REST API version supported -// Technically the first daemon API version released on Windows is v1.25 in -// engine version 1.13. However, some clients are explicitly using downlevel -// APIs (e.g. docker-compose v2.1 file format) and that is just too restrictive. -// Hence also allowing 1.24 on Windows. -const MinVersion string = "1.24" diff --git a/api/server/server_test.go b/api/server/server_test.go index 8b71e9fce6..65dda95c9c 100644 --- a/api/server/server_test.go +++ b/api/server/server_test.go @@ -15,7 +15,8 @@ import ( func TestMiddlewares(t *testing.T) { srv := &Server{} - srv.UseMiddleware(middleware.NewVersionMiddleware("0.1omega2", api.DefaultVersion, api.MinVersion)) + const apiMinVersion = "1.12" + srv.UseMiddleware(middleware.NewVersionMiddleware("0.1omega2", api.DefaultVersion, apiMinVersion)) req, _ := http.NewRequest(http.MethodGet, "/containers/json", nil) resp := httptest.NewRecorder() diff --git a/client/client.go b/client/client.go index 6fb3f3eb04..0b496b0fa6 100644 --- a/client/client.go +++ b/client/client.go @@ -90,6 +90,13 @@ import ( // [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569 const DummyHost = "api.moby.localhost" +// fallbackAPIVersion is the version to fallback to if API-version negotiation +// fails. This version is the highest version of the API before API-version +// negotiation was introduced. If negotiation fails (or no API version was +// included in the API response), we assume the API server uses the most +// recent version before negotiation was introduced. +const fallbackAPIVersion = "1.24" + // Client is the API client that performs all operations // against a docker server. type Client struct { @@ -329,7 +336,7 @@ func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) { func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) { // default to the latest version before versioning headers existed if pingResponse.APIVersion == "" { - pingResponse.APIVersion = "1.24" + pingResponse.APIVersion = fallbackAPIVersion } // if the client is not initialized with a version, start with the latest supported version diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 4015fb2bc9..5a9b132557 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -520,6 +520,20 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { conf.LogLevel = opts.LogLevel conf.LogFormat = log.OutputFormat(opts.LogFormat) + // The DOCKER_MIN_API_VERSION env-var allows overriding the minimum API + // version provided by the daemon within constraints of the minimum and + // maximum (current) supported API versions. + // + // API versions older than [config.defaultMinAPIVersion] are deprecated and + // to be removed in a future release. The "DOCKER_MIN_API_VERSION" env-var + // should only be used for exceptional cases. + if ver := os.Getenv("DOCKER_MIN_API_VERSION"); ver != "" { + if err := config.ValidateMinAPIVersion(ver); err != nil { + return nil, errors.Wrap(err, "invalid DOCKER_MIN_API_VERSION") + } + conf.MinAPIVersion = ver + } + if flags.Changed(FlagTLS) { conf.TLS = &opts.TLS } @@ -689,7 +703,7 @@ func initMiddlewares(s *apiserver.Server, cfg *config.Config, pluginStore plugin exp := middleware.NewExperimentalMiddleware(cfg.Experimental) s.UseMiddleware(exp) - vm := middleware.NewVersionMiddleware(v, api.DefaultVersion, api.MinVersion) + vm := middleware.NewVersionMiddleware(v, api.DefaultVersion, cfg.MinAPIVersion) s.UseMiddleware(vm) if cfg.CorsHeaders != "" { diff --git a/daemon/config/config.go b/daemon/config/config.go index 761f242fd3..33879a977d 100644 --- a/daemon/config/config.go +++ b/daemon/config/config.go @@ -10,16 +10,17 @@ import ( "os" "strings" - "golang.org/x/text/encoding" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" - "github.com/containerd/log" + "github.com/docker/docker/api" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/opts" "github.com/docker/docker/registry" "github.com/imdario/mergo" "github.com/pkg/errors" "github.com/spf13/pflag" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" ) const ( @@ -53,7 +54,11 @@ const ( DefaultContainersNamespace = "moby" // DefaultPluginNamespace is the name of the default containerd namespace used for plugins. DefaultPluginNamespace = "plugins.moby" - + // defaultMinAPIVersion is the minimum API version supported by the API. + // This version can be overridden through the "DOCKER_MIN_API_VERSION" + // environment variable. The minimum allowed version is determined + // by [minAPIVersion]. + defaultMinAPIVersion = "1.24" // SeccompProfileDefault is the built-in default seccomp profile. SeccompProfileDefault = "builtin" // SeccompProfileUnconfined is a special profile name for seccomp to use an @@ -247,6 +252,17 @@ type CommonConfig struct { // CDISpecDirs is a list of directories in which CDI specifications can be found. CDISpecDirs []string `json:"cdi-spec-dirs,omitempty"` + + // The minimum API version provided by the daemon. Defaults to [defaultMinAPIVersion]. + // + // The DOCKER_MIN_API_VERSION allows overriding the minimum API version within + // constraints of the minimum and maximum (current) supported API versions. + // + // API versions older than [defaultMinAPIVersion] are deprecated and + // to be removed in a future release. The "DOCKER_MIN_API_VERSION" env + // var should only be used for exceptional cases, and the MinAPIVersion + // field is therefore not included in the JSON representation. + MinAPIVersion string `json:"-"` } // Proxies holds the proxies that are configured for the daemon. @@ -290,6 +306,7 @@ func New() (*Config, error) { ContainerdNamespace: DefaultContainersNamespace, ContainerdPluginNamespace: DefaultPluginNamespace, DefaultRuntime: StockRuntimeName, + MinAPIVersion: defaultMinAPIVersion, }, } @@ -583,6 +600,25 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag return nil } +// ValidateMinAPIVersion verifies if the given API version is within the +// range supported by the daemon. It is used to validate a custom minimum +// API version set through DOCKER_MIN_API_VERSION. +func ValidateMinAPIVersion(ver string) error { + if ver == "" { + return errors.New(`value is empty`) + } + if strings.EqualFold(ver[0:1], "v") { + return errors.New(`API version must be provided without "v" prefix`) + } + if versions.LessThan(ver, minAPIVersion) { + return errors.Errorf(`minimum supported API version is %s: %s`, minAPIVersion, ver) + } + if versions.GreaterThan(ver, api.DefaultVersion) { + return errors.Errorf(`maximum supported API version is %s: %s`, api.DefaultVersion, ver) + } + return nil +} + // Validate validates some specific configs. // such as config.DNS, config.Labels, config.DNSSearch, // as well as config.MaxConcurrentDownloads, config.MaxConcurrentUploads and config.MaxDownloadAttempts. diff --git a/daemon/config/config_linux.go b/daemon/config/config_linux.go index 39f2d16610..9cc5a55bdf 100644 --- a/daemon/config/config_linux.go +++ b/daemon/config/config_linux.go @@ -29,6 +29,9 @@ const ( // StockRuntimeName is the reserved name/alias used to represent the // OCI runtime being shipped with the docker daemon package. StockRuntimeName = "runc" + + // minAPIVersion represents Minimum REST API version supported + minAPIVersion = "1.12" ) // BridgeConfig stores all the parameters for both the bridge driver and the default bridge network. diff --git a/daemon/config/config_test.go b/daemon/config/config_test.go index b6c362206d..39768875fc 100644 --- a/daemon/config/config_test.go +++ b/daemon/config/config_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api" "github.com/docker/docker/libnetwork/ipamutils" "github.com/docker/docker/opts" "github.com/google/go-cmp/cmp" @@ -502,6 +503,62 @@ func TestValidateConfiguration(t *testing.T) { } } +func TestValidateMinAPIVersion(t *testing.T) { + t.Parallel() + tests := []struct { + doc string + input string + expectedErr string + }{ + { + doc: "empty", + expectedErr: "value is empty", + }, + { + doc: "with prefix", + input: "v1.43", + expectedErr: `API version must be provided without "v" prefix`, + }, + { + doc: "major only", + input: "1", + expectedErr: `minimum supported API version is`, + }, + { + doc: "too low", + input: "1.0", + expectedErr: `minimum supported API version is`, + }, + { + doc: "minor too high", + input: "1.99", + expectedErr: `maximum supported API version is`, + }, + { + doc: "major too high", + input: "9.0", + expectedErr: `maximum supported API version is`, + }, + { + doc: "current version", + input: api.DefaultVersion, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.doc, func(t *testing.T) { + err := ValidateMinAPIVersion(tc.input) + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } else { + assert.Check(t, err) + } + }) + } + +} + func TestConfigInvalidDNS(t *testing.T) { tests := []struct { doc string diff --git a/daemon/config/config_windows.go b/daemon/config/config_windows.go index 05e185da88..825b150f85 100644 --- a/daemon/config/config_windows.go +++ b/daemon/config/config_windows.go @@ -13,6 +13,13 @@ const ( // default value. On Windows keep this empty so the value is auto-detected // based on other options. StockRuntimeName = "" + + // minAPIVersion represents Minimum REST API version supported + // Technically the first daemon API version released on Windows is v1.25 in + // engine version 1.13. However, some clients are explicitly using downlevel + // APIs (e.g. docker-compose v2.1 file format) and that is just too restrictive. + // Hence also allowing 1.24 on Windows. + minAPIVersion string = "1.24" ) // BridgeConfig is meant to store all the parameters for both the bridge driver and the default bridge network. On diff --git a/daemon/info.go b/daemon/info.go index a2668420ff..2705cd5ec8 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -113,7 +113,7 @@ func (daemon *Daemon) SystemVersion(ctx context.Context) (types.Version, error) Details: map[string]string{ "GitCommit": dockerversion.GitCommit, "ApiVersion": api.DefaultVersion, - "MinAPIVersion": api.MinVersion, + "MinAPIVersion": cfg.MinAPIVersion, "GoVersion": runtime.Version(), "Os": runtime.GOOS, "Arch": runtime.GOARCH, @@ -128,7 +128,7 @@ func (daemon *Daemon) SystemVersion(ctx context.Context) (types.Version, error) Version: dockerversion.Version, GitCommit: dockerversion.GitCommit, APIVersion: api.DefaultVersion, - MinAPIVersion: api.MinVersion, + MinAPIVersion: cfg.MinAPIVersion, GoVersion: runtime.Version(), Os: runtime.GOOS, Arch: runtime.GOARCH, diff --git a/hack/make/.integration-daemon-start b/hack/make/.integration-daemon-start index e667d35e86..3e9131daab 100644 --- a/hack/make/.integration-daemon-start +++ b/hack/make/.integration-daemon-start @@ -46,6 +46,9 @@ export DOCKER_ALLOW_SCHEMA1_PUSH_DONOTUSE=1 export DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} export DOCKER_USERLANDPROXY=${DOCKER_USERLANDPROXY:-true} +# Allow testing old API versions +export DOCKER_MIN_API_VERSION=1.12 + # example usage: DOCKER_STORAGE_OPTS="dm.basesize=20G,dm.loopdatasize=200G" storage_params="" if [ -n "$DOCKER_STORAGE_OPTS" ]; then