diff --git a/.changeset/fluffy-carrots-wink.md b/.changeset/fluffy-carrots-wink.md new file mode 100644 index 0000000000..a6f09d9558 --- /dev/null +++ b/.changeset/fluffy-carrots-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceRef` and `FeatureDiscoveryService`. diff --git a/.changeset/hot-planes-hear.md b/.changeset/hot-planes-hear.md new file mode 100644 index 0000000000..6ef29772ef --- /dev/null +++ b/.changeset/hot-planes-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Internal refactor to stop importing the removed `FeatureDiscoveryService` from `@backstage/backend-plugin-api`. diff --git a/.changeset/many-goats-flash.md b/.changeset/many-goats-flash.md new file mode 100644 index 0000000000..f3fec7e79a --- /dev/null +++ b/.changeset/many-goats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceFactory`. Existing usage can be replaced with `discoveryFeatureLoader` from `@backstage/backend-defaults`. diff --git a/.changeset/young-plants-report.md b/.changeset/young-plants-report.md new file mode 100644 index 0000000000..b35fb4b472 --- /dev/null +++ b/.changeset/young-plants-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +**BREAKING**: removed the deprecated `dynamicPluginsFeatureDiscoveryServiceFactory`. diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 071a4a4ba4..5e159be704 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -20,16 +20,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/packages/backend-app-api/report-alpha.api.md b/packages/backend-app-api/report-alpha.api.md deleted file mode 100644 index 163cc325a1..0000000000 --- a/packages/backend-app-api/report-alpha.api.md +++ /dev/null @@ -1,17 +0,0 @@ -## API Report File for "@backstage/backend-app-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; - -// @alpha @deprecated (undocumented) -export const featureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/backend-app-api/src/alpha.ts b/packages/backend-app-api/src/alpha.ts deleted file mode 100644 index a334214738..0000000000 --- a/packages/backend-app-api/src/alpha.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory'; diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts deleted file mode 100644 index 6550a6b1d0..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - startTestBackend, - mockServices, - createMockDirectory, -} from '@backstage/backend-test-utils'; -import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory'; - -const mockDir = createMockDirectory(); -process.argv[1] = mockDir.path; - -const pluginApiPath = require.resolve('@backstage/backend-plugin-api'); - -describe('featureDiscoveryServiceFactory', () => { - beforeEach(() => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'example-app', - dependencies: { - 'detected-plugin': '0.0.0', - 'detected-module': '0.0.0', - 'detected-plugin-with-alpha': '0.0.0', - 'detected-library': '0.0.0', - }, - }), - 'node_modules/detected-plugin': { - 'package.json': JSON.stringify({ - name: 'detected-plugin', - main: 'index.js', - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-module': { - 'package.json': JSON.stringify({ - name: 'detected-module', - main: 'index.js', - backstage: { - role: 'backend-plugin-module', - }, - }), - 'index.js': ` - const { createBackendModule, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendModule({ - pluginId: 'detected', - moduleId: 'derp', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-module'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-plugin-with-alpha': { - 'package.json': JSON.stringify({ - name: 'detected-plugin-with-alpha', - main: 'index.js', - exports: { - '.': { - default: 'index.js', - }, - './alpha': { - default: 'alpha.js', - }, - './package.json': './package.json', - }, - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': `exports.default = undefined;`, - 'alpha.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected-alpha', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin-with-alpha'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-library': { - 'package.json': JSON.stringify({ - name: 'detected-library', - main: 'index.js', - backstage: { - role: 'node-library', - }, - }), - 'index.js': ` - const { createServiceFactory, createServiceRef, coreServices } = require('${pluginApiPath}'); - exports.default = createServiceFactory({ - service: createServiceRef({ id: 'test', scope: 'root' }), - deps: { logger: coreServices.rootLogger }, - factory({ logger }) { - logger.warn('detected-library'); - return {}; - }, - }); - `, - }, - }); - }); - - it('should detect plugin and module packages when "all" is specified', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: 'all' } }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('detects only the packages that are listed as included', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-plugin-with-alpha', - 'detected-library', - ], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - }); - - it('does not detect packages when included is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect an excluded packages', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: ['detected-plugin', 'detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not excluded packages when it is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect packages that are included and excluded', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-module', - 'detected-plugin-with-alpha', - ], - exclude: ['detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - }); - - it('does not detect any packages when "packages" is empty', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: {} } }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); - - it('does not detect any packages when "packages" is not present', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: {} }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts deleted file mode 100644 index 05de659083..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { PackageDiscoveryService } from '../../../backend-defaults/src/PackageDiscoveryService'; - -/** - * @alpha - * @deprecated The `featureDiscoveryServiceFactory` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceFactory = createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - logger: coreServices.rootLogger, - }, - factory({ config, logger }) { - return new PackageDiscoveryService(config, logger); - }, -}); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 6bec5439ec..02fad4f217 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -37,7 +37,6 @@ import type { InternalServiceFactory } from '../../../backend-plugin-api/src/ser import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; import { instanceMetadataServiceRef, - featureDiscoveryServiceRef, BackendFeatureMeta, } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -252,20 +251,6 @@ export class BackendInitializer { this.#addFeature(await feature); } - const featureDiscovery = await this.#serviceRegistry.get( - // TODO: Let's leave this in place and remove it once the deprecated service is removed. We can do that post-1.0 since it's alpha - featureDiscoveryServiceRef, - 'root', - ); - - if (featureDiscovery) { - const { features } = await featureDiscovery.getBackendFeatures(); - for (const feature of features) { - this.#addFeature(unwrapFeature(feature)); - } - this.#serviceRegistry.checkForCircularDeps(); - } - await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); this.#serviceRegistry.add( diff --git a/packages/backend-defaults/src/PackageDiscoveryService.ts b/packages/backend-defaults/src/PackageDiscoveryService.ts index d7d127f4fa..90e8d0f59d 100644 --- a/packages/backend-defaults/src/PackageDiscoveryService.ts +++ b/packages/backend-defaults/src/PackageDiscoveryService.ts @@ -22,7 +22,6 @@ import { RootConfigService, RootLoggerService, } from '@backstage/backend-plugin-api'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { BackstagePackageJson } from '@backstage/cli-node'; const DETECTED_PACKAGE_ROLES = [ @@ -79,7 +78,7 @@ async function findClosestPackageDir( } /** @internal */ -export class PackageDiscoveryService implements FeatureDiscoveryService { +export class PackageDiscoveryService { constructor( private readonly config: RootConfigService, private readonly logger: RootLoggerService, diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ac03cceafa..2464b4cd14 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -45,7 +45,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "^0.25.0", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", @@ -74,6 +73,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", diff --git a/packages/backend-dynamic-feature-service/report.api.md b/packages/backend-dynamic-feature-service/report.api.md index e2ad34c84d..b0fa84edbd 100644 --- a/packages/backend-dynamic-feature-service/report.api.md +++ b/packages/backend-dynamic-feature-service/report.api.md @@ -13,7 +13,6 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; import { EventsService } from '@backstage/plugin-events-node'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { IndexBuilder } from '@backstage/plugin-search-backend-node'; @@ -150,13 +149,6 @@ export interface DynamicPluginsFactoryOptions { // @public @deprecated (undocumented) export const dynamicPluginsFeatureDiscoveryLoader: BackendFeature; -// @public @deprecated (undocumented) -export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @public export const dynamicPluginsFeatureLoader: (( options?: DynamicPluginsFeatureLoaderOptions, diff --git a/packages/backend-dynamic-feature-service/src/manager/index.ts b/packages/backend-dynamic-feature-service/src/manager/index.ts index d24a41a7a3..391ea46249 100644 --- a/packages/backend-dynamic-feature-service/src/manager/index.ts +++ b/packages/backend-dynamic-feature-service/src/manager/index.ts @@ -32,7 +32,6 @@ export type { export { DynamicPluginManager, - dynamicPluginsFeatureDiscoveryServiceFactory, dynamicPluginsServiceFactory, dynamicPluginsServiceFactoryWithOptions, dynamicPluginsServiceRef, diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 398e9fb14c..0162cbef2d 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -37,10 +37,6 @@ import { import { PackageRole, PackageRoles } from '@backstage/cli-node'; import { findPaths } from '@backstage/cli-common'; import * as fs from 'fs'; -import { - FeatureDiscoveryService, - featureDiscoveryServiceRef, -} from '@backstage/backend-plugin-api/alpha'; /** * @public @@ -307,55 +303,27 @@ export const dynamicPluginsServiceFactory = Object.assign( dynamicPluginsServiceFactoryWithOptions(), ); -class DynamicPluginsEnabledFeatureDiscoveryService - implements FeatureDiscoveryService -{ - constructor( - private readonly dynamicPlugins: DynamicPluginProvider, - private readonly featureDiscoveryService?: FeatureDiscoveryService, - ) {} +class DynamicPluginsEnabledFeatureDiscoveryService { + constructor(private readonly dynamicPlugins: DynamicPluginProvider) {} async getBackendFeatures(): Promise<{ features: Array }> { - const staticFeatures = - (await this.featureDiscoveryService?.getBackendFeatures())?.features ?? - []; - return { - features: [ - ...this.dynamicPlugins - .backendPlugins() - .flatMap((plugin): BackendFeature[] => { - if (plugin.installer?.kind === 'new') { - const installed = plugin.installer.install(); - if (Array.isArray(installed)) { - return installed; - } - return [installed]; + features: this.dynamicPlugins + .backendPlugins() + .flatMap((plugin): BackendFeature[] => { + if (plugin.installer?.kind === 'new') { + const installed = plugin.installer.install(); + if (Array.isArray(installed)) { + return installed; } - return []; - }), - ...staticFeatures, - ], + return [installed]; + } + return []; + }), }; } } -/** - * @public - * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. - */ -export const dynamicPluginsFeatureDiscoveryServiceFactory = - createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - dynamicPlugins: dynamicPluginsServiceRef, - }, - factory({ dynamicPlugins }) { - return new DynamicPluginsEnabledFeatureDiscoveryService(dynamicPlugins); - }, - }); - /** * @public * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 8509d1196d..d485e2132e 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @alpha (undocumented) @@ -18,21 +17,6 @@ export type BackendFeatureMeta = moduleId: string; }; -// @alpha (undocumented) -export interface FeatureDiscoveryService { - // (undocumented) - getBackendFeatures(): Promise<{ - features: Array; - }>; -} - -// @alpha @deprecated -export const featureDiscoveryServiceRef: ServiceRef< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @alpha (undocumented) export interface InstanceMetadataService { // (undocumented) diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts index 1a39fe6c23..6e91e10671 100644 --- a/packages/backend-plugin-api/src/alpha.ts +++ b/packages/backend-plugin-api/src/alpha.ts @@ -14,26 +14,7 @@ * limitations under the License. */ -import { - BackendFeature, - createServiceRef, -} from '@backstage/backend-plugin-api'; - -/** @alpha */ -export interface FeatureDiscoveryService { - getBackendFeatures(): Promise<{ features: Array }>; -} - -/** - * An optional service that can be used to dynamically load in additional BackendFeatures at runtime. - * @alpha - * @deprecated The `featureDiscoveryServiceRef` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceRef = - createServiceRef({ - id: 'core.featureDiscovery', - scope: 'root', - }); +import { createServiceRef } from '@backstage/backend-plugin-api'; /** * EXPERIMENTAL: Instance metadata service.