Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/boost/.changeset/ogx-tls-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-ogx-entity-provider': minor
---

Add per-provider TLS connection settings (`caData` and `skipTLSVerify`) to `OgxEntityProviderConfig` so `OgxModelEntityProvider` can fetch `/v1/models` from OGX endpoints that use a private CA or self-signed certificates.
86 changes: 84 additions & 2 deletions workspaces/boost/openspec/specs/ogx-entity-provider/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
>
> **Scope:** The independently deployable OGX model and agent entity
> providers, including their configuration, annotations, version handling,
> and synchronization behavior.
> TLS connection settings, and synchronization behavior.

## Purpose

This specification describes the shipped OGX entity provider: module
registration, configuration resolution, model and agent entity mapping,
annotation and version normalization, and scheduled full synchronization.
annotation and version normalization, TLS connection configuration, and
scheduled full synchronization.

## Requirements

Expand Down Expand Up @@ -51,6 +52,87 @@ URL when neither configuration path provides an OGX base URL.
- **WHEN** the OGX module reads configuration
- **THEN** it uses `http://localhost:8321`

#### Scenario: Read TLS settings from either configuration path

- **GIVEN** `caData` or `skipTLSVerify` is set under the OGX configuration in use
- **WHEN** the OGX module reads configuration
- **THEN** both settings are read from that path
- **AND** the same settings are supported on the `boost.providers.ogx` fallback path
- **AND** each is left unset when the configuration does not provide it

### Requirement: OGX configuration schema

The plugin SHALL declare its configuration schema so that Backstage validates
the OGX configuration keys and enforces their visibility when the module is
loaded independently of `boost-backend`.

#### Scenario: Declare the OGX configuration contract

- **GIVEN** the `ogx-entity-provider` package is installed
- **WHEN** Backstage loads the configuration schema
- **THEN** the package contributes a schema covering `boost.entityProviders.ogx`
and `boost.providers.ogx`
- **AND** `apiKey` is marked with `@visibility secret`
- **AND** `caData` is marked with `@visibility backend`
- **AND** `baseUrl` and `skipTLSVerify` are marked `@configScope yaml-only`

### Requirement: TLS connection configuration

The model provider SHALL apply the configured TLS settings when requesting the
OGX model endpoint. `skipTLSVerify` SHALL take precedence over `caData`. The
dispatcher SHALL be created once and reused across refresh cycles.

#### Scenario: Use default TLS behavior when nothing is configured

- **GIVEN** neither `caData` nor `skipTLSVerify` is set
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** it issues the request without a custom dispatcher
- **AND** the runtime default certificate verification applies

#### Scenario: Verify against a custom CA

- **GIVEN** `caData` contains a PEM-encoded certificate or bundle
- **AND** `skipTLSVerify` is not set
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** it issues the request with a dispatcher carrying that CA
- **AND** certificate verification remains enabled

#### Scenario: Disable certificate verification

- **GIVEN** `skipTLSVerify` is true
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** it issues the request with certificate verification disabled
- **AND** it logs a warning that this is intended for development environments only

#### Scenario: Prefer skipTLSVerify over caData

- **GIVEN** both `caData` and `skipTLSVerify` are set
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** certificate verification is disabled
- **AND** the configured `caData` is not applied

#### Scenario: Report malformed CA data without blocking the request

- **GIVEN** `caData` does not contain matching PEM certificate markers
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** it logs an error naming the expected PEM markers
- **AND** it still applies the configured `caData` and issues the request

#### Scenario: Reuse the dispatcher and warn only once

- **GIVEN** a TLS setting is configured
- **WHEN** the model provider refreshes repeatedly
- **THEN** the dispatcher is created on the first refresh and reused afterwards
- **AND** the `skipTLSVerify` warning is logged only once

#### Scenario: Preserve existing request behavior under TLS settings

- **GIVEN** a TLS setting is configured
- **AND** an API key is configured
- **WHEN** the model provider fetches the OGX model endpoint
- **THEN** the Bearer authorization header is still sent
- **AND** a non-2xx response is still treated as a failed fetch

### Requirement: Model-server entity emission

The model provider SHALL request the OGX `/v1/models` endpoint and emit one
Expand Down
80 changes: 80 additions & 0 deletions workspaces/boost/plugins/ogx-entity-provider/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

/**
* Configuration schema for the OGX entity provider module.
*
* Declares the config paths read by readOgxEntityProviderConfig so that
* Backstage validates and enforces visibility on these keys even if
* the module is loaded independently of boost-backend.
*/
export interface Config {
boost?: {
/** Entity-provider-specific config (standalone deployment). */
entityProviders?: {
/** OGX entity provider connection. */
ogx?: {
/**
* Base URL of the OGX API endpoint.
* @configScope yaml-only
*/
baseUrl?: string;
/**
* API key for authenticated endpoints.
* @visibility secret
*/
apiKey?: string;
/**
* PEM-encoded CA certificate or certificate bundle used to verify the OGX endpoint.
* @visibility backend
*/
caData?: string;
/**
* Disable TLS certificate verification. Development use only.
* @configScope yaml-only
Comment thread
gabemontero marked this conversation as resolved.
*/
skipTLSVerify?: boolean;
};
};

/** Provider module config (composed deployment). */
providers?: {
/** OGX provider connection. */
ogx?: {
/**
* Base URL of the OGX API endpoint.
* @configScope yaml-only
*/
baseUrl?: string;
/**
* API key for authenticated endpoints.
* @visibility secret
*/
apiKey?: string;
/**
* PEM-encoded CA certificate or certificate bundle used to verify the OGX endpoint.
* @visibility backend
*/
caData?: string;
/**
* Disable TLS certificate verification. Development use only.
* @configScope yaml-only
*/
skipTLSVerify?: boolean;
};
};
};
}
7 changes: 5 additions & 2 deletions workspaces/boost/plugins/ogx-entity-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@red-hat-developer-hub/backstage-plugin-ogx-entity-provider",
"version": "0.4.2",
"license": "Apache-2.0",
"configSchema": "config.d.ts",
"description": "OGX entity provider for the Backstage catalog — emits AI models and agents as catalog entities",
"main": "src/index.ts",
"types": "src/index.ts",
Expand Down Expand Up @@ -31,11 +32,13 @@
"@backstage/backend-plugin-api": "^1.10.0",
"@backstage/catalog-model": "^1.10.0",
"@backstage/plugin-catalog-node": "^2.2.4",
"@red-hat-developer-hub/backstage-plugin-boost-entity-provider-sdk": "workspace:^"
"@red-hat-developer-hub/backstage-plugin-boost-entity-provider-sdk": "workspace:^",
"undici": "^6.21.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.11.6",
"@backstage/cli": "^0.36.5"
"@backstage/cli": "^0.36.5",
"@backstage/config": "^1.3.2"
},
"sideEffects": false,
"scripts": {
Expand Down
116 changes: 116 additions & 0 deletions workspaces/boost/plugins/ogx-entity-provider/src/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright Red Hat, Inc.
*
* 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 { ConfigReader } from '@backstage/config';

import { readOgxEntityProviderConfig } from './module';

describe('readOgxEntityProviderConfig', () => {
it('reads caData and skipTLSVerify from boost.entityProviders.ogx', () => {
const config = new ConfigReader({
boost: {
entityProviders: {
ogx: {
baseUrl: 'https://ogx.example.com',
caData:
'-----BEGIN CERTIFICATE-----\nMIIBxTCC...\n-----END CERTIFICATE-----',
skipTLSVerify: true,
},
},
},
});

const result = readOgxEntityProviderConfig(config);

expect(result.baseUrl).toBe('https://ogx.example.com');
expect(result.caData).toBe(
'-----BEGIN CERTIFICATE-----\nMIIBxTCC...\n-----END CERTIFICATE-----',
);
expect(result.skipTLSVerify).toBe(true);
});

it('reads caData and skipTLSVerify from fallback boost.providers.ogx', () => {
const config = new ConfigReader({
boost: {
providers: {
ogx: {
baseUrl: 'https://ogx-fallback.example.com',
caData: 'PEM-CERT-DATA',
skipTLSVerify: false,
},
},
},
});

const result = readOgxEntityProviderConfig(config);

expect(result.baseUrl).toBe('https://ogx-fallback.example.com');
expect(result.caData).toBe('PEM-CERT-DATA');
expect(result.skipTLSVerify).toBe(false);
});

it('returns undefined for caData and skipTLSVerify when not configured', () => {
const config = new ConfigReader({
boost: {
entityProviders: {
ogx: {
baseUrl: 'http://localhost:8321',
},
},
},
});

const result = readOgxEntityProviderConfig(config);

expect(result.baseUrl).toBe('http://localhost:8321');
expect(result.caData).toBeUndefined();
expect(result.skipTLSVerify).toBeUndefined();
});

it('falls back to localhost when no OGX config is present', () => {
const config = new ConfigReader({});

const result = readOgxEntityProviderConfig(config);

expect(result.baseUrl).toBe('http://localhost:8321');
expect(result.caData).toBeUndefined();
expect(result.skipTLSVerify).toBeUndefined();
});

it('prefers entityProviders.ogx over providers.ogx', () => {
const config = new ConfigReader({
boost: {
entityProviders: {
ogx: {
baseUrl: 'https://primary.example.com',
caData: 'PRIMARY-CA',
},
},
providers: {
ogx: {
baseUrl: 'https://fallback.example.com',
caData: 'FALLBACK-CA',
},
},
},
});

const result = readOgxEntityProviderConfig(config);

expect(result.baseUrl).toBe('https://primary.example.com');
expect(result.caData).toBe('PRIMARY-CA');
});
});
10 changes: 9 additions & 1 deletion workspaces/boost/plugins/ogx-entity-provider/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const DEFAULT_AGENT_REFRESH_SECONDS = 300;
* ogx:
* baseUrl: http://localhost:8321
* apiKey: ${OGX_API_KEY} # optional
* caData: '-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----' # optional
* skipTLSVerify: false # optional
* modelRefreshIntervalSeconds: 60
* agentRefreshIntervalSeconds: 300
* agents:
Expand Down Expand Up @@ -114,8 +116,10 @@ export const catalogModuleOgxEntityProvider = createBackendModule({

/**
* Read OGX entity provider configuration from app-config.yaml.
Comment thread
gabemontero marked this conversation as resolved.
*
Comment thread
gabemontero marked this conversation as resolved.
Comment thread
gabemontero marked this conversation as resolved.
Comment thread
gabemontero marked this conversation as resolved.
* @internal Exported for testing only.
*/
function readOgxEntityProviderConfig(
export function readOgxEntityProviderConfig(
config: typeof coreServices.rootConfig extends { T: infer T } ? T : never,
): OgxEntityProviderConfig {
// Try the entity-provider-specific config first
Expand All @@ -134,6 +138,8 @@ function readOgxEntityProviderConfig(
defaultAgent: epConfig.getOptionalString('defaultAgent'),
Comment thread
gabemontero marked this conversation as resolved.
maxAgentTurns: epConfig.getOptionalNumber('maxAgentTurns'),
agents: readAgentConfigs(epConfig),
caData: epConfig.getOptionalString('caData'),
skipTLSVerify: epConfig.getOptionalBoolean('skipTLSVerify'),
};
}

Expand All @@ -147,6 +153,8 @@ function readOgxEntityProviderConfig(
defaultAgent: providerConfig.getOptionalString('defaultAgent'),
maxAgentTurns: providerConfig.getOptionalNumber('maxAgentTurns'),
agents: readAgentConfigs(providerConfig),
caData: providerConfig.getOptionalString('caData'),
skipTLSVerify: providerConfig.getOptionalBoolean('skipTLSVerify'),
};
}

Expand Down
Loading
Loading