Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feature/tenants repo init #247

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/adr/0032-multi-tenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!---~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Copyright (c) 2021 Contributors to the Eclipse Foundation
~
~ See the NOTICE file(s) distributed with this work for additional
~ information regarding copyright ownership.
~
~ This program and the accompanying materials are made available under the
~ terms of the Eclipse Public License 2.0 which is available at
~ http://www.eclipse.org/legal/epl-2.0, or the Apache Software License 2.0
~ which is available at https://www.apache.org/licenses/LICENSE-2.0.
~
~ SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->


# Multi-tenant repository

* Status: [accepted] <!-- optional -->
* Deciders: [Marvin Bechtold](https://github.com/mar-be), [Lukas Harzenetter](https://github.com/lharzenetter) <!-- optional -->
* Date: 2021-08-17 <!-- optional -->

Technical Story: [description | ticket/issue URL] <!-- optional -->

## Context and Problem Statement

Multiple clients need access to separate and independent winery repositories to manage their deployment models.
However, the provider does not want to run a winery instance for each client.
Thus, the winery server needs a multi-tenant mode to serve multiple clients independent of each other with separate repositories.

## Decision Drivers <!-- optional -->

* Small number of adaptions in the existing code
* Downwards compatibility


## Considered Options

1. Use a MultiRepository to manage the repositories in different namespaces
2. Create a new TenantRepository

## Decision Outcome

Option 2 was chosen because it satisfies all requirements and due to its simple implementation.
Option 1 does not satisfy all requirements.

### Positive Consequences <!-- optional -->

* Winery supports a multi-tenant mode that provides each tenant with a separate and independent
* The user can specify the tenant of a request in a convenient way
* Parallel requests from different tenants are supported
* If no tenant is specified, the TenantRepository behaves like a normal repository

### Negative consequences <!-- optional -->

* Additional overhead because the tenant needs to be specified in each request


## Pros and Cons of the Options <!-- optional -->

### Option 1: MultiRepository

We use a MultiRepository and assign each tenant to an exclusive namespace.
The tenant's deployment models are stored in the repository that belongs to his namespace. <!-- optional -->

* Good, because the MultiRepository is already implemented.
* Bad, because a tenant can only use his assigned namespace.
* Bad, because the functionality of the namespace gets alienated from its defined purpose
* Bad, because the repositories of the tenants are not strictly separated and independent

### Option 2: TenantRepository

We create a new TenantRepository that manages a separate repository (e.g., a MultiRepository) for each tenant.
Each request to Winery specifies the corresponding tenant to whose repository it is referring.
A request gets intercepted by a filter that extracts the request's tenant, stores it in a [ThreadLocal](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html) variable, and then forwards it to the requested method.
When this request operates on the repository, the TenantRepository retrieves the tenant from the ThreadLocal variable and applies the operation to the tenant-specific repository.
The construction with the ThreadLocal variable works because each request is processed in a separate thread.
Internally, the TenantRepository creates for each tenant a new directory that contains the tenant's repository.

<!-- optional -->

* Good, because only a few changes are needed in the existing code
* Good, because it strictly separates the repositories of the different tenants
* Good, because it enables parallel operations on the different tenant repositories
* Bad, because we have to implement the TanantRepository and the filter
* Bad, because all requests have to pass the filter, even tenant unrelated requests


## Usage
The tenant mode has to be activated in the config file:
```
repository:
tenantMode: true
```
To use a tenant's repository, the tenant has to be specified in each request.
This can be done via a header field or a query parameter.
If the tenant mode is activated and a request without a specified tenant arrives, the request is routed to a default repository.
Specifying no tenant is the same as choosing the tenant `default`.

### Header field
To set a tenant via a header field, the HTTP header `xTenant` has to contain the tenant's name. If a tenant is specified as a header field, the query parameter is ignored.

### Query parameter
To set a tenant via a query parameter, the parameter `xTenant` in the URL has to contain the tenant's name.
For example:
http://localhost:8080/winery/nodetypes?xTenant=tenant_name


Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*******************************************************************************/
import { Component, Input } from '@angular/core';
import { Component } from '@angular/core';
import { NgRedux } from '@angular-redux/store';
import { IWineryState } from '../redux/store/winery.store';
import { TopologyRendererActions } from '../redux/actions/topologyRenderer.actions';
Expand Down Expand Up @@ -63,7 +63,7 @@ export class EnricherComponent {
* @param node: node template id where enrichment shall be applied later
* @param event: selection changed event from checkbox
*/
protected featureSelectionChanged(feature: FeatureEntity, node: Enrichment, event: any) {
public featureSelectionChanged(feature: FeatureEntity, node: Enrichment, event: any) {
const isChecked = event.target.checked;
const nodeTemplate = node.nodeTemplateId;
// if a new feature was selected and to applicable enrichment array is empty or node template is not added yet
Expand All @@ -87,7 +87,7 @@ export class EnricherComponent {
* This method is called when clicking the "Apply" button.
* It starts the Enricher Service to apply the selected enrichments.
*/
protected applyEnrichment() {
public applyEnrichment() {
this.enricherService.applySelectedFeatures(this.toApply).subscribe(
data => this.enrichmentApplied(data),
error => this.handleError(error)
Expand All @@ -99,7 +99,7 @@ export class EnricherComponent {
* It highlights the respective node template in the topology modeler.
* @param entry: entry of available features displayed in the UI
*/
protected onHoverOver(entry: Enrichment) {
public onHoverOver(entry: Enrichment) {
const nodeTemplateIds: string[] = [];
nodeTemplateIds.push(entry.nodeTemplateId);
this.ngRedux.dispatch(this.actions.highlightNodes(nodeTemplateIds));
Expand All @@ -108,15 +108,15 @@ export class EnricherComponent {
/**
* This method is called when the user hovers out of a node template.
*/
protected hoverOut() {
public hoverOut() {
this.ngRedux.dispatch(this.actions.highlightNodes([]));
}

/**
* This method is called when the User clicks "Cancel".
* It resets the available features, which lets the enrichment sidebar disappear.
*/
protected cancel() {
public cancel() {
this.availableFeatures = null;
this.ngRedux.dispatch(this.actions.enrichNodeTemplates());
}
Expand Down Expand Up @@ -216,7 +216,7 @@ export class EnricherComponent {
* @param data: topology template that was updated
*/
private enrichmentApplied(data: TTopologyTemplate) {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, data, this.entityTypes, this.configurationService.isYaml());
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, data, this.entityTypes, this.configurationService.isYaml(), this.alert);
// reset available features since they are no longer valid
this.availableFeatures = null;
this.alert.success('Updated Topology Template!');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RequirementModel } from './requirementModel';
import { InheritanceUtils } from './InheritanceUtils';
import { QName } from '../../../../shared/src/app/model/qName';
import { TPolicy } from './policiesModalData';
import { ToastrService } from 'ngx-toastr';

export abstract class TopologyTemplateUtil {

Expand Down Expand Up @@ -62,7 +63,8 @@ export abstract class TopologyTemplateUtil {
}

static createTNodeTemplateFromObject(node: TNodeTemplate, nodeVisuals: Visuals[],
isYaml: boolean, types: EntityTypesModel, state?: DifferenceStates): TNodeTemplate {
isYaml: boolean, types: EntityTypesModel, notify: ToastrService,
state?: DifferenceStates): TNodeTemplate {
const nodeVisualsObject = this.getNodeVisualsForNodeTemplate(node.type, nodeVisuals, state);
let properties;
if (node.properties) {
Expand Down Expand Up @@ -113,7 +115,7 @@ export abstract class TopologyTemplateUtil {
if (isYaml) {
if (!types) {
// todo ensure entity types model is always available. See TopologyTemplateUtil.updateTopologyTemplate
console.error('The required entity types model is not available! Unexpected behavior');
notify.error('The required entity types model is not available! Unexpected behavior');
}
// look for missing capabilities and add them
const capDefs: CapabilityDefinitionModel[] = InheritanceUtils.getEffectiveCapabilityDefinitionsOfNodeType(node.type, types);
Expand Down Expand Up @@ -209,7 +211,7 @@ export abstract class TopologyTemplateUtil {
}

static initNodeTemplates(nodeTemplateArray: Array<TNodeTemplate>, nodeVisuals: Visuals[], isYaml: boolean, types: EntityTypesModel,
topologyDifferences?: [ToscaDiff, TTopologyTemplate]): Array<TNodeTemplate> {
notify: ToastrService, topologyDifferences?: [ToscaDiff, TTopologyTemplate]): Array<TNodeTemplate> {
const nodeTemplates: TNodeTemplate[] = [];
if (nodeTemplateArray.length > 0) {
nodeTemplateArray.forEach((node, index) => {
Expand All @@ -220,7 +222,7 @@ export abstract class TopologyTemplateUtil {
}
const state = topologyDifferences ? DifferenceStates.UNCHANGED : null;
nodeTemplates.push(
TopologyTemplateUtil.createTNodeTemplateFromObject(node, nodeVisuals, isYaml, types, state)
TopologyTemplateUtil.createTNodeTemplateFromObject(node, nodeVisuals, isYaml, types, notify, state)
);
});
}
Expand Down Expand Up @@ -280,7 +282,7 @@ export abstract class TopologyTemplateUtil {
}

static updateTopologyTemplate(ngRedux: NgRedux<IWineryState>, wineryActions: WineryActions, topology: TTopologyTemplate,
types: EntityTypesModel, isYaml: boolean) {
types: EntityTypesModel, isYaml: boolean, notify: ToastrService) {
const wineryState = ngRedux.getState().wineryState;

// Required because if the palette is open, the last node inserted will be bound to the mouse movement.
Expand All @@ -296,7 +298,7 @@ export abstract class TopologyTemplateUtil {
node => ngRedux.dispatch(wineryActions.deleteNodeTemplate(node.id))
);

TopologyTemplateUtil.initNodeTemplates(topology.nodeTemplates, wineryState.nodeVisuals, isYaml, types)
TopologyTemplateUtil.initNodeTemplates(topology.nodeTemplates, wineryState.nodeVisuals, isYaml, types, notify)
.forEach(
node => ngRedux.dispatch(wineryActions.saveNodeTemplate(node))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Utils } from '../../../../../tosca-management/src/app/wineryUtils/utils
import { WineryVersion } from '../../../../../tosca-management/src/app/model/wineryVersion';
import { WineryRepositoryConfigurationService } from '../../../../../tosca-management/src/app/wineryFeatureToggleModule/WineryRepositoryConfiguration.service';
import { EntityTypesModel } from '../../models/entityTypesModel';
import { ToastrService } from 'ngx-toastr';

@Component({
selector: 'winery-versions',
Expand Down Expand Up @@ -72,6 +73,7 @@ export class VersionsComponent implements OnInit {
private errorHandler: ErrorHandlerService,
private ngRedux: NgRedux<IWineryState>,
private configurationService: WineryRepositoryConfigurationService,
private notify: ToastrService,
private wineryActions: WineryActions) {
this.ngRedux.select(state => state.wineryState.currentJsonTopology)
.subscribe(topology => this.topologyTemplate = topology);
Expand Down Expand Up @@ -157,7 +159,8 @@ export class VersionsComponent implements OnInit {
}

updateTopology(topology: TTopologyTemplate) {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, topology, this.entityTypes, this.configurationService.isYaml());
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, topology, this.entityTypes,
this.configurationService.isYaml(), this.notify);
}

showKVComparison() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ export class ProblemDetectionComponent {
}

private solutionApplied(data: TTopologyTemplate) {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, data, this.entityTypes, this.configurationService.isYaml());
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, data, this.entityTypes,
this.configurationService.isYaml(), this.alert);
this.loading = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export class StatefulAnnotationsService {
this.http.get<TTopologyTemplate>(url)
.subscribe(
data =>
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.actions, data, this.entityTypes, this.configurationService.isYaml()),
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.actions, data, this.entityTypes,
this.configurationService.isYaml(), this.alert),
error => this.errorHandler.handleError(error)
);
}
Expand All @@ -100,7 +101,7 @@ export class StatefulAnnotationsService {
.subscribe(
data => {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.actions, data.topologyTemplate,
this.entityTypes, this.configurationService.isYaml());
this.entityTypes, this.configurationService.isYaml(), this.alert);
if (data.errorList && data.errorList.length > 0) {
this.alert.warning(
'There were no freeze operations found for some stateful components!',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class InstanceModelComponent implements OnDestroy {
}

private handleError(error: HttpErrorResponse) {
this.notify.error(error.message, 'An error occurred...');
this.running = false;
}

Expand All @@ -124,7 +125,7 @@ export class InstanceModelComponent implements OnDestroy {
this.running = false;
if (value && value.topologyTemplate) {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, value.topologyTemplate,
this.entityTypes, this.configurationService.isYaml());
this.entityTypes, this.configurationService.isYaml(), this.notify);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { WineryActions } from '../../redux/actions/winery.actions';
import { TopologyTemplateUtil } from '../../models/topologyTemplateUtil';
import { WineryRepositoryConfigurationService } from '../../../../../tosca-management/src/app/wineryFeatureToggleModule/WineryRepositoryConfiguration.service';
import { EntityTypesModel } from '../../models/entityTypesModel';
import { ToastrService } from 'ngx-toastr';

@Component({
selector: 'winery-refinement',
Expand Down Expand Up @@ -48,6 +49,7 @@ export class RefinementSidebarComponent implements OnDestroy {
private wineryActions: WineryActions,
private webSocketService: RefinementWebSocketService,
private configurationService: WineryRepositoryConfigurationService,
private notify: ToastrService,
private backendService: BackendService) {
this.ngRedux.select(state => state.wineryState.entityTypes)
.subscribe(types => this.entityTypes = types);
Expand Down Expand Up @@ -110,7 +112,7 @@ export class RefinementSidebarComponent implements OnDestroy {

if (value.currentTopology) {
TopologyTemplateUtil.updateTopologyTemplate(this.ngRedux, this.wineryActions, value.currentTopology,
this.entityTypes, this.configurationService.isYaml());
this.entityTypes, this.configurationService.isYaml(), this.notify);
} else {
this.openModelerFor(value.serviceTemplateContainingRefinements.xmlId.decoded,
value.serviceTemplateContainingRefinements.namespace.decoded,
Expand All @@ -123,6 +125,7 @@ export class RefinementSidebarComponent implements OnDestroy {
}

private handleError(error: any) {
this.notify.error(error.message, 'An error occurred...');
this.refinementIsLoading = false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class TopologyRendererComponent implements OnInit, OnDestroy {
if (node.state === DifferenceStates.REMOVED) {
current = this.oldTopology.nodeTemplates.find(item => item.id === node.element);
current = TopologyTemplateUtil.createTNodeTemplateFromObject(current, this.entityTypes.nodeVisuals,
this.configuration.isYaml(), this.entityTypes, node.state);
this.configuration.isYaml(), this.entityTypes, this.notify, node.state);
this.nodeTemplates.push(current);
} else {
current.state = node.state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { WineryActions } from '../redux/actions/winery.actions';
import { WineryRepositoryConfigurationService } from '../../../../tosca-management/src/app/wineryFeatureToggleModule/WineryRepositoryConfiguration.service';
import { Utils } from '../../../../tosca-management/src/app/wineryUtils/utils';
import { EntityTypesModel } from '../models/entityTypesModel';
import { ToastrService } from 'ngx-toastr';

@Component({
selector: 'winery-version-slider',
Expand Down Expand Up @@ -53,6 +54,7 @@ export class VersionSliderComponent implements OnInit {
private ngRedux: NgRedux<IWineryState>,
private rendererActions: TopologyRendererActions,
private wineryActions: WineryActions,
private notify: ToastrService,
private configurationService: WineryRepositoryConfigurationService) {
this.versionSliderService.getVersions()
.subscribe(versions => this.init(versions));
Expand Down Expand Up @@ -150,7 +152,8 @@ export class VersionSliderComponent implements OnInit {
this.wineryActions,
topologyTemplate,
this.entityTypes,
this.configurationService.isYaml()
this.configurationService.isYaml(),
this.notify
);
}
);
Expand Down
Loading