feat(config): add base url support EE-506 (#5999)

pull/6217/head
Prabhat Khera 2021-12-03 14:34:45 +13:00 committed by GitHub
parent 335f951e6b
commit 4aea5690a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 71 additions and 55 deletions

View File

@ -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()

View File

@ -19,4 +19,5 @@ const (
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
)

View File

@ -17,4 +17,5 @@ const (
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
)

View File

@ -653,6 +653,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
}
}

View File

@ -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()

View File

@ -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{

View File

@ -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"))

View File

@ -95,6 +95,7 @@ type (
SSLKey *string
Rollback *bool
SnapshotInterval *string
BaseURL *string
}
// CustomTemplate represents a custom template

View File

@ -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])

View File

@ -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>

View File

@ -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]>

View File

@ -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('&');

View File

@ -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])

View File

@ -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;

View File

@ -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' },

View File

@ -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];

View File

@ -0,0 +1,10 @@
/**
* calculates baseHref
*
* return [string]
*
*/
export function baseHref() {
const base = document.getElementById('base');
return base ? base.getAttribute('href') : '/';
}

View File

@ -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;

View File

@ -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) {

View File

@ -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',
},

View File

@ -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 () {