feat(config): add base url support EE-506 (#5999)
parent
335f951e6b
commit
4aea5690a8
|
@ -55,6 +55,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
|
||||
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
|
|
|
@ -19,4 +19,5 @@ const (
|
|||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
)
|
||||
|
|
|
@ -17,4 +17,5 @@ const (
|
|||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
)
|
||||
|
|
|
@ -653,6 +653,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
BaseURL: *flags.BaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,15 +21,17 @@ type Handler struct {
|
|||
kubernetesClientFactory *cli.ClientFactory
|
||||
authorizationService *authorization.Service
|
||||
JwtService portainer.JWTService
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory, baseURL string) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
authorizationService: authorizationService,
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
|
||||
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
|
||||
|
@ -133,7 +134,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
|||
return nil, &httperror.HandlerError{http.StatusInternalServerError, fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username), err}
|
||||
}
|
||||
|
||||
configClusters[idx] = buildCluster(r, endpoint)
|
||||
configClusters[idx] = buildCluster(r, handler.BaseURL, endpoint)
|
||||
configContexts[idx] = buildContext(serviceAccount.Name, endpoint)
|
||||
if !authInfosSet[serviceAccount.Name] {
|
||||
configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccount.Name, bearerToken))
|
||||
|
@ -151,8 +152,11 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
|||
}, nil
|
||||
}
|
||||
|
||||
func buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
proxyURL := fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpoint.ID)
|
||||
func buildCluster(r *http.Request, baseURL string, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
if baseURL != "/" {
|
||||
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
|
||||
}
|
||||
proxyURL := fmt.Sprintf("https://%s%sapi/endpoints/%d/kubernetes", r.Host, baseURL, endpoint.ID)
|
||||
return clientV1.NamedCluster{
|
||||
Name: buildClusterName(endpoint.Name),
|
||||
Cluster: clientV1.Cluster{
|
||||
|
|
|
@ -96,6 +96,7 @@ type Server struct {
|
|||
ShutdownCtx context.Context
|
||||
ShutdownTrigger context.CancelFunc
|
||||
StackDeployer stackdeployer.StackDeployer
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -172,7 +173,7 @@ func (server *Server) Start() error {
|
|||
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory)
|
||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory, server.BaseURL)
|
||||
kubernetesHandler.JwtService = server.JWTService
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
|
|
@ -95,6 +95,7 @@ type (
|
|||
SSLKey *string
|
||||
Rollback *bool
|
||||
SnapshotInterval *string
|
||||
BaseURL *string
|
||||
}
|
||||
|
||||
// CustomTemplate represents a custom template
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Terminal } from 'xterm';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||
'$scope',
|
||||
|
@ -69,7 +70,8 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
|||
};
|
||||
|
||||
var url =
|
||||
window.location.href.split('#')[0] +
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
'api/websocket/attach?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
@ -109,7 +111,8 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
|||
};
|
||||
|
||||
var url =
|
||||
window.location.href.split('#')[0] +
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
'api/websocket/exec?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
|
|
@ -19,9 +19,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ng-model="formValues.UploadFile"
|
||||
>Select file</button
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ng-model="formValues.UploadFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.UploadFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.UploadFile" aria-hidden="true"></i>
|
||||
|
@ -42,16 +40,16 @@
|
|||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tag" title-text="Tag the image"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
set-validity="setPullImageValidity"
|
||||
check-rate-limits="true"
|
||||
></por-image-registry>
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
set-validity="setPullImageValidity"
|
||||
check-rate-limits="true"
|
||||
></por-image-registry>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,12 @@
|
|||
<title>Portainer</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="author" content="<%= author %>" />
|
||||
<base id="base" />
|
||||
<script>
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
</script>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Terminal } from 'xterm';
|
||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
export default class KubectlShellController {
|
||||
/* @ngInject */
|
||||
|
@ -91,7 +92,7 @@ export default class KubectlShellController {
|
|||
};
|
||||
|
||||
const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const path = '/api/websocket/kubernetes-shell';
|
||||
const path = baseHref() + 'api/websocket/kubernetes-shell';
|
||||
const queryParams = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import angular from 'angular';
|
||||
import { Terminal } from 'xterm';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
class KubernetesApplicationConsoleController {
|
||||
/* @ngInject */
|
||||
|
@ -59,7 +60,8 @@ class KubernetesApplicationConsoleController {
|
|||
};
|
||||
|
||||
let url =
|
||||
window.location.href.split('#')[0] +
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
'api/websocket/pod?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
|
|
@ -11,9 +11,7 @@ interface InputGroupSubComponents {
|
|||
NumberInput: typeof NumberInput;
|
||||
}
|
||||
|
||||
const InputGroup: typeof MainComponent &
|
||||
InputGroupSubComponents = MainComponent as typeof MainComponent &
|
||||
InputGroupSubComponents;
|
||||
const InputGroup: typeof MainComponent & InputGroupSubComponents = MainComponent as typeof MainComponent & InputGroupSubComponents;
|
||||
|
||||
InputGroup.Addon = InputGroupAddon;
|
||||
InputGroup.ButtonWrapper = InputGroupButtonWrapper;
|
||||
|
|
|
@ -2,18 +2,7 @@ import { arrayMove } from './utils';
|
|||
|
||||
it('moves items in an array', () => {
|
||||
expect(arrayMove(['a', 'b', 'c'], 2, 0)).toEqual(['c', 'a', 'b']);
|
||||
expect(
|
||||
arrayMove(
|
||||
[
|
||||
{ name: 'Fred' },
|
||||
{ name: 'Barney' },
|
||||
{ name: 'Wilma' },
|
||||
{ name: 'Betty' },
|
||||
],
|
||||
2,
|
||||
1
|
||||
)
|
||||
).toEqual([
|
||||
expect(arrayMove([{ name: 'Fred' }, { name: 'Barney' }, { name: 'Wilma' }, { name: 'Betty' }], 2, 1)).toEqual([
|
||||
{ name: 'Fred' },
|
||||
{ name: 'Wilma' },
|
||||
{ name: 'Barney' },
|
||||
|
|
|
@ -10,23 +10,13 @@ export function arrayMove<T>(array: Array<T>, from: number, to: number) {
|
|||
|
||||
if (diff > 0) {
|
||||
// move left
|
||||
return [
|
||||
...array.slice(0, to),
|
||||
item,
|
||||
...array.slice(to, from),
|
||||
...array.slice(from + 1, length),
|
||||
];
|
||||
return [...array.slice(0, to), item, ...array.slice(to, from), ...array.slice(from + 1, length)];
|
||||
}
|
||||
|
||||
if (diff < 0) {
|
||||
// move right
|
||||
const targetIndex = to + 1;
|
||||
return [
|
||||
...array.slice(0, from),
|
||||
...array.slice(from + 1, targetIndex),
|
||||
item,
|
||||
...array.slice(targetIndex, length),
|
||||
];
|
||||
return [...array.slice(0, from), ...array.slice(from + 1, targetIndex), item, ...array.slice(targetIndex, length)];
|
||||
}
|
||||
|
||||
return [...array];
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* calculates baseHref
|
||||
*
|
||||
* return [string]
|
||||
*
|
||||
*/
|
||||
export function baseHref() {
|
||||
const base = document.getElementById('base');
|
||||
return base ? base.getAttribute('href') : '/';
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
angular.module('portainer.app').factory('WebhookHelper', [
|
||||
'$location',
|
||||
'API_ENDPOINT_WEBHOOKS',
|
||||
|
@ -11,11 +13,11 @@ angular.module('portainer.app').factory('WebhookHelper', [
|
|||
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
|
||||
|
||||
helper.returnWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
return `${protocol}://${$location.host()}${displayPort}${baseHref()}${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
};
|
||||
|
||||
helper.returnStackWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}/${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
return `${protocol}://${$location.host()}${displayPort}${baseHref()}${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { HIDE_INTERNAL_AUTH } from '@/portainer/feature-flags/feature-ids';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
import providers, { getProviderByUrl } from './providers';
|
||||
|
||||
|
@ -95,7 +96,7 @@ export default class OAuthSettingsController {
|
|||
}
|
||||
|
||||
if (this.settings.RedirectURI === '') {
|
||||
this.settings.RedirectURI = window.location.origin;
|
||||
this.settings.RedirectURI = window.location.origin + baseHref();
|
||||
}
|
||||
|
||||
if (this.settings.AuthorizationURI) {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
export default {
|
||||
microsoft: {
|
||||
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
|
||||
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
|
||||
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
|
||||
logoutUrl: `https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=${window.location.origin}/#!/auth`,
|
||||
logoutUrl: `https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=${window.location.origin}${baseHref()}#!/auth`,
|
||||
userIdentifier: 'userPrincipalName',
|
||||
scopes: 'id,email,name',
|
||||
},
|
||||
|
@ -11,7 +13,7 @@ export default {
|
|||
authUrl: 'https://accounts.google.com/o/oauth2/auth',
|
||||
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
|
||||
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
|
||||
logoutUrl: `https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=${window.location.origin}/#!/auth`,
|
||||
logoutUrl: `https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=${window.location.origin}${baseHref()}#!/auth`,
|
||||
userIdentifier: 'email',
|
||||
scopes: 'profile email',
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { PortainerEndpointCreationTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel';
|
||||
|
||||
angular
|
||||
|
@ -84,7 +85,8 @@ angular
|
|||
};
|
||||
|
||||
$scope.setDefaultPortainerInstanceURL = function () {
|
||||
$scope.formValues.URL = window.location.origin;
|
||||
const baseHREF = baseHref();
|
||||
$scope.formValues.URL = window.location.origin + (baseHREF !== '/' ? baseHREF : '');
|
||||
};
|
||||
|
||||
$scope.resetEndpointURL = function () {
|
||||
|
|
Loading…
Reference in New Issue