diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 93325c9df2..ea7eecb6fd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -270,26 +270,27 @@ jobs: services: # First group of services represents central apiml instance with central gateway registry + api-catalog-services: + image: ghcr.io/balhar-jakub/api-catalog-services:${{ github.run_id }}-${{ github.run_number }} + volumes: + - /api-defs:/api-defs discovery-service: image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} - gateway-service: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_APIMLID: central-apiml - APIML_SERVICE_HOSTNAME: gateway-service zaas-service: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates - central-gateway-service: + gateway-service: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_APIMLID: central-apiml - APIML_SERVICE_HOSTNAME: central-gateway-service + APIML_SERVICE_HOSTNAME: gateway-service APIML_GATEWAY_REGISTRY_ENABLED: true APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} # Second group of services represents domain apiml instance which registers it's gateway in central's discovery service discovery-service-2: @@ -299,16 +300,6 @@ jobs: env: APIML_SERVICE_HOSTNAME: discovery-service-2 APIML_SERVICE_PORT: 10031 - gateway-service-2: - image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} - env: - APIML_SERVICE_APIMLID: domain-apiml - APIML_SERVICE_HOSTNAME: gateway-service-2 - APIML_SERVICE_PORT: 10037 - APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: / - ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: / zaas-service-2: image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }} env: @@ -316,11 +307,11 @@ jobs: APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ - central-gateway-service-2: + gateway-service-2: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: APIML_SERVICE_APIMLID: domain-apiml - APIML_SERVICE_HOSTNAME: central-gateway-service-2 + APIML_SERVICE_HOSTNAME: gateway-service-2 APIML_GATEWAY_REGISTRY_ENABLED: false APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ @@ -447,7 +438,6 @@ jobs: gateway-service: image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} env: - APIML_SECURITY_X509_ENABLED: true APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service:10010/gateway/certificates mock-services: diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java index 03cb15e6dc..edc2993115 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java @@ -384,13 +384,17 @@ APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) { String title = instanceInfo.getMetadata().get(SERVICE_TITLE); if (StringUtils.equalsIgnoreCase(GATEWAY.getServiceId(), serviceId)) { if (RegistrationType.of(instanceInfo.getMetadata()).isAdditional()) { - // additional registration for GW means domain one, update serviceId with the ApimlId + // additional registration for GW means domain one, update serviceId and basePath with the ApimlId String apimlId = instanceInfo.getMetadata().get(APIML_ID); if (apimlId != null) { serviceId = apimlId; + apiBasePath = String.join("/", "", serviceId.toLowerCase()); title += " (" + apimlId + ")"; } } + else { + apiBasePath = "/"; + } } return new APIService.Builder(StringUtils.lowerCase(serviceId)) diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java index 7728dff03e..20839e7376 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java @@ -546,6 +546,7 @@ void givenPrimaryInstance_whenCreateDto_thenDoNotUpdateTitle() { var dto = createDto(RegistrationType.ADDITIONAL); assertEquals("title (apimlId)", dto.getTitle()); assertEquals("apimlid", dto.getServiceId()); + assertEquals("/apimlid", dto.getBasePath()); } @Test @@ -553,6 +554,7 @@ void givenPrimaryInstance_whenCreateDto_thenAddApimlIdIntoTitle() { var dto = createDto(RegistrationType.PRIMARY); assertEquals("title", dto.getTitle()); assertEquals("gateway", dto.getServiceId()); + assertEquals("/", dto.getBasePath()); } } diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js index 2290286c6b..fe6214ed4b 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/multiple-gateway-services.cy.js @@ -43,7 +43,7 @@ describe('>>> Multi-tenancy deployment test', () => { '#swaggerContainer > div > div:nth-child(2) > div.scheme-container > section > div:nth-child(1) > div > div > label > select > option' ) .should('exist') - .should('contain', `${baseUrl.match(/^https?:\/\/([^/?#]+)(?:[/?#]|$)/i)[1]}/gateway/api/v1`); + .should('contain', `${baseUrl.match(/^https?:\/\/([^/?#]+)(?:[/?#]|$)/i)[1]}/apiml2/gateway/api/v1`); cy.get('.tabs-container').should('not.exist'); cy.get('.serviceTab').should('exist').and('contain', 'API Gateway'); diff --git a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js index 9e01a1309c..7214f781d0 100644 --- a/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js +++ b/api-catalog-ui/frontend/cypress/e2e/detail-page/swagger-rendering.cy.js @@ -56,7 +56,12 @@ describe("Swagger rendering", () => { .get('label') .should('contain', "API Base Path:"); - const regex = new RegExp(`^\/${service.serviceId}\/api(\/v1)?$`); + let regexContent = `^\/${service.serviceId}\/api(\/v1)?$`; + if (service.serviceId === 'gateway') { + regexContent = '/'; + } + const regex = new RegExp(regexContent); + cy.get('@basePath') .get('#apiBasePath').invoke("text").should(text => { expect(text).to.match(regex); diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index c5bbe565da..7af0f3c103 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -36,6 +36,7 @@ "lodash": "4.17.21", "loglevel": "1.9.2", "openapi-snippet": "0.14.0", + "process": "0.11.10", "react": "18.3.1", "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index 5c8cda7ba5..8ee9b73005 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -32,6 +32,7 @@ "lodash": "4.17.21", "loglevel": "1.9.2", "openapi-snippet": "0.14.0", + "process": "0.11.10", "react": "18.3.1", "react-app-polyfill": "3.0.0", "react-dom": "18.3.1", @@ -136,8 +137,8 @@ "source-map-explorer": "2.5.3", "start-server-and-test": "2.0.8", "tmpl": "1.0.5", - "yaml": "2.6.0", - "undici": "6.19.8" + "undici": "6.19.8", + "yaml": "2.6.0" }, "overrides": { "nth-check": "2.1.1", diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx index 383f0d7806..0fda99ea06 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.jsx @@ -36,14 +36,19 @@ export default class ServiceTab extends Component { const { selectedVersion } = this.state; let basePath = ''; - if (selectedService.basePath) { - const version = selectedVersion || selectedService.defaultApiVersion; - let gatewayUrl = ''; - if (selectedService.apis && selectedService.apis[version] && selectedService.apis[version].gatewayUrl) { - gatewayUrl = selectedService.apis[version].gatewayUrl; + if (selectedService?.basePath) { + if (selectedService?.instances?.[0]?.includes('gateway')) { + // Return the basePath right away, since it's a GW instance (either primary or additional) + basePath = selectedService.basePath; + } else { + const version = selectedVersion || selectedService.defaultApiVersion; + let gatewayUrl = ''; + if (selectedService.apis && selectedService.apis[version] && selectedService.apis[version].gatewayUrl) { + gatewayUrl = selectedService.apis[version].gatewayUrl; + } + // Take the first part of the basePath and then add the gatewayUrl + basePath = `/${selectedService.serviceId}/${gatewayUrl}`; } - // Take the first part of the basePath and then add the gatewayUrl - basePath = `/${selectedService.serviceId}/${gatewayUrl}`; } return basePath; } @@ -321,6 +326,7 @@ ServiceTab.propTypes = { gatewayUrl: PropTypes.string, }) ), + instances: PropTypes.arrayOf(PropTypes.string), apiVersions: PropTypes.arrayOf(PropTypes.string), serviceId: PropTypes.string, status: PropTypes.string, diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx index a22c631bbb..15ba989606 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/ServiceTab.test.jsx @@ -31,6 +31,7 @@ const selectedService = { defaultApiVersion: ['org.zowe v1'], ssoAllInstances: true, apis: { 'org.zowe v1': { gatewayUrl: 'api/v1' } }, + instances: ["localhost:gateway:10010"] }; const selectedServiceDown = { diff --git a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx index 8fd836cb4c..603fd2e5f2 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx +++ b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.jsx @@ -14,23 +14,30 @@ import InstanceInfo from '../ServiceTab/InstanceInfo'; import getBaseUrl from '../../helpers/urls'; import { CustomizedSnippedGenerator } from '../../utils/generateSnippets'; import { AdvancedFilterPlugin } from '../../utils/filterApis'; +import PropTypes from "prop-types"; -function transformSwaggerToCurrentHost(swagger) { +function transformSwaggerToCurrentHost(swagger, selectedService) { swagger.host = window.location.host; - if (swagger.servers !== null && swagger.servers !== undefined) { + if (swagger.servers?.length) { swagger.servers.forEach((server) => { const location = `${window.location.protocol}//${window.location.host}`; try { const swaggerUrl = new URL(server.url); - server.url = location + swaggerUrl.pathname; + if (swaggerUrl?.pathname?.includes('gateway')) { + const basePath = selectedService?.basePath === '/' ? '' : selectedService?.basePath || ''; + + server.url = location + basePath + swaggerUrl.pathname; + } + else { + server.url = location + swaggerUrl.pathname; + } } catch (e) { // not a proper url, assume it is an endpoint server.url = location + server; } }); } - return swagger; } @@ -130,11 +137,9 @@ export default class SwaggerUIApiml extends Component { // If no version selected use the default apiDoc if ( (selectedVersion === null || selectedVersion === undefined) && - selectedService.apiDoc !== null && - selectedService.apiDoc !== undefined && - selectedService.apiDoc.length !== 0 + selectedService?.apiDoc?.length ) { - const swagger = transformSwaggerToCurrentHost(JSON.parse(selectedService.apiDoc)); + const swagger = transformSwaggerToCurrentHost(JSON.parse(selectedService.apiDoc), selectedService); this.setState({ swaggerReady: true, @@ -148,9 +153,9 @@ export default class SwaggerUIApiml extends Component { }, }); } - if (selectedVersion !== null && selectedVersion !== undefined) { + if (selectedVersion && selectedService) { const basePath = `${selectedService.serviceId}/${selectedVersion}`; - const url = `${getBaseUrl()}${process.env.REACT_APP_APIDOC_UPDATE}/${basePath}`; + const url = `${getBaseUrl()}${process?.env.REACT_APP_APIDOC_UPDATE}/${basePath}`; this.setState({ swaggerReady: true, swaggerProps: { @@ -161,7 +166,7 @@ export default class SwaggerUIApiml extends Component { plugins: [this.customPlugins, AdvancedFilterPlugin, CustomizedSnippedGenerator(codeSnippets)], responseInterceptor: (res) => { // response.text field is used to render the swagger - const swagger = transformSwaggerToCurrentHost(JSON.parse(res.text)); + const swagger = transformSwaggerToCurrentHost(JSON.parse(res.text), selectedService); res.text = JSON.stringify(swagger); return res; }, @@ -204,6 +209,13 @@ export default class SwaggerUIApiml extends Component { } } +SwaggerUIApiml.propTypes = { + selectedService: PropTypes.shape({ + apiDoc: PropTypes.string, + }).isRequired, + url: PropTypes.string, +}; + SwaggerUIApiml.defaultProps = { url: `${getBaseUrl()}/apidoc`, }; diff --git a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.test.jsx similarity index 89% rename from api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx rename to api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.test.jsx index 874199559f..f0557c609b 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx +++ b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUIApiml.test.jsx @@ -104,7 +104,8 @@ describe('>>> Swagger component tests', () => { const container = document.createElement('div'); document.body.appendChild(container); - await act(async () => createRoot(container).render(, container)); + const root = createRoot(container); + await act(async () => root.render()); expect(container.textContent).toContain(`API documentation could not be retrieved`); }); @@ -294,6 +295,44 @@ describe('>>> Swagger component tests', () => { expect(swaggerDiv).toBeDefined(); }); + it('should set correct Gateway server URL based on its basePath', async () => { + const endpoint = '/gateway/api/v1'; + const service = { + serviceId: 'gateway', + title: 'Gateway service', + description: 'Gateway service', + status: 'UP', + secured: true, + homePageUrl: 'http://localhost:10010/', + basePath: '/', + apiDoc: JSON.stringify({ + openapi: '3.0.0', + servers: [{ url: `https://bad.com${endpoint}` }], + }), + apis: { + default: { + apiId: 'gateway', + }, + }, + defaultApiVersion: 0, + }; + const wrapper = shallow( +
+ +
+ ); + + + const tiles = [{}]; + const container = document.createElement('div'); + + await act(async () => + createRoot(container).render(, container) + ); + expect(container.textContent).toContain(`Servershttp://localhost${endpoint}`); + + }); + it('should not create element api portal disabled and span already exists', () => { const service = { serviceId: 'testservice', diff --git a/api-catalog-ui/frontend/src/epics/fetch-tiles.jsx b/api-catalog-ui/frontend/src/epics/fetch-tiles.jsx index 482b6a41c8..36edcd30bd 100644 --- a/api-catalog-ui/frontend/src/epics/fetch-tiles.jsx +++ b/api-catalog-ui/frontend/src/epics/fetch-tiles.jsx @@ -11,6 +11,8 @@ import * as log from 'loglevel'; import { of, throwError, timer } from 'rxjs'; import { ofType } from 'redux-observable'; +import process from 'process'; +window.process = process; // Polyfill process for the browser import { catchError, debounceTime, exhaustMap, map, mergeMap, retryWhen, takeUntil } from 'rxjs/operators'; import { FETCH_TILES_REQUEST, FETCH_NEW_TILES_REQUEST, FETCH_TILES_STOP } from '../constants/catalog-tile-constants'; import { @@ -22,9 +24,9 @@ import { import { userActions } from '../actions/user-actions'; import getBaseUrl from '../helpers/urls'; -const updatePeriod = Number(process.env.REACT_APP_STATUS_UPDATE_PERIOD); -const debounce = Number(process.env.REACT_APP_STATUS_UPDATE_DEBOUNCE); -const scalingDuration = process.env.REACT_APP_STATUS_UPDATE_SCALING_DURATION; +const updatePeriod = Number(process?.env.REACT_APP_STATUS_UPDATE_PERIOD); +const debounce = Number(process?.env.REACT_APP_STATUS_UPDATE_DEBOUNCE); +const scalingDuration = process?.env.REACT_APP_STATUS_UPDATE_SCALING_DURATION; // terminate the epic if you get any of the following Ajax error codes const terminatingStatusCodes = [500, 401, 403]; @@ -33,11 +35,11 @@ const excludedMessageCodes = ['ZWEAM104']; function checkOrigin() { // only allow the gateway url to authenticate the user - let allowOrigin = process.env.REACT_APP_GATEWAY_URL; + let allowOrigin = process?.env.REACT_APP_GATEWAY_URL; if ( - process.env.REACT_APP_GATEWAY_URL === null || - process.env.REACT_APP_GATEWAY_URL === undefined || - process.env.REACT_APP_GATEWAY_URL === '' + process?.env.REACT_APP_GATEWAY_URL === null || + process?.env.REACT_APP_GATEWAY_URL === undefined || + process?.env.REACT_APP_GATEWAY_URL === '' ) { allowOrigin = window.location.origin; } @@ -53,7 +55,7 @@ function checkOrigin() { * @returns the URL to call */ function getUrl(action) { - let url = `${getBaseUrl()}${process.env.REACT_APP_CATALOG_UPDATE}`; + let url = `${getBaseUrl()}${process?.env.REACT_APP_CATALOG_UPDATE}`; if (action.payload !== undefined) { url += `/${action.payload}`; } @@ -83,7 +85,7 @@ function shouldTerminate(error) { export const retryMechanism = (scheduler) => - ({ maxRetries = Number(process.env.REACT_APP_STATUS_UPDATE_MAX_RETRIES) } = {}) => + ({ maxRetries = Number(process?.env.REACT_APP_STATUS_UPDATE_MAX_RETRIES) } = {}) => (attempts) => attempts.pipe( mergeMap((error, i) => { diff --git a/api-catalog-ui/frontend/src/index.js b/api-catalog-ui/frontend/src/index.js index f6d3d897b6..749bfd6270 100644 --- a/api-catalog-ui/frontend/src/index.js +++ b/api-catalog-ui/frontend/src/index.js @@ -8,6 +8,7 @@ * Copyright Contributors to the Zowe Project. */ +import process from 'process'; import 'react-app-polyfill/ie11'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; @@ -32,6 +33,8 @@ import { AsyncAppContainer } from './components/App/AsyncModules'; import('./index.css'); +window.process = process; // Polyfill process for the browser + function errorHandler(error, getState, lastAction, dispatch) { log.error(error); log.debug('current state', getState()); @@ -54,7 +57,7 @@ const epicMiddleware = createEpicMiddleware({ }); const composeEnhancers = compose; const middlewares = [epicMiddleware, thunk, reduxCatch(errorHandler)]; -if (process.env.NODE_ENV !== 'production') { +if (typeof process !== 'undefined' && process?.env?.NODE_ENV !== 'production') { middlewares.push(logger); } const persistedReducer = persistReducer(persistConfig, rootReducer); diff --git a/api-catalog-ui/frontend/src/utils/utilFunctions.js b/api-catalog-ui/frontend/src/utils/utilFunctions.js index 8d7848f8ac..ba19de70cd 100644 --- a/api-catalog-ui/frontend/src/utils/utilFunctions.js +++ b/api-catalog-ui/frontend/src/utils/utilFunctions.js @@ -14,6 +14,8 @@ export const isValidUrl = (url) => { try { return Boolean(new URL(url)); } catch (e) { + // eslint-disable-next-line no-console + console.error(`Invalid URL: ${url}, Error: ${e.message}`); return false; } }; @@ -93,8 +95,7 @@ export const customUIStyle = async (uiConfig) => { const root = document.documentElement; const logo = document.getElementById('logo'); if (logo && uiConfig.logo) { - const img = await fetchImagePath(); - logo.src = img; + logo.src = await fetchImagePath(); logo.style.height = 'auto'; logo.style.width = 'auto'; } diff --git a/config/local/multi-tenancy/README.md b/config/local/multi-tenancy/README.md new file mode 100644 index 0000000000..1d0cd54d80 --- /dev/null +++ b/config/local/multi-tenancy/README.md @@ -0,0 +1,17 @@ +# Deploying Services Locally Using Multi-Tenancy Setup + +Start the Domain services using the additional configuration in the [domain](./domain) folder, which overrides certain properties, along with the configuration in the [local](../../local). +You can do that by passing multiple files via `spring.config.additional-location`. + +**Example:** + +`spring.config.additional-location=file:./config/local/api-catalog-service.yml,file:./config/local/multi-tenancy/domain/api-catalog-service.yml` + +## Setting Up Mutual Registration +For mutual registration of the Central Gateway and Domain Gateway, you need to set the following environment variables to specify the discovery service URLs for registration: + +1. **Domain Gateway**: + Set the following environment variable: `ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS=https://localhost:10011/eureka` + +2. **Central Gateway**: + Set the following environment variable: `ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS=https://localhost:10021/eureka` diff --git a/config/local/multi-tenancy/domain/api-catalog-service.yml b/config/local/multi-tenancy/domain/api-catalog-service.yml new file mode 100644 index 0000000000..1855a635a0 --- /dev/null +++ b/config/local/multi-tenancy/domain/api-catalog-service.yml @@ -0,0 +1,6 @@ +apiml: + service: + port: 10024 + discoveryServiceUrls: https://localhost:10021/eureka/ + gatewayHostname: https://localhost:10020 + diff --git a/config/local/multi-tenancy/domain/discovery-service.yml b/config/local/multi-tenancy/domain/discovery-service.yml new file mode 100644 index 0000000000..6c5d16d366 --- /dev/null +++ b/config/local/multi-tenancy/domain/discovery-service.yml @@ -0,0 +1,5 @@ +apiml: + service: + port: 10021 + + diff --git a/config/local/multi-tenancy/domain/gateway-service.yml b/config/local/multi-tenancy/domain/gateway-service.yml new file mode 100644 index 0000000000..2469da958b --- /dev/null +++ b/config/local/multi-tenancy/domain/gateway-service.yml @@ -0,0 +1,8 @@ +apiml: + service: + apimlId: apiml2 + port: 10020 +eureka: + client: + serviceUrl: + defaultZone: https://localhost:10021/eureka/ diff --git a/config/local/multi-tenancy/domain/zaas-service.yml b/config/local/multi-tenancy/domain/zaas-service.yml new file mode 100644 index 0000000000..7d39879902 --- /dev/null +++ b/config/local/multi-tenancy/domain/zaas-service.yml @@ -0,0 +1,9 @@ +apiml: + service: + port: 10033 + discoveryServiceUrls: https://localhost:10021/eureka/ + + security: + x509: + certificatesUrl: https://localhost:10020/gateway/certificates + diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java index fff55eb448..b0b9a142d3 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/gateway/CentralRegistryTest.java @@ -10,11 +10,14 @@ package org.zowe.apiml.functional.gateway; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; import io.restassured.response.ValidatableResponse; import lombok.SneakyThrows; +import net.minidev.json.JSONArray; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -26,7 +29,9 @@ import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; import org.zowe.apiml.util.config.*; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -37,15 +42,21 @@ import static io.restassured.RestAssured.with; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.zowe.apiml.util.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; +import static org.zowe.apiml.util.SecurityUtils.gatewayToken; @DiscoverableClientDependentTest @Tag("GatewayCentralRegistry") class CentralRegistryTest implements TestWithStartedInstances { static final String CENTRAL_REGISTRY_PATH = "/" + CoreService.GATEWAY.getServiceId() + "/api/v1/registry"; + public static final String APIML_CONTAINER_PATH = "/" + CoreService.API_CATALOG.getServiceId() + "/api/v1/containers/apimediationlayer"; + public static final String DOMAIN_APIML = "domain-apiml"; + public static final String CENTRAL_APIML = "central-apiml"; - static ServiceConfiguration conf = ConfigReader.environmentConfiguration().getCentralGatewayServiceConfiguration(); + static ServiceConfiguration conf = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); static DiscoveryServiceConfiguration discoveryConf = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); @BeforeAll @@ -69,7 +80,7 @@ void setup() { void shouldFindRegisteredGatewayInCentralApiml() { ValidatableResponse response = listCentralRegistry("/central-apiml", "zowe.apiml.gateway", null); - List> services = response.extract().jsonPath().getObject("[0].services", new TypeRef>>() { + List> services = response.extract().jsonPath().getObject("[0].services", new TypeRef<>() { }); assertThat(services).hasSize(1); @@ -86,7 +97,7 @@ void shouldFindBothApimlIds() { List apimlIds = listCentralRegistry(null, null, null) .extract().jsonPath().getList("apimlId"); - assertThat(apimlIds).contains("central-apiml", "domain-apiml"); + assertThat(apimlIds).contains(CENTRAL_APIML, DOMAIN_APIML); } @Test @@ -99,11 +110,11 @@ void shouldFindTwoRegisteredGatewaysInTheEurekaApps() { .jsonPath() .getObject("applications.application.findAll { it.name == 'GATEWAY' }.instance.metadata", typeRef).get(0); - assertThat(metadata).hasSize(4); + assertThat(metadata).hasSize(2); assertThat(metadata) .extracting(map -> map.get("apiml.service.apimlId")) - .containsOnly("central-apiml", "domain-apiml"); + .containsOnly(CENTRAL_APIML, DOMAIN_APIML); } @Test @@ -152,6 +163,35 @@ private ValidatableResponse listEurekaApps() { .contentType(ContentType.JSON); } + @Test + void shouldContainCorrectBasePaths() throws MalformedURLException, URISyntaxException { + URI containers = new URL(conf.getScheme(), conf.getHost(), conf.getPort(), APIML_CONTAINER_PATH) + .toURI(); + + final String jwt = gatewayToken(); + String responseBody = with().given() + .header(ACCEPT, APPLICATION_JSON_VALUE) + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .get(containers) + .then() + .statusCode(200) + .contentType("application/json") + .extract() + .body() + .asString(); + + DocumentContext jsonContext = JsonPath.parse(responseBody); + + JSONArray gatewayBasePath = jsonContext.read("$[0].services[?(@.serviceId == 'gateway')].basePath"); + assertNotNull(gatewayBasePath, String.format("BasePath for central gw should not be null but it was '%s'", gatewayBasePath)); + assertFalse(gatewayBasePath.isEmpty(), String.format("BasePath for central gw should not be empty but it was '%s'", gatewayBasePath)); + assertEquals("/", gatewayBasePath.get(0)); + JSONArray domainGatewayBasePath = jsonContext.read("$[0].services[?(@.serviceId == 'domain-apiml')].basePath"); + assertNotNull(domainGatewayBasePath, String.format("BasePath for domain gw should not be null but it was '%s'", domainGatewayBasePath)); + assertFalse(domainGatewayBasePath.isEmpty(), String.format("BasePath for domain gw should not be empty but it was '%s'", domainGatewayBasePath)); + assertEquals("/" + DOMAIN_APIML, domainGatewayBasePath.get(0)); + } + @SneakyThrows private URI buildRegistryURI(String apimlId, String apiId, String serviceId) {