Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3deb85b0da | ||
|
|
e115055a1b | ||
|
|
384cb53c64 | ||
|
|
4240cbf029 | ||
|
|
eb28dd4f4e | ||
|
|
78127f8f3d | ||
|
|
c474322889 | ||
|
|
83527da1a8 | ||
|
|
7c8bef84b1 | ||
|
|
5b3dba130b | ||
|
|
4039c3a693 | ||
|
|
b1dceb15e4 | ||
|
|
2feaacddb9 | ||
|
|
65e0344975 | ||
|
|
915beecce3 |
@@ -60,6 +60,7 @@ func CLIFlags() *portainer.CLIFlags {
|
||||
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
|
||||
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
|
||||
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
|
||||
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,21 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultPullLimitCheckDisabled = "false"
|
||||
)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultPullLimitCheckDisabled = "false"
|
||||
)
|
||||
|
||||
@@ -575,6 +575,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
AdminCreationDone: adminCreationDone,
|
||||
PendingActionsService: pendingActionsService,
|
||||
PlatformService: platformService,
|
||||
PullLimitCheckDisabled: *flags.PullLimitCheckDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.27.2",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.27.7",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -943,7 +943,7 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.27.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.27.7\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func copyFile(src, dst string) error {
|
||||
defer from.Close()
|
||||
|
||||
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := os.Create(dst)
|
||||
|
||||
@@ -80,6 +80,13 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
if handler.PullLimitCheckDisabled {
|
||||
return response.JSON(w, &dockerhubStatusResponse{
|
||||
Limit: 10,
|
||||
Remaining: 10,
|
||||
})
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
token, err := getDockerHubToken(httpClient, registry)
|
||||
if err != nil {
|
||||
|
||||
@@ -26,19 +26,20 @@ func hideFields(endpoint *portainer.Endpoint) {
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer security.BouncerService
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SnapshotService portainer.SnapshotService
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
AuthorizationService *authorization.Service
|
||||
DockerClientFactory *dockerclient.ClientFactory
|
||||
BindAddress string
|
||||
BindAddressHTTPS string
|
||||
PendingActionsService *pendingactions.PendingActionsService
|
||||
requestBouncer security.BouncerService
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SnapshotService portainer.SnapshotService
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
AuthorizationService *authorization.Service
|
||||
DockerClientFactory *dockerclient.ClientFactory
|
||||
BindAddress string
|
||||
BindAddressHTTPS string
|
||||
PendingActionsService *pendingactions.PendingActionsService
|
||||
PullLimitCheckDisabled bool
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
|
||||
@@ -81,7 +81,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.27.2
|
||||
// @version 2.27.7
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
36
api/http/middlewares/plaintext_http_request.go
Normal file
36
api/http/middlewares/plaintext_http_request.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
)
|
||||
|
||||
var (
|
||||
// Idempotent (safe) methods as defined by RFC7231 section 4.2.2.
|
||||
safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
)
|
||||
|
||||
type plainTextHTTPRequestHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (h *plainTextHTTPRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if slices.Contains(safeMethods, r.Method) {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
req := r
|
||||
// If original request was HTTPS (via proxy), keep CSRF checks.
|
||||
if xfproto := r.Header.Get("X-Forwarded-Proto"); xfproto != "https" {
|
||||
req = csrf.PlaintextHTTPRequest(r)
|
||||
}
|
||||
|
||||
h.next.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func PlaintextHTTPRequest(next http.Handler) http.Handler {
|
||||
return &plainTextHTTPRequestHandler{next: next}
|
||||
}
|
||||
@@ -7,12 +7,31 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Note that we discard any non-canonical headers by design
|
||||
var allowedHeaders = map[string]struct{}{
|
||||
"Accept": {},
|
||||
"Accept-Encoding": {},
|
||||
"Accept-Language": {},
|
||||
"Cache-Control": {},
|
||||
"Content-Length": {},
|
||||
"Content-Type": {},
|
||||
"Private-Token": {},
|
||||
"User-Agent": {},
|
||||
"X-Portaineragent-Target": {},
|
||||
"X-Portainer-Volumename": {},
|
||||
"X-Registry-Auth": {},
|
||||
}
|
||||
|
||||
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
return &httputil.ReverseProxy{Director: createDirector(target)}
|
||||
}
|
||||
|
||||
func createDirector(target *url.URL) func(*http.Request) {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
@@ -26,8 +45,14 @@ func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseP
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
for k := range req.Header {
|
||||
if _, ok := allowedHeaders[k]; !ok {
|
||||
// We use delete here instead of req.Header.Del because we want to delete non canonical headers.
|
||||
delete(req.Header, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
|
||||
190
api/http/proxy/factory/reverse_proxy_test.go
Normal file
190
api/http/proxy/factory/reverse_proxy_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func Test_createDirector(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target *url.URL
|
||||
req *http.Request
|
||||
expectedReq *http.Request
|
||||
}{
|
||||
{
|
||||
name: "base case",
|
||||
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
|
||||
req: createRequest(
|
||||
t,
|
||||
"GET",
|
||||
"https://agent-portainer.io/test?c=7",
|
||||
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": "something"},
|
||||
true,
|
||||
),
|
||||
expectedReq: createRequest(
|
||||
t,
|
||||
"GET",
|
||||
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
|
||||
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": "something"},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "no User-Agent",
|
||||
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
|
||||
req: createRequest(
|
||||
t,
|
||||
"GET",
|
||||
"https://agent-portainer.io/test?c=7",
|
||||
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json"},
|
||||
true,
|
||||
),
|
||||
expectedReq: createRequest(
|
||||
t,
|
||||
"GET",
|
||||
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
|
||||
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": ""},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Sensitive Headers",
|
||||
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
|
||||
req: createRequest(
|
||||
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",
|
||||
},
|
||||
true,
|
||||
),
|
||||
expectedReq: createRequest(
|
||||
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",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Non canonical Headers",
|
||||
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
|
||||
req: createRequest(
|
||||
t,
|
||||
"GET",
|
||||
"https://agent-portainer.io/test?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",
|
||||
portainer.PortainerAgentTargetHeader: "test-agent-1",
|
||||
"X-Portainer-VolumeName": "test-volume-1",
|
||||
"X-Registry-Auth": "test-registry-auth",
|
||||
},
|
||||
false,
|
||||
),
|
||||
expectedReq: createRequest(
|
||||
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-Registry-Auth": "test-registry-auth",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
director := createDirector(tc.target)
|
||||
director(tc.req)
|
||||
|
||||
if diff := cmp.Diff(tc.req, tc.expectedReq, cmp.Comparer(compareRequests)); diff != "" {
|
||||
t.Fatalf("requests are different: \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createURL(t *testing.T, urlString string) *url.URL {
|
||||
parsedURL, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create url: %s", err)
|
||||
}
|
||||
|
||||
return parsedURL
|
||||
}
|
||||
|
||||
func createRequest(t *testing.T, method, url string, headers map[string]string, canonicalHeaders bool) *http.Request {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create http request: %s", err)
|
||||
} else {
|
||||
for k, v := range headers {
|
||||
if canonicalHeaders {
|
||||
req.Header.Add(k, v)
|
||||
} else {
|
||||
req.Header[k] = []string{v}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func compareRequests(a, b *http.Request) bool {
|
||||
methodEqual := a.Method == b.Method
|
||||
urlEqual := cmp.Diff(a.URL, b.URL) == ""
|
||||
hostEqual := a.Host == b.Host
|
||||
protoEqual := a.Proto == b.Proto && a.ProtoMajor == b.ProtoMajor && a.ProtoMinor == b.ProtoMinor
|
||||
headersEqual := cmp.Diff(a.Header, b.Header) == ""
|
||||
|
||||
return methodEqual && urlEqual && hostEqual && protoEqual && headersEqual
|
||||
}
|
||||
@@ -112,6 +112,7 @@ type Server struct {
|
||||
AdminCreationDone chan struct{}
|
||||
PendingActionsService *pendingactions.PendingActionsService
|
||||
PlatformService platform.Service
|
||||
PullLimitCheckDisabled bool
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
@@ -181,6 +182,7 @@ func (server *Server) Start() error {
|
||||
endpointHandler.BindAddress = server.BindAddress
|
||||
endpointHandler.BindAddressHTTPS = server.BindAddressHTTPS
|
||||
endpointHandler.PendingActionsService = server.PendingActionsService
|
||||
endpointHandler.PullLimitCheckDisabled = server.PullLimitCheckDisabled
|
||||
|
||||
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer, server.DataStore, server.FileService, server.ReverseTunnelService)
|
||||
|
||||
@@ -347,7 +349,7 @@ func (server *Server) Start() error {
|
||||
log.Info().Str("bind_address", server.BindAddress).Msg("starting HTTP server")
|
||||
httpServer := &http.Server{
|
||||
Addr: server.BindAddress,
|
||||
Handler: handler,
|
||||
Handler: middlewares.PlaintextHTTPRequest(handler),
|
||||
ErrorLog: errorLogger,
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ type (
|
||||
LogLevel *string
|
||||
LogMode *string
|
||||
KubectlShellImage *string
|
||||
PullLimitCheckDisabled *bool
|
||||
}
|
||||
|
||||
// CustomTemplateVariableDefinition
|
||||
@@ -1637,7 +1638,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.27.2"
|
||||
APIVersion = "2.27.7"
|
||||
// Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support)
|
||||
APIVersionSupport = "LTS"
|
||||
// Edition is what this edition of Portainer is called
|
||||
@@ -1689,6 +1690,8 @@ const (
|
||||
PortainerCacheHeader = "X-Portainer-Cache"
|
||||
// KubectlShellImageEnvVar is the environment variable used to override the default kubectl shell image
|
||||
KubectlShellImageEnvVar = "KUBECTL_SHELL_IMAGE"
|
||||
// PullLimitCheckDisabledEnvVar is the environment variable used to disable the pull limit check
|
||||
PullLimitCheckDisabledEnvVar = "PULL_LIMIT_CHECK_DISABLED"
|
||||
)
|
||||
|
||||
// List of supported features
|
||||
|
||||
@@ -71,7 +71,9 @@ class KubernetesClusterController {
|
||||
|
||||
const applicationsResources = await getTotalResourcesForAllApplications(this.endpoint.Id);
|
||||
this.resourceReservation = new KubernetesResourceReservation();
|
||||
this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest / 1000);
|
||||
|
||||
// Using same rounding method as CPULimit in getNodesAsync for consistency
|
||||
this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest * 10000) / 10000;
|
||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(applicationsResources.MemoryRequest);
|
||||
|
||||
if (this.hasResourceUsageAccess()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"docker": "v27.5.1",
|
||||
"helm": "v3.17.2",
|
||||
"helm": "v3.17.3",
|
||||
"kubectl": "v1.32.2",
|
||||
"mingit": "2.48.1.1"
|
||||
"mingit": "2.49.0.1"
|
||||
}
|
||||
|
||||
20
go.mod
20
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/portainer/portainer
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.10
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
@@ -25,9 +25,9 @@ require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-playground/validator/v10 v10.12.0
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/csrf v1.7.3
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
@@ -48,11 +48,11 @@ require (
|
||||
github.com/urfave/negroni v1.0.0
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.11
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||
golang.org/x/mod v0.21.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/sync v0.12.0
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.29.2
|
||||
@@ -242,10 +242,10 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.25.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -280,8 +280,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -311,8 +311,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
|
||||
github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
@@ -695,8 +695,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -715,10 +715,10 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -727,8 +727,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -755,20 +755,20 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.27.2",
|
||||
"version": "2.27.7",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
|
||||
@@ -98,6 +98,11 @@ func withComposeService(
|
||||
WorkingDir: filepath.Dir(filePaths[0]),
|
||||
}
|
||||
|
||||
if options.ProjectDir != "" {
|
||||
// When relative paths are used in the compose file, the project directory is used as the base path
|
||||
configDetails.WorkingDir = options.ProjectDir
|
||||
}
|
||||
|
||||
for _, p := range filePaths {
|
||||
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user