diff --git a/docs/adr/0032-multi-tenancy.md b/docs/adr/0032-multi-tenancy.md new file mode 100644 index 0000000000..c926f4a620 --- /dev/null +++ b/docs/adr/0032-multi-tenancy.md @@ -0,0 +1,107 @@ + + + +# Multi-tenant repository + +* Status: [accepted] +* Deciders: [Marvin Bechtold](https://github.com/mar-be), [Lukas Harzenetter](https://github.com/lharzenetter) +* Date: 2021-08-17 + +Technical Story: [description | ticket/issue URL] + +## 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 + +* 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 + +* 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 + +* Additional overhead because the tenant needs to be specified in each request + + +## Pros and Cons of the Options + +### 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. + +* 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. + + + +* 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 + + diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/enricher/enricher.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/enricher/enricher.component.ts index 1d75862e01..2db1500b32 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/enricher/enricher.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/enricher/enricher.component.ts @@ -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'; @@ -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 @@ -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) @@ -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)); @@ -108,7 +108,7 @@ 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([])); } @@ -116,7 +116,7 @@ export class EnricherComponent { * 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()); } @@ -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!'); diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/models/topologyTemplateUtil.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/models/topologyTemplateUtil.ts index 593ea9704e..7772175ef3 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/models/topologyTemplateUtil.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/models/topologyTemplateUtil.ts @@ -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 { @@ -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) { @@ -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); @@ -209,7 +211,7 @@ export abstract class TopologyTemplateUtil { } static initNodeTemplates(nodeTemplateArray: Array, nodeVisuals: Visuals[], isYaml: boolean, types: EntityTypesModel, - topologyDifferences?: [ToscaDiff, TTopologyTemplate]): Array { + notify: ToastrService, topologyDifferences?: [ToscaDiff, TTopologyTemplate]): Array { const nodeTemplates: TNodeTemplate[] = []; if (nodeTemplateArray.length > 0) { nodeTemplateArray.forEach((node, index) => { @@ -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) ); }); } @@ -280,7 +282,7 @@ export abstract class TopologyTemplateUtil { } static updateTopologyTemplate(ngRedux: NgRedux, 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. @@ -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)) ); diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/node/versions/versions.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/node/versions/versions.component.ts index 366235b4af..e3c753b5d3 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/node/versions/versions.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/node/versions/versions.component.ts @@ -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', @@ -72,6 +73,7 @@ export class VersionsComponent implements OnInit { private errorHandler: ErrorHandlerService, private ngRedux: NgRedux, private configurationService: WineryRepositoryConfigurationService, + private notify: ToastrService, private wineryActions: WineryActions) { this.ngRedux.select(state => state.wineryState.currentJsonTopology) .subscribe(topology => this.topologyTemplate = topology); @@ -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() { diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/problemDetection/problemDetection.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/problemDetection/problemDetection.component.ts index 9727d10aaa..e48bf4c511 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/problemDetection/problemDetection.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/problemDetection/problemDetection.component.ts @@ -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; } } diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/services/statefulAnnotations.service.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/services/statefulAnnotations.service.ts index d93dd0f6ef..34b2896547 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/services/statefulAnnotations.service.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/services/statefulAnnotations.service.ts @@ -83,7 +83,8 @@ export class StatefulAnnotationsService { this.http.get(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) ); } @@ -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!', diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/instanceModel/instanceModel.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/instanceModel/instanceModel.component.ts index f4881a7e10..3868ab9128 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/instanceModel/instanceModel.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/instanceModel/instanceModel.component.ts @@ -116,6 +116,7 @@ export class InstanceModelComponent implements OnDestroy { } private handleError(error: HttpErrorResponse) { + this.notify.error(error.message, 'An error occurred...'); this.running = false; } @@ -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); } } diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/refinement/refinementSidebar.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/refinement/refinementSidebar.component.ts index 4786ad0bc9..b61ca8b698 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/refinement/refinementSidebar.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/sidebars/refinement/refinementSidebar.component.ts @@ -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', @@ -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); @@ -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, @@ -123,6 +125,7 @@ export class RefinementSidebarComponent implements OnDestroy { } private handleError(error: any) { + this.notify.error(error.message, 'An error occurred...'); this.refinementIsLoading = false; } diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/topology-renderer/topology-renderer.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/topology-renderer/topology-renderer.component.ts index d446ffed40..b15428aa9b 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/topology-renderer/topology-renderer.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/topology-renderer/topology-renderer.component.ts @@ -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; diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/version-slider/version-slider.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/version-slider/version-slider.component.ts index a45c6c9989..338ea82c62 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/version-slider/version-slider.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/version-slider/version-slider.component.ts @@ -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', @@ -53,6 +54,7 @@ export class VersionSliderComponent implements OnInit { private ngRedux: NgRedux, private rendererActions: TopologyRendererActions, private wineryActions: WineryActions, + private notify: ToastrService, private configurationService: WineryRepositoryConfigurationService) { this.versionSliderService.getVersions() .subscribe(versions => this.init(versions)); @@ -150,7 +152,8 @@ export class VersionSliderComponent implements OnInit { this.wineryActions, topologyTemplate, this.entityTypes, - this.configurationService.isYaml() + this.configurationService.isYaml(), + this.notify ); } ); diff --git a/org.eclipse.winery.frontends/app/topologymodeler/src/app/winery.component.ts b/org.eclipse.winery.frontends/app/topologymodeler/src/app/winery.component.ts index 4fa8b3adaf..04b2ff6766 100644 --- a/org.eclipse.winery.frontends/app/topologymodeler/src/app/winery.component.ts +++ b/org.eclipse.winery.frontends/app/topologymodeler/src/app/winery.component.ts @@ -80,6 +80,7 @@ export class WineryComponent implements OnInit, AfterViewInit { private uiActions: WineryActions, private alert: ToastrService, private activatedRoute: ActivatedRoute, + private notify: ToastrService, private configurationService: WineryRepositoryConfigurationService) { this.subscriptions.push(this.ngRedux.select(state => state.wineryState.hideNavBarAndPaletteState) .subscribe(hideNavBar => this.hideNavBarState = hideNavBar)); @@ -164,7 +165,7 @@ export class WineryComponent implements OnInit, AfterViewInit { initTopologyTemplateForRendering(nodeTemplateArray: Array, relationshipTemplateArray: Array) { // init node templates this.nodeTemplates = TopologyTemplateUtil.initNodeTemplates(nodeTemplateArray, this.entityTypes.nodeVisuals, - this.configurationService.isYaml(), this.entityTypes, this.topologyDifferences); + this.configurationService.isYaml(), this.entityTypes, this.notify, this.topologyDifferences); // init relationship templates this.relationshipTemplates = TopologyTemplateUtil.initRelationTemplates(relationshipTemplateArray, this.nodeTemplates, this.configurationService.isYaml(), this.topologyDifferences); diff --git a/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/TenantRepositoryTest.java b/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/TenantRepositoryTest.java index f4a4592a24..b2c3a31829 100644 --- a/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/TenantRepositoryTest.java +++ b/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/TenantRepositoryTest.java @@ -43,10 +43,12 @@ public static void shutdown() throws Exception { @Test public void testTenantRepository() throws Exception { - this.setRevisionTo("origin/tenants"); - String[] results = new String[3]; + this.assertPost("servicetemplates?xTenant=tenant_1", "tenants/tenants1_create.json"); + this.assertPost("servicetemplates?xTenant=tenant_3", "tenants/tenants3_create1.json"); + this.assertPost("servicetemplates?xTenant=tenant_3", "tenants/tenants3_create2.json"); + Thread tenant1Thread = new Thread(() -> results[0] = start() .accept(ContentType.JSON.toString()) diff --git a/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/entitytypes/nodetypes/YamlNodeTypesResourceTest.java b/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/entitytypes/nodetypes/YamlNodeTypesResourceTest.java index b2d7a9fd01..192bcaf15b 100644 --- a/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/entitytypes/nodetypes/YamlNodeTypesResourceTest.java +++ b/org.eclipse.winery.repository.rest/src/test/java/org/eclipse/winery/repository/rest/resources/entitytypes/nodetypes/YamlNodeTypesResourceTest.java @@ -37,7 +37,7 @@ public void createNodeType() throws Exception { this.assertPost("nodetypes", "entitytypes/nodetypes/addYamlNodeType.json"); this.assertGetSize("nodetypes", 3); - Path filePath = this.repositoryPath.resolve("nodetypes/org.example.tosca.nodetypes/myLittleExample_1.0.0-w1-1/NodeType.tosca"); + Path filePath = repositoryPath.resolve("nodetypes/org.example.tosca.nodetypes/myLittleExample_1.0.0-w1-1/NodeType.tosca"); String fileContent = FileUtils.readFileToString(filePath.toFile(), "UTF-8"); assertEquals(readFromClasspath("entitytypes/nodetypes/addecYamlNodeType.yml"), fileContent); diff --git a/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants1_create.json b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants1_create.json new file mode 100644 index 0000000000..1de487bb6b --- /dev/null +++ b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants1_create.json @@ -0,0 +1,4 @@ +{ + "localname": "test1", + "namespace": "namespace" +} diff --git a/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create1.json b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create1.json new file mode 100644 index 0000000000..1b43078a3f --- /dev/null +++ b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create1.json @@ -0,0 +1,4 @@ +{ + "localname": "test3", + "namespace": "namespace" +} diff --git a/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create2.json b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create2.json new file mode 100644 index 0000000000..9010b31299 --- /dev/null +++ b/org.eclipse.winery.repository.rest/src/test/resources/tenants/tenants3_create2.json @@ -0,0 +1,4 @@ +{ + "localname": "test53", + "namespace": "namespace" +} diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/TestWithGitBackedRepository.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/TestWithGitBackedRepository.java index 9717d05236..a011ea858a 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/TestWithGitBackedRepository.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/TestWithGitBackedRepository.java @@ -13,6 +13,7 @@ *******************************************************************************/ package org.eclipse.winery.repository; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -33,6 +34,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; @@ -47,11 +49,10 @@ public abstract class TestWithGitBackedRepository { protected static final Logger LOGGER = LoggerFactory.getLogger(TestWithGitBackedRepository.class); - public final Path repositoryPath; - - public final IRepository repository; - - public final Git git; + private static final String TEST_REPOSITORY = "https://github.com/winery/test-repository.git"; + + protected final Path repositoryPath; + protected final IRepository repository; /** * Initializes the git repository from https://github.com/winery/test-repository into %TEMP%/test-repository @@ -64,56 +65,76 @@ public TestWithGitBackedRepository() { protected TestWithGitBackedRepository(RepositoryConfigurationObject.RepositoryProvider provider) { this.repositoryPath = Paths.get(System.getProperty("java.io.tmpdir")).resolve("test-repository"); - String remoteUrl = "https://github.com/winery/test-repository.git"; - + LOGGER.debug("Testing with repository directory {}", this.repositoryPath); + getGit().close(); + + // inject the current path to the repository factory + FileBasedRepositoryConfiguration fileBasedRepositoryConfiguration = new FileBasedRepositoryConfiguration(this.repositoryPath, provider); + // force xml repository provider + fileBasedRepositoryConfiguration.setRepositoryProvider(provider); + GitBasedRepositoryConfiguration gitBasedRepositoryConfiguration = new GitBasedRepositoryConfiguration(false, fileBasedRepositoryConfiguration); try { - LOGGER.debug("Testing with repository directory {}", repositoryPath); + RepositoryFactory.reconfigure(gitBasedRepositoryConfiguration); + } catch (IOException | GitAPIException e) { + LOGGER.error("Error while initializing test repository!", e); + throw new RuntimeException(e); + } + + this.repository = RepositoryFactory.getRepository(); + } - if (!Files.exists(repositoryPath)) { - Files.createDirectory(repositoryPath); + protected Git getGit() { + Git git; + try { + if (!Files.exists(this.repositoryPath)) { + Files.createDirectory(this.repositoryPath); } FileRepositoryBuilder builder = new FileRepositoryBuilder(); - if (!Files.exists(repositoryPath.resolve(".git"))) { - FileUtils.cleanDirectory(repositoryPath.toFile()); - this.git = Git.cloneRepository() - .setURI(remoteUrl) - .setBare(false) - .setCloneAllBranches(true) - .setDirectory(repositoryPath.toFile()) - .call(); + if (!Files.exists(this.repositoryPath.resolve(".git"))) { + git = this.createTestRepository(); } else { - Repository gitRepo = builder.setWorkTree(repositoryPath.toFile()).setMustExist(false).build(); - this.git = new Git(gitRepo); + Repository gitRepo = builder.setWorkTree(this.repositoryPath.toFile()).setMustExist(false).build(); + git = new Git(gitRepo); try { - this.git.fetch().call(); + git.fetch().call(); } catch (TransportException e) { // we ignore it to enable offline testing LOGGER.debug("Working in offline mode", e); + } catch (InvalidRemoteException e) { + LOGGER.error("Repository corrupt, reinitializing..."); + git.close(); + git = this.createTestRepository(); } } - - // inject the current path to the repository factory - FileBasedRepositoryConfiguration fileBasedRepositoryConfiguration = new FileBasedRepositoryConfiguration(repositoryPath, provider); - // force xml repository provider - fileBasedRepositoryConfiguration.setRepositoryProvider(provider); - GitBasedRepositoryConfiguration gitBasedRepositoryConfiguration = new GitBasedRepositoryConfiguration(false, fileBasedRepositoryConfiguration); - RepositoryFactory.reconfigure(gitBasedRepositoryConfiguration); - - this.repository = RepositoryFactory.getRepository(); - LOGGER.debug("Initialized test repository"); } catch (Exception e) { throw new RuntimeException(e); } + + return git; + } + + private Git createTestRepository() throws IOException, GitAPIException { + FileUtils.cleanDirectory(this.repositoryPath.toFile()); + Git git = Git.cloneRepository() + .setURI(TEST_REPOSITORY) + .setBare(false) + .setCloneAllBranches(true) + .setDirectory(this.repositoryPath.toFile()) + .call(); + LOGGER.debug("Initialized test repository"); + return git; } protected void setRevisionTo(String ref) throws GitAPIException { + Git git = this.getGit(); git.clean().setForce(true).setCleanDirectories(true).call(); git.reset() .setMode(ResetCommand.ResetType.HARD) .setRef(ref) .call(); LOGGER.debug("Switched to commit {}", ref); + git.close(); } protected void makeSomeChanges(NodeTypeId id) throws Exception { diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/RepositoryFactory.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/RepositoryFactory.java index 7dc94e9931..84512d51e5 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/RepositoryFactory.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/RepositoryFactory.java @@ -48,6 +48,11 @@ public static boolean repositoryContainsMultiRepositoryConfiguration(FileBasedRe return repositoryContainsRepoConfig(config, Filename.FILENAME_JSON_MUTLI_REPOSITORIES); } + public static boolean repositoryContainsTenantConfiguration(FileBasedRepositoryConfiguration config) { + return Environments.getInstance().getRepositoryConfig().isTenantRepository() + || repositoryContainsRepoConfig(config, Filename.FILENAME_TENANT_REPOSITORIES); + } + public static boolean repositoryContainsRepoConfig(FileBasedRepositoryConfiguration config, String fileName) { if (config.getRepositoryPath().isPresent()) { return new File(config.getRepositoryPath().get().toString(), fileName).exists(); @@ -70,7 +75,7 @@ public static void reconfigure(GitBasedRepositoryConfiguration configuration) th if (repositoryContainsMultiRepositoryConfiguration(configuration)) { repository = new MultiRepository(configuration.getRepositoryPath().get()); - } else if (Environments.getInstance().getRepositoryConfig().isTenantRepository()) { + } else if (repositoryContainsTenantConfiguration(configuration)) { repository = new TenantRepository(configuration.getRepositoryPath().get()); } else { // if a repository root is specified, use it instead of the root specified in the config @@ -91,7 +96,7 @@ public static void reconfigure(FileBasedRepositoryConfiguration configuration) { } catch (IOException | GitAPIException e) { LOGGER.error("Error while initializing Multi-Repository!"); } - } else if (Environments.getInstance().getRepositoryConfig().isTenantRepository()) { + } else if (repositoryContainsTenantConfiguration(configuration)) { try { repository = new TenantRepository(configuration.getRepositoryPath().get()); } catch (IOException | GitAPIException e) { diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/constants/Filename.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/constants/Filename.java index a4d97e9f52..44a0106a51 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/constants/Filename.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/constants/Filename.java @@ -46,5 +46,6 @@ public class Filename { public static final String FILENAME_PROPERTIES_SERVICETEMPLATE = "ServiceTemplate.properties"; public static final String FILENAME_PROPERTIES_TAGS = "tags.properties"; public static final String FILENAME_PROPERTIES_VISUALAPPEARANCE = "VisualAppearance.properties"; + public static final String FILENAME_TENANT_REPOSITORIES = ".tenant_repo"; } diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/filebased/GitBasedRepository.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/filebased/GitBasedRepository.java index 9b1b40e23b..4a7b02cc6d 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/filebased/GitBasedRepository.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/backend/filebased/GitBasedRepository.java @@ -117,7 +117,7 @@ import org.slf4j.LoggerFactory; /** - * Allows to reset repository to a certain commit id + * Allows resetting repository to a certain commit id */ public class GitBasedRepository extends AbstractFileBasedRepository implements IWrappingRepository { diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/export/CsarExporter.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/export/CsarExporter.java index 9871ba1d4b..798d1701f9 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/export/CsarExporter.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/export/CsarExporter.java @@ -282,12 +282,12 @@ protected void addArtifactTemplateToZipFile(ZipOutputStream zos, RepositoryRefBa // TODO: This is not quite correct. The files should reside checked out at "source/" // TODO: Hash all these git files (to be included in the provenance) Path tempDir = Files.createTempDirectory(WINERY_TEMP_DIR_PREFIX); - try { - Git git = Git - .cloneRepository() - .setURI(gitInfo.URL) - .setDirectory(tempDir.toFile()) - .call(); + try (Git git = Git + .cloneRepository() + .setURI(gitInfo.URL) + .setDirectory(tempDir.toFile()) + .call() + ) { git.checkout().setName(gitInfo.BRANCH).call(); String path = "artifacttemplates/" + EncodingUtil.URLencode(((ArtifactTemplateId) csarEntry.getReference().getParent().getParent()).getQName().getNamespaceURI()) diff --git a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/filebased/TenantRepository.java b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/filebased/TenantRepository.java index 40c690a1c2..c142583379 100644 --- a/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/filebased/TenantRepository.java +++ b/org.eclipse.winery.repository/src/main/java/org/eclipse/winery/repository/filebased/TenantRepository.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; @@ -40,6 +41,7 @@ import org.eclipse.winery.repository.backend.NamespaceManager; import org.eclipse.winery.repository.backend.RepositoryFactory; import org.eclipse.winery.repository.backend.filebased.AbstractFileBasedRepository; +import org.eclipse.winery.repository.backend.filebased.FileUtils; import org.eclipse.winery.repository.backend.filebased.GitBasedRepository; import org.eclipse.winery.repository.common.RepositoryFileReference; import org.eclipse.winery.repository.exceptions.WineryRepositoryException; @@ -50,6 +52,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.winery.repository.backend.constants.Filename.FILENAME_TENANT_REPOSITORIES; + public class TenantRepository implements IWrappingRepository { private static final Logger LOGGER = LoggerFactory.getLogger(TenantRepository.class); @@ -71,13 +75,27 @@ public TenantRepository(Path repositoryRoot) throws IOException, GitAPIException new GitBasedRepositoryConfiguration(false, fileBasedConfig) ); - File[] files = this.repositoryRoot.toFile().listFiles(File::isDirectory); - if (files != null) { - for (File file : files) { - if (!".git".equals(file.getName()) || !defaultRepositoryFolder.equals(file.getName())) { - initTenantRepository(file.getName()); + if (repositoryRoot.resolve(FILENAME_TENANT_REPOSITORIES).toFile().exists()) { + File[] files = this.repositoryRoot.toFile().listFiles(File::isDirectory); + if (files != null) { + for (File file : files) { + if (!".git".equals(file.getName()) || !defaultRepositoryFolder.equals(file.getName())) { + initTenantRepository(file.getName()); + } } } + } else { + ArrayList ignoreFiles = new ArrayList<>(); + ignoreFiles.add(defaultRepositoryFolder); + Path targetPath = repositoryRoot.resolve(defaultRepositoryFolder); + FileUtils.copyFiles(this.repositoryRoot, targetPath, ignoreFiles); + try { + FileUtils.forceDeleteFile(this.repositoryRoot.resolve(".git").toFile()); + } catch (Exception e) { + LOGGER.error("Failed to delete .git directory in root folder of the MultiRepository"); + } + FileUtils.deleteFiles(this.repositoryRoot, ignoreFiles); + repositoryRoot.resolve(FILENAME_TENANT_REPOSITORIES).toFile().createNewFile(); } } diff --git a/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/xml/XmlRepositoryIntegrationTests.java b/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/xml/XmlRepositoryIntegrationTests.java index 7d1cf304da..1703fd4696 100644 --- a/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/xml/XmlRepositoryIntegrationTests.java +++ b/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/xml/XmlRepositoryIntegrationTests.java @@ -28,6 +28,7 @@ import org.eclipse.winery.repository.TestWithGitBackedRepository; import org.eclipse.winery.repository.backend.BackendUtils; +import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.BeforeEach; @@ -56,7 +57,9 @@ public void roundTripDoesNotChangeContent() { TDefinitions retrieved = repository.getDefinitions(definitionsId); try { repository.putDefinition(definitionsId, retrieved); + Git git = this.getGit(); final Status gitStatus = git.status().call(); + git.close(); assertTrue(gitStatus.isClean(), "Failed for definitionsId " + definitionsId); } catch (IOException | GitAPIException e) { Preconditions.condition(false, "Exception occurred during validation"); diff --git a/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/yaml/YamlRepositoryIntegrationTests.java b/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/yaml/YamlRepositoryIntegrationTests.java index edae9b097c..dc0dbcb8bb 100644 --- a/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/yaml/YamlRepositoryIntegrationTests.java +++ b/org.eclipse.winery.repository/src/test/java/org/eclipse/winery/repository/yaml/YamlRepositoryIntegrationTests.java @@ -28,6 +28,7 @@ import org.eclipse.winery.repository.TestWithGitBackedRepository; import org.eclipse.winery.repository.backend.BackendUtils; +import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.Disabled; @@ -137,7 +138,9 @@ public void roundTripDoesNotChangeContent() throws Exception { TDefinitions retrieved = repository.getDefinitions(definitionsId); try { repository.putDefinition(definitionsId, retrieved); + Git git = this.getGit(); final Status gitStatus = git.status().call(); + git.close(); assertTrue(gitStatus.isClean(), "Failed for definitionsId " + definitionsId); } catch (IOException | GitAPIException e) { Preconditions.condition(false, "Exception occurred during validation");