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
3 changes: 2 additions & 1 deletion dataflow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
},
"dependencies": {
"@apollo/client": "3.13.8",
"@kubernetes/client-node": "^0.21.0",
"@labring/sealos-desktop-sdk": "0.1.23",
"@monaco-editor/react": "^4.7.0",
"@types/react-grid-layout": "^1.3.6",
"@types/react-resizable": "^3.0.8",
Expand All @@ -38,7 +40,6 @@
"react-dom": "19.2.0",
"react-grid-layout": "^1.5.2",
"react-resizable": "^3.1.3",
"sealos-desktop-sdk": "^0.1.14",
"sql-formatter": "^15.7.2",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
Expand Down
482 changes: 468 additions & 14 deletions dataflow/pnpm-lock.yaml

Large diffs are not rendered by default.

88 changes: 9 additions & 79 deletions dataflow/src/stores/useSealosStore.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { create } from 'zustand'
import { EVENT_NAME } from 'sealos-desktop-sdk'
import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'
import { EVENT_NAME, type SessionV1 } from '@labring/sealos-desktop-sdk'
import { createSealosApp, sealosApp } from '@labring/sealos-desktop-sdk/app'

export interface SealosSession {
token?: {
access_token: string
token_type: string
refresh_token: string
expiry: string
}
user: {
id: string
name: string
avatar: string
}
kubeconfig: string
}
type SealosSession = Pick<SessionV1, 'user' | 'kubeconfig'>

interface SealosState {
loading: boolean
Expand All @@ -30,72 +17,10 @@ let initializePromise: Promise<void> | null = null
let sdkCleanup: (() => void) | undefined
let languageCleanup: (() => void) | undefined

function getLocalStorageSessionShape(): 'missing' | 'raw-session' | 'zustand-session' | 'no-kubeconfig' | 'malformed' {
try {
const rawSession = localStorage.getItem('session')
if (!rawSession) return 'missing'

const session = JSON.parse(rawSession)
if (hasKubeconfig(session)) return 'raw-session'
if (hasKubeconfig(session?.state?.session ?? null)) return 'zustand-session'
return 'no-kubeconfig'
} catch {
return 'malformed'
}
}

function logSealosAuthDebug(message: string, details: Record<string, unknown> = {}) {
const localStorageSessionShape = getLocalStorageSessionShape()

console.warn('[SealosAuthDebug]', {
message,
origin: window.location.origin,
inIframe: window.top !== window,
hasLocalStorageSession: localStorageSessionShape !== 'missing',
localStorageSessionShape,
...details,
})
}

function hasKubeconfig(session: SealosSession | null): session is SealosSession {
return typeof session?.kubeconfig === 'string' && session.kubeconfig.trim().length > 0
}

function summarizeError(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error

try {
return JSON.stringify(error)
} catch {
return String(error)
}
}

function getDbprovider51Session(): SealosSession | null {
try {
const rawSession = localStorage.getItem('session')
if (!rawSession) return null

const session = JSON.parse(rawSession) as SealosSession | null
return hasKubeconfig(session) ? session : null
} catch {
return null
}
}

async function resolveSealosSession(): Promise<SealosSession | null> {
try {
const session = await sealosApp.getSession()
if (hasKubeconfig(session)) return session
} catch (error) {
return getDbprovider51Session()
}

logSealosAuthDebug('sdk session missing kubeconfig; trying dbprovider 5.1 localStorage')
return getDbprovider51Session()
}

export const useSealosStore = create<SealosState>((set) => ({
loading: true,
initialized: false,
Expand All @@ -118,7 +43,12 @@ export const useSealosStore = create<SealosState>((set) => ({
let session: SealosSession | null = null
let language: string | null = null

session = await resolveSealosSession()
try {
const resolvedSession = await sealosApp.getSession()
session = hasKubeconfig(resolvedSession) ? resolvedSession : null
} catch {
session = null
}

try {
const result = await sealosApp.getLanguage()
Expand Down
81 changes: 28 additions & 53 deletions dataflow/src/test/useSealosStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const getSessionMock = vi.fn()
const getLanguageMock = vi.fn()
const addAppEventListenMock = vi.fn()
const createSealosAppMock = vi.fn(() => undefined)

vi.mock('sealos-desktop-sdk', () => ({
vi.mock('@labring/sealos-desktop-sdk', () => ({
EVENT_NAME: {
CHANGE_I18N: 'change-i18n',
},
}))

vi.mock('sealos-desktop-sdk/app', () => ({
createSealosApp: vi.fn(() => undefined),
vi.mock('@labring/sealos-desktop-sdk/app', () => ({
createSealosApp: createSealosAppMock,
sealosApp: {
getSession: getSessionMock,
getLanguage: getLanguageMock,
Expand All @@ -22,88 +23,46 @@ vi.mock('sealos-desktop-sdk/app', () => ({
describe('useSealosStore', () => {
beforeEach(() => {
vi.resetModules()
const storage = new Map<string, string>()
vi.stubGlobal('localStorage', {
getItem: vi.fn((key: string) => storage.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
storage.set(key, value)
}),
removeItem: vi.fn((key: string) => {
storage.delete(key)
}),
clear: vi.fn(() => {
storage.clear()
}),
})
getSessionMock.mockReset()
getLanguageMock.mockReset()
addAppEventListenMock.mockReset()
createSealosAppMock.mockReset()
createSealosAppMock.mockReturnValue(undefined)
getLanguageMock.mockResolvedValue({ lng: 'en' })
addAppEventListenMock.mockReturnValue(undefined)
})

it('prefers the Sealos SDK session when it includes kubeconfig', async () => {
getSessionMock.mockResolvedValue({
token: 'sdk-token',
user: {
id: 'u-1',
name: 'Ada',
avatar: '',
},
kubeconfig: 'sdk-kubeconfig',
})
localStorage.setItem(
'session',
JSON.stringify({
user: {
id: 'u-2',
name: 'Grace',
avatar: '',
},
kubeconfig: 'local-storage-kubeconfig',
}),
)

const { useSealosStore } = await import('@/stores/useSealosStore')

await useSealosStore.getState().initialize()

expect(useSealosStore.getState().session?.kubeconfig).toBe('sdk-kubeconfig')
expect(createSealosAppMock).toHaveBeenCalledOnce()
})

it('loads the Sealos session from dbprovider 5.1 localStorage when SDK session is unavailable', async () => {
it('treats an unavailable SDK session as outside Sealos Desktop', async () => {
getSessionMock.mockRejectedValue(new Error('not in desktop bridge'))
localStorage.setItem(
'session',
JSON.stringify({
user: {
id: 'u-1',
name: 'Ada',
avatar: '',
},
kubeconfig: 'apiVersion: v1\ncurrent-context: ns-admin\n',
}),
)

const { useSealosStore } = await import('@/stores/useSealosStore')

await useSealosStore.getState().initialize()

expect(useSealosStore.getState().session?.kubeconfig).toBe('apiVersion: v1\ncurrent-context: ns-admin\n')
expect(useSealosStore.getState().isInSealosDesktop).toBe(true)
expect(useSealosStore.getState().session).toBeNull()
expect(useSealosStore.getState().isInSealosDesktop).toBe(false)
})

it('ignores malformed dbprovider 5.1 localStorage sessions without kubeconfig', async () => {
it('ignores SDK sessions without kubeconfig', async () => {
getSessionMock.mockResolvedValue(null)
localStorage.setItem(
'session',
JSON.stringify({
user: {
id: 'u-1',
name: 'Ada',
avatar: '',
},
}),
)

const { useSealosStore } = await import('@/stores/useSealosStore')

Expand All @@ -112,4 +71,20 @@ describe('useSealosStore', () => {
expect(useSealosStore.getState().session).toBeNull()
expect(useSealosStore.getState().isInSealosDesktop).toBe(false)
})

it('updates the language from Desktop events', async () => {
let changeI18n: ((data: { currentLanguage?: string }) => void) | undefined
getSessionMock.mockResolvedValue(null)
addAppEventListenMock.mockImplementation((_, callback) => {
changeI18n = callback
return undefined
})

const { useSealosStore } = await import('@/stores/useSealosStore')

await useSealosStore.getState().initialize()
changeI18n?.({ currentLanguage: 'zh' })

expect(useSealosStore.getState().language).toBe('zh')
})
})
17 changes: 17 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,20 @@ During install, `install.sh` ensures the user override file exists at:
When the file does not exist yet, it is initialized from:

- `deploy/charts/dataflow/dataflow-values.yaml`

## Embedded UI

To allow an external parent page to embed DataFlow, set
`dataflowConfig.embeddedAllowedOrigins` to exact HTTP(S) origins, including the
protocol and optional port. For example:

```yaml
dataflowConfig:
embeddedAllowedOrigins:
- https://province.example.com
```

Wildcards, paths, and protocol-less hostnames are rejected during Helm
rendering. The Ingress removes `X-Frame-Options` and limits CSP
`frame-ancestors` to Sealos origins plus this list for both NGINX Ingress and
Higress.
3 changes: 3 additions & 0 deletions deploy/charts/dataflow/dataflow-values.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
replicaCount: 1

dataflowConfig:
embeddedAllowedOrigins: []

deploymentStrategy:
type: RollingUpdate
rollingUpdate:
Expand Down
23 changes: 20 additions & 3 deletions deploy/charts/dataflow/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
{{- if .Values.ingress.enabled -}}
{{- $host := include "dataflow.ingressHost" . -}}
{{- $dataflowConfig := default (dict) .Values.dataflowConfig -}}
{{- $embeddedAllowedOrigins := default (list) $dataflowConfig.embeddedAllowedOrigins -}}
{{- range $index, $origin := $embeddedAllowedOrigins -}}
{{- if not (regexMatch `^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\[[0-9A-Fa-f:.]+\])(?::[0-9]+)?$` $origin) -}}
{{- fail (printf "dataflowConfig.embeddedAllowedOrigins[%d] must be an exact http(s) origin without wildcards or paths: %s" $index $origin) -}}
{{- end -}}
{{- end -}}
{{- $embeddedOrigins := join " " $embeddedAllowedOrigins -}}
{{- $frameAncestors := printf "frame-ancestors 'self' https://%s https://*.%s" .Values.cloudDomain .Values.cloudDomain -}}
{{- if $embeddedOrigins -}}
{{- $frameAncestors = printf "%s %s" $frameAncestors $embeddedOrigins -}}
{{- end -}}
{{- $ingressAnnotations := default (dict) .Values.ingress.annotations -}}
{{- $securityAnnotations := dict
"nginx.ingress.kubernetes.io/configuration-snippet" (printf "more_clear_headers \"X-Frame-Options:\";\nmore_set_headers \"Content-Security-Policy: %s\";" $frameAncestors)
"higress.io/response-header-control-remove" "X-Frame-Options"
"higress.io/response-header-control-update" (printf "Content-Security-Policy \"%s\"" $frameAncestors)
-}}
{{- $annotations := mergeOverwrite (deepCopy $ingressAnnotations) $securityAnnotations -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "dataflow.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "dataflow.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- toYaml $annotations | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
tls:
Expand Down
3 changes: 3 additions & 0 deletions deploy/charts/dataflow/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ fullnameOverride: ''

cloudDomain: '127.0.0.1.nip.io'

dataflowConfig:
embeddedAllowedOrigins: []

session:
encryptionKey: 'replace-with-32-byte-secret-key!'
ttl: 24h
Expand Down
Loading