mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 09:54:49 +00:00
fix(swarm): allow manager operation header in reverse proxy [BE-13066] (#3174)
This commit is contained in:
@@ -7,25 +7,29 @@ import (
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libstack/swarm"
|
||||
)
|
||||
|
||||
// Note that we discard any non-canonical headers by design
|
||||
var allowedHeaders = map[string]struct{}{
|
||||
"Accept": {},
|
||||
"Accept-Encoding": {},
|
||||
"Accept-Language": {},
|
||||
"Cache-Control": {},
|
||||
"Connection": {},
|
||||
"Content-Length": {},
|
||||
"Content-Type": {},
|
||||
"Private-Token": {},
|
||||
"Upgrade": {},
|
||||
"User-Agent": {},
|
||||
"X-Portaineragent-Target": {},
|
||||
"X-Portainer-Volumename": {},
|
||||
"X-Registry-Auth": {},
|
||||
"X-Registry-Config": {},
|
||||
"X-Stream-Protocol-Version": {},
|
||||
"Accept": {},
|
||||
"Accept-Encoding": {},
|
||||
"Accept-Language": {},
|
||||
"Cache-Control": {},
|
||||
"Connection": {},
|
||||
"Content-Length": {},
|
||||
"Content-Type": {},
|
||||
"Private-Token": {},
|
||||
"Upgrade": {},
|
||||
"User-Agent": {},
|
||||
|
||||
"X-Portaineragent-Target": {},
|
||||
http.CanonicalHeaderKey(swarm.ManagerOperationHeader): {},
|
||||
"X-Portainer-Volumename": {},
|
||||
"X-Registry-Auth": {},
|
||||
"X-Registry-Config": {},
|
||||
"X-Stream-Protocol-Version": {},
|
||||
|
||||
// WebSocket headers those are required for kubectl exec/attach/port-forward operations
|
||||
"Sec-Websocket-Key": {},
|
||||
"Sec-Websocket-Version": {},
|
||||
|
||||
@@ -65,22 +65,23 @@ func Test_createRewriteFn(t *testing.T) {
|
||||
"GET",
|
||||
"https://agent-portainer.io/test?c=7",
|
||||
map[string]string{
|
||||
"Authorization": "secret",
|
||||
"Proxy-Authorization": "secret",
|
||||
"Cookie": "secret",
|
||||
"X-Csrf-Token": "secret",
|
||||
"X-Api-Key": "secret",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Accept-Language": "en-GB",
|
||||
"Cache-Control": "None",
|
||||
"Content-Length": "100",
|
||||
"Content-Type": "application/json",
|
||||
"Private-Token": "test-private-token",
|
||||
"User-Agent": "test-user-agent",
|
||||
"X-Portaineragent-Target": "test-agent-1",
|
||||
"X-Portainer-Volumename": "test-volume-1",
|
||||
"X-Registry-Auth": "test-registry-auth",
|
||||
"Authorization": "secret",
|
||||
"Proxy-Authorization": "secret",
|
||||
"Cookie": "secret",
|
||||
"X-Csrf-Token": "secret",
|
||||
"X-Api-Key": "secret",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Accept-Language": "en-GB",
|
||||
"Cache-Control": "None",
|
||||
"Content-Length": "100",
|
||||
"Content-Type": "application/json",
|
||||
"Private-Token": "test-private-token",
|
||||
"User-Agent": "test-user-agent",
|
||||
"X-Portaineragent-Target": "test-agent-1",
|
||||
"X-Portaineragent-Manageroperation": "1",
|
||||
"X-Portainer-Volumename": "test-volume-1",
|
||||
"X-Registry-Auth": "test-registry-auth",
|
||||
},
|
||||
true,
|
||||
),
|
||||
@@ -89,17 +90,18 @@ func Test_createRewriteFn(t *testing.T) {
|
||||
"GET",
|
||||
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
|
||||
map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Accept-Language": "en-GB",
|
||||
"Cache-Control": "None",
|
||||
"Content-Length": "100",
|
||||
"Content-Type": "application/json",
|
||||
"Private-Token": "test-private-token",
|
||||
"User-Agent": "test-user-agent",
|
||||
"X-Portaineragent-Target": "test-agent-1",
|
||||
"X-Portainer-Volumename": "test-volume-1",
|
||||
"X-Registry-Auth": "test-registry-auth",
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Accept-Language": "en-GB",
|
||||
"Cache-Control": "None",
|
||||
"Content-Length": "100",
|
||||
"Content-Type": "application/json",
|
||||
"Private-Token": "test-private-token",
|
||||
"User-Agent": "test-user-agent",
|
||||
"X-Portaineragent-Target": "test-agent-1",
|
||||
"X-Portaineragent-Manageroperation": "1",
|
||||
"X-Portainer-Volumename": "test-volume-1",
|
||||
"X-Registry-Auth": "test-registry-auth",
|
||||
},
|
||||
true,
|
||||
),
|
||||
|
||||
@@ -32,10 +32,10 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// managerOperationHeader forces the Portainer agent to route requests to a swarm
|
||||
// ManagerOperationHeader forces the Portainer agent to route requests to a swarm
|
||||
// manager node. Swarm-scoped operations (e.g. network create/remove) fail on worker
|
||||
// nodes without it. This constant is originally defined in the agent, see package/agent/README.md.
|
||||
const managerOperationHeader = "X-PortainerAgent-ManagerOperation"
|
||||
const ManagerOperationHeader = "X-PortainerAgent-ManagerOperation"
|
||||
|
||||
// cliOptions builds the Docker CLI options for swarm operations, always setting the
|
||||
// manager-operation header so requests proxied through an agent target a manager node.
|
||||
@@ -43,7 +43,7 @@ func cliOptions(host string, registries []configtypes.AuthConfig) libstack.Docke
|
||||
return libstack.DockerCliOptions{
|
||||
Host: host,
|
||||
Registries: registries,
|
||||
Headers: map[string]string{managerOperationHeader: "1"},
|
||||
Headers: map[string]string{ManagerOperationHeader: "1"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,13 @@ func Test_cliOptions(t *testing.T) {
|
||||
expected: libstack.DockerCliOptions{
|
||||
Host: "tcp://127.0.0.1:2377",
|
||||
Registries: registries,
|
||||
Headers: map[string]string{managerOperationHeader: "1"},
|
||||
Headers: map[string]string{ManagerOperationHeader: "1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty host and nil registries still set the header",
|
||||
expected: libstack.DockerCliOptions{
|
||||
Headers: map[string]string{managerOperationHeader: "1"},
|
||||
Headers: map[string]string{ManagerOperationHeader: "1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user