diff --git a/.changeset/nice-pigs-roll.md b/.changeset/nice-pigs-roll.md new file mode 100644 index 00000000..265a1692 --- /dev/null +++ b/.changeset/nice-pigs-roll.md @@ -0,0 +1,5 @@ +--- +"observability": patch +--- + +use direct calls to observability diff --git a/.nvmrc b/.nvmrc index 2edeafb0..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +22 diff --git a/package.json b/package.json index c7b7bcbd..2d3b8c48 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "Apache-2.0", "private": true, "engines": { - "node": ">=20" + "node": ">=22" }, "workspaces": [ "pkg/*" diff --git a/pkg/observability/components/ComponentHealth.vue b/pkg/observability/components/ComponentHealth.vue index 2c7c00bd..d0548f94 100644 --- a/pkg/observability/components/ComponentHealth.vue +++ b/pkg/observability/components/ComponentHealth.vue @@ -46,7 +46,7 @@ export default { return; } - const component = await loadComponent(this.$store, settings, this.urn); + const component = await loadComponent(settings, this.urn); this.health = component.state.healthState; }, }; diff --git a/pkg/observability/components/Dashboard/ConfigurationView.vue b/pkg/observability/components/Dashboard/ConfigurationView.vue index 69c623fc..acbf02c8 100644 --- a/pkg/observability/components/Dashboard/ConfigurationView.vue +++ b/pkg/observability/components/Dashboard/ConfigurationView.vue @@ -78,7 +78,7 @@ export default { }, async save(btnCb) { - const conn = await checkConnection(this.$store, { + const conn = await checkConnection({ apiURL: this.suseObservabilityURL, serviceToken: this.suseObservabilityServiceToken, }); diff --git a/pkg/observability/components/MonitorTab.vue b/pkg/observability/components/MonitorTab.vue index f0b72845..33ea2b67 100644 --- a/pkg/observability/components/MonitorTab.vue +++ b/pkg/observability/components/MonitorTab.vue @@ -97,10 +97,9 @@ export default { this.urn = this.componentIdentifier; - const component = await loadComponent(this.$store, settings, this.urn); + const component = await loadComponent(settings, this.urn); if (!component) { this.observationStatus = await loadObservationStatus( - this.$store, this.clusterId, settings, ); diff --git a/pkg/observability/components/ObservabilityClusterCard.vue b/pkg/observability/components/ObservabilityClusterCard.vue index 75fcf048..a78522db 100644 --- a/pkg/observability/components/ObservabilityClusterCard.vue +++ b/pkg/observability/components/ObservabilityClusterCard.vue @@ -70,7 +70,6 @@ export default { this.installUrl = `${settings.url}/#/stackpacks/kubernetes-v2`; this.observationStatus = await loadObservationStatus( - this.$store, this.resource.spec.displayName, settings, ); @@ -80,7 +79,6 @@ export default { try { this.snapshot = await getSnapshot( - this.$store, `not healthstate in ("CLEAR", "UNKNOWN") AND label = "cluster-name:${this.resource.spec.displayName}"`, settings, ); diff --git a/pkg/observability/components/__tests__/ObservabilityClusterCard.spec.ts b/pkg/observability/components/__tests__/ObservabilityClusterCard.spec.ts index ba0bcf63..0fd4e390 100644 --- a/pkg/observability/components/__tests__/ObservabilityClusterCard.spec.ts +++ b/pkg/observability/components/__tests__/ObservabilityClusterCard.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "vitest"; +import { beforeAll, afterAll, afterEach, test, expect } from "vitest"; import { mount } from "@vue/test-utils"; import ObservabilityClusterCard from "../ObservabilityClusterCard.vue"; @@ -23,6 +23,71 @@ const mountComponent = (mockStore: any) => { }); }; +const setupServer = () => { + const restHandlers = [ + { + url: "https://ye-observability.invalid.com/api/components", + resp: () => new Response("Unauthorized", { status: 401 }), + }, + { + url: "https://ye-observability.invalid.com/api/snapshot", + resp: () => new Response("Unauthorized", { status: 401 }), + }, + { + url: "https://ye-observability.example.com/api/components", + resp: () => new Response(JSON.stringify({}), { status: 200 }), + }, + { + url: "https://ye-observability.example.com/api/snapshot", + resp: () => + new Response( + JSON.stringify({ + viewSnapshotResponse: { + components: [ + { + state: { + healthState: "CRITICAL", + }, + }, + ], + }, + }), + { status: 200 }, + ), + }, + { + url: "https://no-observability.example.com/api/components", + resp: () => new Response("Not found", { status: 404 }), + }, + { + url: "https://no-observability.example.com/api/snapshot", + resp: () => + new Response( + JSON.stringify({ + viewSnapshotResponse: { components: [] }, + }), + { status: 200 }, + ), + }, + ]; + global.fetch = (url: RequestInfo | URL, options?: RequestInit) => { + const handler = restHandlers.find((handler) => + url.toString().startsWith(handler.url), + ); + return Promise.resolve(handler!.resp()); + }; +}; + +const fetch = global.fetch; + +// Start server before all tests +beforeAll(() => setupServer()); + +// Close server after all tests +afterAll(() => { + global.fetch = fetch; +}); + test("initial state", () => { const mockStore = {}; const wrapper = mountComponent(mockStore); @@ -57,18 +122,6 @@ test("happy flow - installed & connected", async () => { }, }, ]); - case "management/request": - return Promise.resolve({ - viewSnapshotResponse: { - components: [ - { - state: { - healthState: "CRITICAL", - }, - }, - ], - }, - }); } }, }; @@ -144,13 +197,11 @@ test("configured, but cannot connect", async () => { }, apiVersion: "observability.rancher.io/v1", spec: { - url: "https://ye-observability.example.com", + url: "https://ye-observability.invalid.com", serviceToken: "ye-token", }, }, ]); - case "management/request": - return Promise.reject(Error("Cannot connect to SUSE Observability")); } }, }; @@ -183,14 +234,11 @@ test("no component for cluster, agent is not deployed", async () => { }, apiVersion: "observability.rancher.io/v1", spec: { - url: "https://ye-observability.example.com", + url: "https://no-observability.example.com", serviceToken: "ye-token", }, }, ]); - case "management/request": - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject("no such cluster component"); case "cluster/request": return Promise.resolve({ data: [] }); } @@ -225,14 +273,11 @@ test("no component for cluster, though agent is deployed", async () => { }, apiVersion: "observability.rancher.io/v1", spec: { - url: "https://ye-observability.example.com", + url: "https://no-observability.example.com", serviceToken: "ye-token", }, }, ]); - case "management/request": - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject("no such cluster component"); case "cluster/request": return Promise.resolve({ data: [ diff --git a/pkg/observability/formatters/ComponentLinkedHealthState.vue b/pkg/observability/formatters/ComponentLinkedHealthState.vue index 9bce6066..0bf1f1da 100644 --- a/pkg/observability/formatters/ComponentLinkedHealthState.vue +++ b/pkg/observability/formatters/ComponentLinkedHealthState.vue @@ -65,11 +65,7 @@ export default { try { const settings = await loadSuseObservabilitySettings(this.$store); - const component = await loadComponent( - this.$store, - settings, - componentIdentifier, - ); + const component = await loadComponent(settings, componentIdentifier); this.data = { health: component.state.healthState, diff --git a/pkg/observability/modules/rancher.ts b/pkg/observability/modules/rancher.ts index 789156a1..aedde5c1 100644 --- a/pkg/observability/modules/rancher.ts +++ b/pkg/observability/modules/rancher.ts @@ -149,7 +149,6 @@ export async function loadAgentStatus( "stackstate-k8s-agent"), ); - console.log(deployments); return deployments.length > 0 ? AgentStatus.Installed : AgentStatus.NotInstalled; diff --git a/pkg/observability/modules/suseObservability.ts b/pkg/observability/modules/suseObservability.ts index 7438f188..ec8f9951 100644 --- a/pkg/observability/modules/suseObservability.ts +++ b/pkg/observability/modules/suseObservability.ts @@ -1,5 +1,15 @@ import { ConnectionInfo } from "types/component"; import { ObservabilitySettings } from "./settings"; + +class FetchError extends Error { + status: number; + + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + /** * Check whether the connection credentials are valid. * @param store The Vue Store @@ -13,32 +23,29 @@ export enum ConnectionStatus { } export async function checkConnection( - store: any, credentials: ConnectionInfo, ): Promise { const creds = token(credentials.serviceToken); try { - const resp = await store.dispatch("management/request", { - url: `${credentials.apiURL}/api/server/info`, - method: "GET", + const resp = await fetch(`${credentials.apiURL}/api/server/info`, { + credentials: "omit", + mode: "cors", headers: { "Content-Type": "application/json", Authorization: creds, }, - redirectUnauthorized: false, }); - - if (resp._status !== 200) { + if (resp.ok) { + return ConnectionStatus.Connected; + } else { return ConnectionStatus.InvalidToken; } - - return ConnectionStatus.Connected; } catch (e) { - if (e instanceof Error) { - return ConnectionStatus.CrossOriginError; - } else { + if (e instanceof FetchError) { return ConnectionStatus.InvalidToken; + } else { + return ConnectionStatus.CrossOriginError; } } } @@ -50,25 +57,25 @@ export enum ObservationStatus { } export async function loadObservationStatus( - store: any, clusterName: string, settings: ObservabilitySettings, ): Promise { try { const clusterUrn = `urn:cluster:/kubernetes:${clusterName}`; - await loadComponent(store, settings, clusterUrn); + await loadComponent(settings, clusterUrn); return ObservationStatus.Observed; } catch (e) { - if (e instanceof Error) { - return ObservationStatus.ConnectionError; - } else { - return ObservationStatus.NotDeployed; + if (e instanceof FetchError) { + const err = e as FetchError; + if (err.status === 404) { + return ObservationStatus.NotDeployed; + } } + return ObservationStatus.ConnectionError; } } export async function getSnapshot( - store: any, stql: string, settings: ObservabilitySettings, ): Promise { @@ -81,15 +88,15 @@ export async function getSnapshot( const httpToken = token(serviceToken); - return await store.dispatch("management/request", { - url: `${suseObservabilityURL}/api/snapshot`, + const resp = await fetch(`${suseObservabilityURL}/api/snapshot`, { method: "POST", + credentials: "omit", + mode: "cors", headers: { "Content-Type": "application/json", Authorization: httpToken, }, - withCredentials: true, - data: { + body: JSON.stringify({ query: stql, queryVersion: "1.0", metadata: { @@ -104,22 +111,34 @@ export async function getSnapshot( neighboringComponents: false, showFullComponent: false, }, - }, + }), }); + if (resp.ok) { + return await resp.json(); + } else { + throw new FetchError(await resp.text(), resp.status); + } } -export function loadComponent( - store: any, +export async function loadComponent( spec: ObservabilitySettings, identifier: string, ) { const creds = token(spec.serviceToken); - - return store.dispatch("management/request", { - url: `${spec.url}/api/components?identifier=${encodeURIComponent(identifier)}`, - method: "GET", - headers: { "Content-Type": "application/json", Authorization: creds }, - }); + const resp = await fetch( + `${spec.url}/api/components?identifier=${encodeURIComponent(identifier)}`, + { + method: "GET", + mode: "cors", + credentials: "omit", + headers: { "Content-Type": "application/json", Authorization: creds }, + }, + ); + if (resp.ok) { + return await resp.json(); + } else { + throw new FetchError(await resp.text(), resp.status); + } } function token(serviceToken: string): string { diff --git a/yarn.lock b/yarn.lock index 0a76ad22..46f5ddcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15626,7 +15626,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -15643,6 +15643,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -15694,7 +15703,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -15708,6 +15717,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -17190,7 +17206,7 @@ worker-loader@3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17216,6 +17232,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"