feat(web): integration-hub + wizard wiring for local-setup flow
Integration hub: extends integration.models with fields needed by the wizard (capabilities, credentials, readiness), updates the shell and list components, adds routing for the new hub flow, and broadens the integration-list spec. Integration wizard: new integrations-hub.component, extended wizard with capability/credential handling, updated template + type models, and broader spec coverage. Sprint docs: SPRINT_20260413_003 (UI-driven local setup rerun) updated with wiring notes; SPRINT_20260410_001 (no-mocks) adjusted. ReleaseOrchestrator architecture doc gets a minor clarification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,10 @@
|
||||
* scm - Source control managers
|
||||
* ci - CI/CD systems
|
||||
* runtime-hosts - Runtime and host connector inventory
|
||||
* feeds - Advisory source connectors
|
||||
* feeds - Feed mirror connectors
|
||||
* vex-sources - VEX source connectors
|
||||
* secrets - Secrets managers / vaults
|
||||
* storage - Object storage connectors
|
||||
* registry-admin - Registry plan management and audit (Sprint 023)
|
||||
* :id - Integration detail (standard contract template)
|
||||
*
|
||||
@@ -76,6 +77,13 @@ export const integrationHubRoutes: Routes = [
|
||||
loadComponent: () =>
|
||||
import('./integration-list.component').then((m) => m.IntegrationListComponent),
|
||||
},
|
||||
{
|
||||
path: 'feeds',
|
||||
title: 'Feed Mirrors',
|
||||
data: { breadcrumb: 'Feed Mirrors', type: 'FeedMirror' },
|
||||
loadComponent: () =>
|
||||
import('./integration-list.component').then((m) => m.IntegrationListComponent),
|
||||
},
|
||||
{
|
||||
path: 'advisory-vex-sources',
|
||||
title: 'Advisory & VEX Sources',
|
||||
@@ -112,6 +120,13 @@ export const integrationHubRoutes: Routes = [
|
||||
loadComponent: () =>
|
||||
import('./integration-list.component').then((m) => m.IntegrationListComponent),
|
||||
},
|
||||
{
|
||||
path: 'storage',
|
||||
title: 'Object Storage',
|
||||
data: { breadcrumb: 'Object Storage', type: 'ObjectStorage' },
|
||||
loadComponent: () =>
|
||||
import('./integration-list.component').then((m) => m.IntegrationListComponent),
|
||||
},
|
||||
|
||||
{
|
||||
path: 'notifications',
|
||||
|
||||
@@ -111,6 +111,18 @@ describe('IntegrationListComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('routes object-storage inventory to the storage onboarding flow', () => {
|
||||
const navigateSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
|
||||
(component as any).integrationType = IntegrationType.ObjectStorage;
|
||||
component.typeLabel = 'Object Storage';
|
||||
|
||||
component.addIntegration();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['/ops/integrations', 'onboarding', 'storage'], {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows inline success feedback instead of alert on successful test-connection', () => {
|
||||
integrationService.testConnection.and.returnValue(of({
|
||||
integrationId: 'int-1',
|
||||
|
||||
@@ -405,6 +405,8 @@ export class IntegrationListComponent implements OnInit {
|
||||
SecretsManager: 'Secrets Vault',
|
||||
FeedMirror: 'Feed Mirror',
|
||||
Feed: 'Feed Mirror',
|
||||
ObjectStorage: 'Object Storage',
|
||||
Storage: 'Object Storage',
|
||||
SymbolSource: 'Symbol Source',
|
||||
Marketplace: 'Marketplace',
|
||||
Notification: 'Notification Provider',
|
||||
@@ -616,6 +618,7 @@ export class IntegrationListComponent implements OnInit {
|
||||
case 'FeedMirror': case 'Feed': return IntegrationType.FeedMirror;
|
||||
case 'RepoSource': return IntegrationType.RepoSource;
|
||||
case 'SecretsManager': case 'Secrets': return IntegrationType.SecretsManager;
|
||||
case 'ObjectStorage': case 'Storage': return IntegrationType.ObjectStorage;
|
||||
case 'SymbolSource': return IntegrationType.SymbolSource;
|
||||
case 'Marketplace': return IntegrationType.Marketplace;
|
||||
default: return undefined;
|
||||
@@ -630,6 +633,7 @@ export class IntegrationListComponent implements OnInit {
|
||||
case IntegrationType.FeedMirror: return 'feed';
|
||||
case IntegrationType.RepoSource: return 'repo';
|
||||
case IntegrationType.SecretsManager: return 'secrets';
|
||||
case IntegrationType.ObjectStorage: return 'storage';
|
||||
case IntegrationType.Registry:
|
||||
default: return 'registry';
|
||||
}
|
||||
@@ -640,7 +644,10 @@ export class IntegrationListComponent implements OnInit {
|
||||
this.integrationType === IntegrationType.Registry ||
|
||||
this.integrationType === IntegrationType.Scm ||
|
||||
this.integrationType === IntegrationType.CiCd ||
|
||||
this.integrationType === IntegrationType.RuntimeHost
|
||||
this.integrationType === IntegrationType.RuntimeHost ||
|
||||
this.integrationType === IntegrationType.FeedMirror ||
|
||||
this.integrationType === IntegrationType.SecretsManager ||
|
||||
this.integrationType === IntegrationType.ObjectStorage
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/
|
||||
import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component';
|
||||
import { IntegrationHubComponent } from './integration-hub.component';
|
||||
|
||||
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'advisory-vex-sources' | 'secrets';
|
||||
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'feeds' | 'advisory-vex-sources' | 'secrets' | 'storage';
|
||||
|
||||
const KNOWN_TAB_IDS: readonly string[] = [
|
||||
'registries', 'scm', 'ci', 'runtime-hosts', 'advisory-vex-sources', 'secrets',
|
||||
'registries', 'scm', 'ci', 'runtime-hosts', 'feeds', 'advisory-vex-sources', 'secrets', 'storage',
|
||||
];
|
||||
|
||||
const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
@@ -18,12 +18,14 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
{ id: 'scm', label: 'SCM', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
|
||||
{ id: 'ci', label: 'CI/CD', icon: 'M5 3l14 9-14 9V3z' },
|
||||
{ id: 'runtime-hosts', label: 'Runtimes / Hosts', icon: 'M2 2h20v8H2z|||M2 14h20v8H2z|||M6 6h.01|||M6 18h.01' },
|
||||
{ id: 'feeds', label: 'Feed Mirrors', icon: 'M4 6h16|||M4 12h16|||M4 18h10' },
|
||||
{ id: 'advisory-vex-sources', label: 'Advisory & VEX', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'secrets', label: 'Secrets', icon: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' },
|
||||
{ id: 'storage', label: 'Storage', icon: 'M3 6h18|||M5 6v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6|||M8 10h8|||M8 14h5' },
|
||||
];
|
||||
|
||||
/** Priority order for auto-selecting the first populated tab */
|
||||
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'advisory-vex-sources', 'runtime-hosts', 'secrets'];
|
||||
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'feeds', 'advisory-vex-sources', 'runtime-hosts', 'secrets', 'storage'];
|
||||
|
||||
@Component({
|
||||
selector: 'app-integration-shell',
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum IntegrationType {
|
||||
SymbolSource = 7,
|
||||
Marketplace = 8,
|
||||
SecretsManager = 9,
|
||||
ObjectStorage = 10,
|
||||
}
|
||||
|
||||
export enum IntegrationProvider {
|
||||
@@ -39,6 +40,7 @@ export enum IntegrationProvider {
|
||||
NuGetOrg = 403,
|
||||
CratesIo = 404,
|
||||
GoProxy = 405,
|
||||
S3Compatible = 450,
|
||||
EbpfAgent = 500,
|
||||
EtwAgent = 501,
|
||||
DyldInterposer = 502,
|
||||
@@ -168,6 +170,8 @@ export function getIntegrationTypeLabel(type: IntegrationType): string {
|
||||
return 'Marketplace';
|
||||
case IntegrationType.SecretsManager:
|
||||
return 'Secrets Manager';
|
||||
case IntegrationType.ObjectStorage:
|
||||
return 'Object Storage';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
@@ -293,6 +297,8 @@ export function getProviderLabel(provider: IntegrationProvider): string {
|
||||
return 'crates.io';
|
||||
case IntegrationProvider.GoProxy:
|
||||
return 'Go Proxy';
|
||||
case IntegrationProvider.S3Compatible:
|
||||
return 'S3-Compatible Storage';
|
||||
case IntegrationProvider.EbpfAgent:
|
||||
return 'eBPF Agent';
|
||||
case IntegrationProvider.EtwAgent:
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
@if (currentStep() === 'auth') {
|
||||
<div class="step-content">
|
||||
<h2>Connection & Credentials</h2>
|
||||
<p class="step-description">StellaOps stores only AuthRef URIs here. Keep the actual secret in your vault.</p>
|
||||
<p class="step-description">Prefer AuthRef URIs for credentials. Local public probes can be created without one when the provider supports it.</p>
|
||||
|
||||
@if (selectedProvider(); as provider) {
|
||||
<div class="auth-method-card selected">
|
||||
@@ -83,7 +83,9 @@
|
||||
<div class="form-field">
|
||||
<label for="authRefUri">
|
||||
AuthRef URI
|
||||
<span class="required">*</span>
|
||||
@if (isAuthRefRequired()) {
|
||||
<span class="required">*</span>
|
||||
}
|
||||
</label>
|
||||
<input
|
||||
id="authRefUri"
|
||||
@@ -139,10 +141,10 @@
|
||||
@if (currentStep() === 'scope') {
|
||||
<div class="step-content">
|
||||
<h2>Discovery Scope</h2>
|
||||
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector.</p>
|
||||
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector when discovery scope matters.</p>
|
||||
|
||||
<div class="scope-form">
|
||||
@if (integrationType() === 'registry' || integrationType() === 'scm') {
|
||||
@if (hasScopeSelectors()) {
|
||||
<div class="form-field">
|
||||
<label for="repositories">Repositories</label>
|
||||
<textarea
|
||||
@@ -188,7 +190,17 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<span class="field-hint">At least one owner, repository, namespace, branch, or tag scope is required before creation.</span>
|
||||
@if (!hasScopeSelectors()) {
|
||||
<div class="check-item status-warning">
|
||||
<span class="check-status">i</span>
|
||||
<div class="check-info">
|
||||
<span class="check-name">Optional scope</span>
|
||||
<span class="check-message">This connector can be created without extra discovery filters and tightened later.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<span class="field-hint">{{ getScopeGuidance() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,18 +2,21 @@ import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IntegrationProvider, IntegrationType } from '../integration-hub/integration.models';
|
||||
import { IntegrationWizardComponent } from './integration-wizard.component';
|
||||
import { resolveSupportedProviders } from './models/integration.models';
|
||||
import { IntegrationOnboardingType, resolveSupportedProviders } from './models/integration.models';
|
||||
|
||||
describe('IntegrationWizardComponent', () => {
|
||||
let component: IntegrationWizardComponent;
|
||||
|
||||
function createComponent() {
|
||||
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
|
||||
(component as any).integrationType = () => 'scm';
|
||||
(component as any).supportedProviders = () => resolveSupportedProviders('scm', [
|
||||
function createComponent(
|
||||
type: IntegrationOnboardingType = 'scm',
|
||||
catalog = [
|
||||
{ name: 'github-app', type: IntegrationType.Scm, provider: IntegrationProvider.GitHubApp },
|
||||
]);
|
||||
component.draft.update((draft) => ({ ...draft, type: 'scm' }));
|
||||
],
|
||||
) {
|
||||
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
|
||||
(component as any).integrationType = () => type;
|
||||
(component as any).supportedProviders = () => resolveSupportedProviders(type, catalog);
|
||||
component.draft.update((draft) => ({ ...draft, type }));
|
||||
}
|
||||
|
||||
it('selects a supported provider and seeds its default endpoint', () => {
|
||||
@@ -25,20 +28,32 @@ describe('IntegrationWizardComponent', () => {
|
||||
expect(component.draft().name).toContain('GitHub App');
|
||||
});
|
||||
|
||||
it('requires AuthRef URI and provider metadata before leaving the connection step', () => {
|
||||
it('requires provider metadata but allows optional AuthRef for local-friendly providers', () => {
|
||||
createComponent();
|
||||
component.selectProvider(IntegrationProvider.GitHubApp);
|
||||
component.currentStep.set('auth');
|
||||
|
||||
expect(component.canGoNext()).toBeFalse();
|
||||
|
||||
component.updateAuthRefUri('authref://vault/github#app');
|
||||
component.updateConfigField('appId', '12345');
|
||||
component.updateConfigField('installationId', '67890');
|
||||
|
||||
expect(component.canGoNext()).toBeTrue();
|
||||
});
|
||||
|
||||
it('allows no-auth and no-scope onboarding for local object storage connectors', () => {
|
||||
createComponent('storage', [
|
||||
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
|
||||
]);
|
||||
component.selectProvider(IntegrationProvider.S3Compatible);
|
||||
|
||||
component.currentStep.set('auth');
|
||||
expect(component.canGoNext()).toBeTrue();
|
||||
|
||||
component.currentStep.set('scope');
|
||||
expect(component.canGoNext()).toBeTrue();
|
||||
});
|
||||
|
||||
it('emits a canonical create request instead of a UI-only draft', () => {
|
||||
createComponent();
|
||||
component.selectProvider(IntegrationProvider.GitHubApp);
|
||||
@@ -73,4 +88,26 @@ describe('IntegrationWizardComponent', () => {
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('emits null authRefUri for optional-auth object storage connectors', () => {
|
||||
createComponent('storage', [
|
||||
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
|
||||
]);
|
||||
component.selectProvider(IntegrationProvider.S3Compatible);
|
||||
const emitSpy = jasmine.createSpy('emit');
|
||||
spyOn(component.create, 'emit').and.callFake(emitSpy);
|
||||
|
||||
component.updateName('Local MinIO');
|
||||
component.currentStep.set('review');
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
name: 'Local MinIO',
|
||||
type: IntegrationType.ObjectStorage,
|
||||
provider: IntegrationProvider.S3Compatible,
|
||||
endpoint: 'http://minio.stella-ops.local:9000',
|
||||
authRefUri: null,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ export class IntegrationWizardComponent {
|
||||
readonly selectedProvider = computed(() => resolveProviderDefinition(this.draft().provider));
|
||||
readonly deploymentTemplate = computed(() => null);
|
||||
readonly copySafetyGuidance = computed(() => [
|
||||
'Only AuthRef URIs are stored. Keep the underlying secret in a vault.',
|
||||
'Use AuthRef URIs whenever the connector requires credentials; local public probes can be created without one.',
|
||||
'Non-secret provider fields such as App ID or Installation ID are stored as connector metadata.',
|
||||
'Use environment-specific endpoints so prod, stage, and lab connectors remain explicit.',
|
||||
]);
|
||||
@@ -302,11 +302,36 @@ export class IntegrationWizardComponent {
|
||||
return 'CI/CD';
|
||||
case 'host':
|
||||
return 'Host';
|
||||
case 'feed':
|
||||
return 'Feed Mirror';
|
||||
case 'secrets':
|
||||
return 'Secrets';
|
||||
case 'storage':
|
||||
return 'Object Storage';
|
||||
default:
|
||||
return 'Integration';
|
||||
}
|
||||
}
|
||||
|
||||
isAuthRefRequired(): boolean {
|
||||
return this.selectedProvider()?.authRefRequired ?? true;
|
||||
}
|
||||
|
||||
hasScopeSelectors(): boolean {
|
||||
const type = this.integrationType();
|
||||
return type === 'registry' || type === 'scm';
|
||||
}
|
||||
|
||||
isScopeRequired(): boolean {
|
||||
return this.selectedProvider()?.scopeRequired ?? this.hasScopeSelectors();
|
||||
}
|
||||
|
||||
getScopeGuidance(): string {
|
||||
return this.isScopeRequired()
|
||||
? 'At least one owner, repository, namespace, branch, or tag scope is required before creation.'
|
||||
: 'Scope is optional. Leave it blank to create the connector with endpoint-level health probing only.';
|
||||
}
|
||||
|
||||
copyToClipboard(text: string): void {
|
||||
void navigator.clipboard.writeText(text);
|
||||
}
|
||||
@@ -330,7 +355,11 @@ export class IntegrationWizardComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (draft.endpoint.trim().length === 0 || draft.authRefUri.trim().length === 0) {
|
||||
if (draft.endpoint.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAuthRefRequired() && draft.authRefUri.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -341,6 +370,10 @@ export class IntegrationWizardComponent {
|
||||
|
||||
private isScopeValid(): boolean {
|
||||
const draft = this.draft();
|
||||
if (!this.isScopeRequired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
draft.organizationId.trim().length > 0 ||
|
||||
draft.repositories.length > 0 ||
|
||||
@@ -372,9 +405,23 @@ export class IntegrationWizardComponent {
|
||||
private getPreflightChecks(): PreflightCheck[] {
|
||||
const provider = this.selectedProvider();
|
||||
const checks: PreflightCheck[] = [
|
||||
{ id: 'authref', name: 'Credential indirection', description: 'Verify the connector uses an AuthRef URI instead of raw secrets.', status: 'pending' },
|
||||
{
|
||||
id: 'authref',
|
||||
name: 'Credential indirection',
|
||||
description: this.isAuthRefRequired()
|
||||
? 'Verify the connector uses an AuthRef URI instead of raw secrets.'
|
||||
: 'Prefer AuthRef URI indirection when the connector needs credentials; allow empty auth for local public probes.',
|
||||
status: 'pending',
|
||||
},
|
||||
{ id: 'endpoint', name: 'Endpoint contract', description: 'Confirm the endpoint matches the provider health/test contract.', status: 'pending' },
|
||||
{ id: 'scope', name: 'Discovery scope', description: 'Ensure at least one repository, namespace, branch, or owner scope is set.', status: 'pending' },
|
||||
{
|
||||
id: 'scope',
|
||||
name: 'Discovery scope',
|
||||
description: this.isScopeRequired()
|
||||
? 'Ensure at least one repository, namespace, branch, or owner scope is set.'
|
||||
: 'Scope is optional for this connector; endpoint-only health checks are allowed.',
|
||||
status: 'pending',
|
||||
},
|
||||
{ id: 'schedule', name: 'Probe schedule', description: 'Validate the deterministic check cadence for this connector.', status: 'pending' },
|
||||
];
|
||||
|
||||
@@ -397,6 +444,12 @@ export class IntegrationWizardComponent {
|
||||
|
||||
switch (check.id) {
|
||||
case 'authref':
|
||||
if (draft.authRefUri.trim().length === 0) {
|
||||
return this.isAuthRefRequired()
|
||||
? { status: 'error', message: 'This connector requires an authref:// URI instead of embedding secrets in the connector.' }
|
||||
: { status: 'warning', message: 'No AuthRef configured; the connector will rely on anonymous or endpoint-only health probing.' };
|
||||
}
|
||||
|
||||
return draft.authRefUri.trim().startsWith('authref://')
|
||||
? { status: 'success', message: 'Credential indirection is configured via AuthRef URI.' }
|
||||
: { status: 'error', message: 'Use an authref:// URI instead of embedding secrets in the connector.' };
|
||||
@@ -405,9 +458,13 @@ export class IntegrationWizardComponent {
|
||||
? { status: 'success', message: `Endpoint ${draft.endpoint.trim()} will be used for connector probes.` }
|
||||
: { status: 'error', message: 'A provider endpoint is required.' };
|
||||
case 'scope':
|
||||
return this.isScopeValid()
|
||||
? { status: 'success', message: 'Connector scope is explicitly defined.' }
|
||||
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
|
||||
if (this.hasExplicitScopeInputs()) {
|
||||
return this.isScopeValid()
|
||||
? { status: 'success', message: 'Connector scope is explicitly defined.' }
|
||||
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
|
||||
}
|
||||
|
||||
return { status: 'warning', message: 'No explicit discovery scope is configured; the connector can still be created for endpoint-level checks.' };
|
||||
case 'schedule':
|
||||
return this.isScheduleValid()
|
||||
? { status: 'success', message: `Connector checks will run in ${draft.schedule.type} mode.` }
|
||||
@@ -426,4 +483,15 @@ export class IntegrationWizardComponent {
|
||||
: { status: 'error', message: 'Select a supported provider first.' };
|
||||
}
|
||||
}
|
||||
|
||||
private hasExplicitScopeInputs(): boolean {
|
||||
const draft = this.draft();
|
||||
return (
|
||||
draft.organizationId.trim().length > 0 ||
|
||||
draft.repositories.length > 0 ||
|
||||
draft.branches.length > 0 ||
|
||||
draft.namespaces.length > 0 ||
|
||||
draft.tagPatterns.length > 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,69 @@ import {
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="category-section">
|
||||
<div class="category-header">
|
||||
<div>
|
||||
<h2>Secrets & Config Stores</h2>
|
||||
<p class="category-desc">Connect Vault and Consul surfaces that back secret indirection and local config checks.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" (click)="openWizard('secrets')" [disabled]="secretProviders().length === 0">
|
||||
+ Add Secrets
|
||||
</button>
|
||||
</div>
|
||||
@if (secretProviders().length > 0) {
|
||||
<div class="provider-pills">
|
||||
@for (provider of secretProviders(); track provider.provider) {
|
||||
<span class="provider-pill">{{ provider.name }}</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="category-empty">No secrets-manager connector plugins are installed in this environment.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="category-section">
|
||||
<div class="category-header">
|
||||
<div>
|
||||
<h2>Feed Mirrors</h2>
|
||||
<p class="category-desc">Register the StellaOps, NVD, and OSV mirror connectors used in the local advisory lane.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" (click)="openWizard('feed')" [disabled]="feedProviders().length === 0">
|
||||
+ Add Feed Mirror
|
||||
</button>
|
||||
</div>
|
||||
@if (feedProviders().length > 0) {
|
||||
<div class="provider-pills">
|
||||
@for (provider of feedProviders(); track provider.provider) {
|
||||
<span class="provider-pill">{{ provider.name }}</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="category-empty">No feed-mirror connector plugins are installed in this environment.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="category-section">
|
||||
<div class="category-header">
|
||||
<div>
|
||||
<h2>Object Storage</h2>
|
||||
<p class="category-desc">Connect S3-compatible storage for air-gap bundles, exports, and mirrored artifacts.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" (click)="openWizard('storage')" [disabled]="storageProviders().length === 0">
|
||||
+ Add Storage
|
||||
</button>
|
||||
</div>
|
||||
@if (storageProviders().length > 0) {
|
||||
<div class="provider-pills">
|
||||
@for (provider of storageProviders(); track provider.provider) {
|
||||
<span class="provider-pill">{{ provider.name }}</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="category-empty">No object-storage connector plugins are installed in this environment.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="category-section">
|
||||
<div class="category-header">
|
||||
<div>
|
||||
@@ -278,6 +341,9 @@ export class IntegrationsHubComponent implements OnInit {
|
||||
readonly scmProviders = computed(() => resolveSupportedProviders('scm', this.supportedCatalog()));
|
||||
readonly ciProviders = computed(() => resolveSupportedProviders('ci', this.supportedCatalog()));
|
||||
readonly hostProviders = computed(() => resolveSupportedProviders('host', this.supportedCatalog()));
|
||||
readonly feedProviders = computed(() => resolveSupportedProviders('feed', this.supportedCatalog()));
|
||||
readonly secretProviders = computed(() => resolveSupportedProviders('secrets', this.supportedCatalog()));
|
||||
readonly storageProviders = computed(() => resolveSupportedProviders('storage', this.supportedCatalog()));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.subscribe((params) => {
|
||||
@@ -372,6 +438,12 @@ export class IntegrationsHubComponent implements OnInit {
|
||||
return this.ciProviders();
|
||||
case 'host':
|
||||
return this.hostProviders();
|
||||
case 'feed':
|
||||
return this.feedProviders();
|
||||
case 'secrets':
|
||||
return this.secretProviders();
|
||||
case 'storage':
|
||||
return this.storageProviders();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -383,6 +455,9 @@ export class IntegrationsHubComponent implements OnInit {
|
||||
case 'scm':
|
||||
case 'ci':
|
||||
case 'host':
|
||||
case 'feed':
|
||||
case 'secrets':
|
||||
case 'storage':
|
||||
return type;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -5,7 +5,15 @@ import {
|
||||
SupportedProviderInfo,
|
||||
} from '../../integration-hub/integration.models';
|
||||
|
||||
export type IntegrationOnboardingType = 'registry' | 'scm' | 'ci' | 'host' | 'advisory-vex';
|
||||
export type IntegrationOnboardingType =
|
||||
| 'registry'
|
||||
| 'scm'
|
||||
| 'ci'
|
||||
| 'host'
|
||||
| 'feed'
|
||||
| 'secrets'
|
||||
| 'storage'
|
||||
| 'advisory-vex';
|
||||
export type WizardStep = 'provider' | 'auth' | 'scope' | 'schedule' | 'preflight' | 'review';
|
||||
|
||||
export interface ProviderField {
|
||||
@@ -25,8 +33,10 @@ export interface IntegrationProviderDefinition {
|
||||
defaultEndpoint: string;
|
||||
endpointHint: string;
|
||||
authRefHint: string;
|
||||
authRefRequired?: boolean;
|
||||
organizationLabel?: string;
|
||||
organizationHint?: string;
|
||||
scopeRequired?: boolean;
|
||||
exposeInUi: boolean;
|
||||
configFields: ProviderField[];
|
||||
}
|
||||
@@ -73,8 +83,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://harbor.local',
|
||||
endpointHint: 'Use the Harbor base URL; StellaOps probes /api/v2.0/health.',
|
||||
authRefHint: 'Reference a robot-account or username:password secret, for example authref://vault/harbor#robot-account.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Project / Namespace',
|
||||
organizationHint: 'Optional Harbor project scope used for list and policy views.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -131,14 +143,16 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
{
|
||||
provider: IntegrationProvider.DockerHub,
|
||||
type: 'registry',
|
||||
name: 'Docker Hub',
|
||||
name: 'Docker Registry / Hub',
|
||||
icon: 'DH',
|
||||
description: 'Docker Hub registry with personal access token or username/password authentication.',
|
||||
defaultEndpoint: 'https://registry.hub.docker.com',
|
||||
endpointHint: 'Use the Docker Hub registry URL. Leave as default for Docker Hub Cloud.',
|
||||
authRefHint: 'Reference a vault secret containing a Docker Hub access token or username:password.',
|
||||
description: 'Open OCI registry or Docker Hub-compatible registry with optional credential-backed probes.',
|
||||
defaultEndpoint: 'http://registry.stella-ops.local:5000',
|
||||
endpointHint: 'Use the registry base URL. The local scratch lane uses registry.stella-ops.local:5000.',
|
||||
authRefHint: 'Reference a vault secret containing username:password when the registry requires authentication.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Namespace / Organization',
|
||||
organizationHint: 'Docker Hub namespace or organization to scope repository discovery.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -179,8 +193,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://nexus.local',
|
||||
endpointHint: 'Use the Nexus base URL with the Docker hosted or proxy repository port.',
|
||||
authRefHint: 'Reference a vault secret containing a Nexus user token or username:password.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Repository Name',
|
||||
organizationHint: 'Nexus Docker repository name to scope image discovery.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -222,8 +238,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://github.com',
|
||||
endpointHint: 'Use https://github.com for GitHub Cloud or your GitHub Enterprise Server base URL.',
|
||||
authRefHint: 'Reference a vault secret that resolves to the app JWT or installation token.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Owner / Organization',
|
||||
organizationHint: 'Optional owner used to scope repository discovery and policy views.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [
|
||||
{
|
||||
@@ -277,8 +295,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://gitea.local',
|
||||
endpointHint: 'Use your Gitea or Forgejo instance base URL.',
|
||||
authRefHint: 'Reference a vault secret containing a Gitea API token.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Organization / Owner',
|
||||
organizationHint: 'Gitea organization or owner to scope repository discovery.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -336,8 +356,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://jenkins.local',
|
||||
endpointHint: 'Use the Jenkins base URL (including context path if configured).',
|
||||
authRefHint: 'Reference a vault secret containing a Jenkins API token or username:apiToken.',
|
||||
authRefRequired: false,
|
||||
organizationLabel: 'Folder / View',
|
||||
organizationHint: 'Optional Jenkins folder or view to scope job discovery.',
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -409,6 +431,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://agent.local:9443',
|
||||
endpointHint: 'Use the eBPF agent gRPC or HTTP endpoint on the target host.',
|
||||
authRefHint: 'Reference a vault secret containing the agent mTLS client certificate or bearer token.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -421,6 +445,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://agent.local:9443',
|
||||
endpointHint: 'Use the ETW agent HTTP endpoint on the target Windows host.',
|
||||
authRefHint: 'Reference a vault secret containing the agent bearer token.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
@@ -433,10 +459,96 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
|
||||
defaultEndpoint: 'https://agent.local:9443',
|
||||
endpointHint: 'Use the dyld interposer agent HTTP endpoint on the target macOS host.',
|
||||
authRefHint: 'Reference a vault secret containing the agent bearer token.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
// ── InMemory (testing only, not exposed) ────────────────────────
|
||||
{
|
||||
provider: IntegrationProvider.StellaOpsMirror,
|
||||
type: 'feed',
|
||||
name: 'StellaOps Mirror',
|
||||
icon: 'SM',
|
||||
description: 'Local or air-gap StellaOps advisory mirror served by Concelier.',
|
||||
defaultEndpoint: 'http://concelier.stella-ops.local',
|
||||
endpointHint: 'Use the Concelier base URL that serves the StellaOps mirror health surface.',
|
||||
authRefHint: 'AuthRef is optional for local mirror health probes.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.NvdMirror,
|
||||
type: 'feed',
|
||||
name: 'NVD Mirror',
|
||||
icon: 'NVD',
|
||||
description: 'Local or mirrored NVD feed surface served by Concelier.',
|
||||
defaultEndpoint: 'http://concelier.stella-ops.local',
|
||||
endpointHint: 'Use the Concelier base URL that serves the NVD mirror health surface.',
|
||||
authRefHint: 'AuthRef is optional for local mirror health probes.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.OsvMirror,
|
||||
type: 'feed',
|
||||
name: 'OSV Mirror',
|
||||
icon: 'OSV',
|
||||
description: 'Local or mirrored OSV feed surface served by Concelier.',
|
||||
defaultEndpoint: 'http://concelier.stella-ops.local',
|
||||
endpointHint: 'Use the Concelier base URL that serves the OSV mirror health surface.',
|
||||
authRefHint: 'AuthRef is optional for local mirror health probes.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.Vault,
|
||||
type: 'secrets',
|
||||
name: 'HashiCorp Vault',
|
||||
icon: 'V',
|
||||
description: 'Vault KV and secret-reference provider for local and production credential indirection.',
|
||||
defaultEndpoint: 'http://vault.stella-ops.local:8200',
|
||||
endpointHint: 'Use the Vault base URL; StellaOps validates the Vault health surface.',
|
||||
authRefHint: 'The Vault connector itself does not require an AuthRef for the local dev server.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.Consul,
|
||||
type: 'secrets',
|
||||
name: 'HashiCorp Consul',
|
||||
icon: 'C',
|
||||
description: 'Consul KV and settings integration for the optional local service-discovery lane.',
|
||||
defaultEndpoint: 'http://consul.stella-ops.local:8500',
|
||||
endpointHint: 'Use the Consul HTTP API base URL.',
|
||||
authRefHint: 'The local single-node Consul profile does not require an AuthRef.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.S3Compatible,
|
||||
type: 'storage',
|
||||
name: 'S3-Compatible Storage',
|
||||
icon: 'S3',
|
||||
description: 'S3-compatible object storage used for exports, bundles, and mirrored artifacts.',
|
||||
defaultEndpoint: 'http://minio.stella-ops.local:9000',
|
||||
endpointHint: 'Use the storage base URL. The local MinIO lane probes /minio/health/live automatically.',
|
||||
authRefHint: 'AuthRef is optional for the default local MinIO health probe.',
|
||||
authRefRequired: false,
|
||||
scopeRequired: false,
|
||||
exposeInUi: true,
|
||||
configFields: [],
|
||||
},
|
||||
{
|
||||
provider: IntegrationProvider.InMemory,
|
||||
type: 'registry',
|
||||
@@ -455,6 +567,9 @@ export const REGISTRY_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) =>
|
||||
export const SCM_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'scm');
|
||||
export const CI_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'ci');
|
||||
export const HOST_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'host');
|
||||
export const FEED_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'feed');
|
||||
export const SECRETS_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'secrets');
|
||||
export const STORAGE_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'storage');
|
||||
|
||||
export const scheduleOptions = [
|
||||
{ value: 'manual', label: 'Manual', description: 'Create the connector now and trigger checks on demand.' },
|
||||
@@ -499,6 +614,12 @@ export function toBackendIntegrationType(type: IntegrationOnboardingType | null)
|
||||
return IntegrationType.CiCd;
|
||||
case 'host':
|
||||
return IntegrationType.RuntimeHost;
|
||||
case 'feed':
|
||||
return IntegrationType.FeedMirror;
|
||||
case 'secrets':
|
||||
return IntegrationType.SecretsManager;
|
||||
case 'storage':
|
||||
return IntegrationType.ObjectStorage;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user