Skip to content

Commit 6bb7a60

Browse files
authored
Merge pull request #366 from oasisprotocol/lw/proxy-domains
Display proxy domains
2 parents 12ffa9b + 64133e0 commit 6bb7a60

5 files changed

Lines changed: 224 additions & 1 deletion

File tree

.changelog/366.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Display proxy domains

src/backend/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const uploadArtifact = async ({ id, file }: ArtifactUploadRequest, token: string
168168
})
169169
}
170170

171-
const downloadArtifact = async (id: ArtifactId, token: string): Promise<ArtifactDownloadResponse> => {
171+
export const downloadArtifact = async (id: ArtifactId, token: string): Promise<ArtifactDownloadResponse> => {
172172
const response = await axios.get(`${BACKEND_URL}/artifacts/${id}`, {
173173
headers: {
174174
Authorization: `Bearer ${token}`,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as yaml from 'yaml'
2+
3+
// https://docs.docker.com/reference/compose-file/services/#ports
4+
type PortMapping = string | number | { published?: string | number /* ignore other props */ }
5+
6+
// Try to match Go code https://github.com/oasisprotocol/cli/blob/61749d6/cmd/rofl/build/validate.go#L182-L203
7+
export function parsePublishedPortsFromCompose(composeYaml: string) {
8+
const compose = yaml.parse(composeYaml)
9+
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
return Object.entries<any>(compose.services).flatMap(([serviceName, service]) => {
12+
if (!service.ports) return []
13+
return (service.ports as PortMapping[]).flatMap(portMapping => {
14+
const portPublished = publishedPortFromMapping(portMapping)
15+
if (!portPublished) return []
16+
const proxyMode = service.annotations?.[`net.oasis.proxy.ports.${portPublished}.mode`]
17+
if (proxyMode === 'ignore') return []
18+
const genericDomain = 'p' + portPublished
19+
const customDomain: string =
20+
service.annotations?.[`net.oasis.proxy.ports.${portPublished}.custom_domain`]
21+
22+
return [
23+
{
24+
ServiceName: serviceName,
25+
Port: portPublished,
26+
ProxyMode: proxyMode,
27+
GenericDomain: genericDomain,
28+
CustomDomain: customDomain,
29+
},
30+
]
31+
})
32+
})
33+
}
34+
35+
function publishedPortFromMapping(portMapping: PortMapping) {
36+
if (typeof portMapping === 'number') return undefined
37+
if (typeof portMapping === 'object') return portMapping.published?.toString()
38+
if (typeof portMapping !== 'string' || portMapping === '') return undefined
39+
40+
// https://github.com/oasisprotocol/cli/blob/61749d6/cmd/rofl/build/validate.go#L182
41+
if (portMapping.replace('/tcp', '').includes('/')) return undefined
42+
if (portMapping.replace('/tcp', '').split(':').length <= 1) return undefined
43+
return portMapping.replace('/tcp', '').split(':').slice(-2)[0]
44+
}
45+
46+
// TODO: move into vitest
47+
// Adjusted from https://github.com/compose-spec/compose-go/blob/61f9cea/types/types_test.go#L32-L199
48+
const testCases = [
49+
{ value: '80', expected: undefined },
50+
{ value: '80:8080', expected: '80' },
51+
{ value: '80-90:8080', expected: '80-90' },
52+
{ value: '8080:80/tcp', expected: '8080' },
53+
{ value: '80:8080/udp', expected: undefined },
54+
{ value: '80-81:8080-8081/tcp', expected: '80-81' },
55+
{ value: '80-82:8080-8082/udp', expected: undefined },
56+
{ value: '80-82:8080/udp', expected: undefined },
57+
{ value: '80-80:8080/tcp', expected: '80-80' },
58+
{ value: '9999999', expected: undefined },
59+
{ value: '80/xyz', expected: undefined },
60+
{ value: 'tcp', expected: undefined },
61+
{ value: 'udp', expected: undefined },
62+
{ value: '', expected: undefined },
63+
{ value: '1.1.1.1:80:80', expected: '80' },
64+
{ value: '::1:6001:6002', expected: '6001' },
65+
{ value: '[::1]:6001:6002', expected: '6001' },
66+
{ value: '[2001:aa:bb:cc:dd:ee:ff:1]:6001:6002', expected: '6001' },
67+
]
68+
testCases.forEach(test => {
69+
const result = publishedPortFromMapping(test.value)
70+
if (result !== test.expected) {
71+
throw new Error(`Test "${test.value}" FAILED. Expected: "${test.expected}", Got: "${result}".`)
72+
}
73+
})

src/backend/useRoflAppDomains.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import {
3+
GetRuntimeRoflAppsIdInstancesRak,
4+
GetRuntimeRoflmarketInstances,
5+
GetRuntimeRoflmarketProvidersAddress,
6+
RoflMarketInstance,
7+
} from '../nexus/api'
8+
import { isMachineRemoved } from '../components/MachineStatusIcon/isMachineRemoved'
9+
import { RoflmarketDeployment } from '@oasisprotocol/client-rt/dist/types'
10+
import { useRoflAppBackendAuthContext } from '../contexts/RoflAppBackendAuth/hooks'
11+
import { downloadArtifact } from './api'
12+
import { parsePublishedPortsFromCompose } from './parsePublishedPortsFromCompose'
13+
14+
const MetadataKeySchedulerRAK = 'net.oasis.scheduler.rak'
15+
const MetadataKeyProxyDomain = 'net.oasis.proxy.domain'
16+
const MetadataKeyProxyCustomDomains = 'net.oasis.proxy.custom_domains'
17+
18+
// Try to match Go code https://github.com/oasisprotocol/cli/blob/61749d6/cmd/rofl/build/validate.go#L26-L44
19+
// PortMapping represents a port mapping.
20+
interface PortMapping {
21+
// ServiceName is the name of the service.
22+
ServiceName: string
23+
// Port is the port number.
24+
Port: string
25+
// ProxyMode is the proxy mode for the port.
26+
ProxyMode: string
27+
// GenericDomain is the generic domain name.
28+
GenericDomain: string
29+
// CustomDomain is the custom domain name (if any).
30+
CustomDomain?: string
31+
}
32+
33+
// AppExtraConfig represents extra configuration for the ROFL app.
34+
interface AppExtraConfig {
35+
// Ports are the port mappings exposed by the app.
36+
Ports: PortMapping[]
37+
}
38+
39+
/** Proxy Domains */
40+
export function useRoflAppDomains(network: 'mainnet' | 'testnet', appID: string) {
41+
const paratime = 'sapphire' as const
42+
const { token } = useRoflAppBackendAuthContext()
43+
44+
const query = useQuery({
45+
queryKey: ['useRoflAppDomains', network, appID, paratime],
46+
queryFn: async () => {
47+
const composeYaml = await downloadArtifact(`${appID}-compose-yaml`, token || '').catch(() => undefined)
48+
const publishedPortsFromCompose = composeYaml ? parsePublishedPortsFromCompose(composeYaml) : []
49+
const extraCfg: AppExtraConfig | undefined = publishedPortsFromCompose?.length
50+
? { Ports: publishedPortsFromCompose }
51+
: undefined
52+
53+
const appMachines = (await GetRuntimeRoflmarketInstances(network, paratime, { deployed_app_id: appID }))
54+
.data.instances
55+
56+
const appDomains: { ServiceName: string; Domain: string }[] = []
57+
for (const insDsc of appMachines.filter(machine => !isMachineRemoved(machine))) {
58+
// Try to match Go code https://github.com/oasisprotocol/cli/blob/1cc571e/cmd/rofl/machine/show.go#L102-L109
59+
const machineID = insDsc.id
60+
const providerAddr = insDsc.provider
61+
62+
const providerDsc = (await GetRuntimeRoflmarketProvidersAddress(network, paratime, providerAddr)).data
63+
64+
const schedulerRAK = insDsc.metadata[MetadataKeySchedulerRAK] as string | undefined
65+
if (!schedulerRAK) return []
66+
const schedulerDsc = (
67+
await GetRuntimeRoflAppsIdInstancesRak(network, paratime, providerDsc.scheduler, schedulerRAK)
68+
).data
69+
70+
const proxyDomain1 = schedulerDsc?.metadata?.[MetadataKeyProxyDomain] as string | undefined
71+
if (!proxyDomain1) return []
72+
const numericMachineID = BigInt('0x' + machineID)
73+
const proxyDomain2 = 'm' + numericMachineID + '.' + proxyDomain1
74+
75+
appDomains.push(...showMachinePorts(extraCfg ?? impliedExtraCfg(insDsc), appID, insDsc, proxyDomain2))
76+
}
77+
78+
return appDomains
79+
},
80+
})
81+
82+
return query
83+
}
84+
85+
// Try to match Go code https://github.com/oasisprotocol/cli/blob/1cc571e/cmd/rofl/machine/show.go#L195-L221
86+
function showMachinePorts(
87+
extraCfg: AppExtraConfig,
88+
_appID: string,
89+
_insDsc: RoflMarketInstance,
90+
domain: string,
91+
) {
92+
return extraCfg.Ports.map(p => {
93+
const genericDomain = p.GenericDomain + '.' + domain
94+
// TODO: DomainVerificationToken
95+
return {
96+
ServiceName: p.ServiceName,
97+
Domain: 'https://' + (p.CustomDomain ?? genericDomain),
98+
}
99+
})
100+
}
101+
102+
function impliedExtraCfg(insDsc: RoflMarketInstance): AppExtraConfig {
103+
const customDomains = (insDsc.deployment as unknown as RoflmarketDeployment).metadata?.[
104+
MetadataKeyProxyCustomDomains
105+
] as string | undefined
106+
107+
return {
108+
Ports: [
109+
// https://github.com/oasisprotocol/oasis-sdk/blob/777bcc4/rofl-scheduler/src/proxy/mod.rs#L231
110+
...(customDomains?.split(' ') || []).map(
111+
(CustomDomain): PortMapping => ({
112+
ServiceName: '',
113+
Port: '<unknown port>',
114+
GenericDomain: 'p<unknown port>',
115+
CustomDomain: CustomDomain,
116+
ProxyMode: 'terminate-tls',
117+
}),
118+
),
119+
{
120+
ServiceName: '',
121+
Port: '<exposed ports>',
122+
GenericDomain: 'p<exposed ports>',
123+
ProxyMode: 'terminate-tls',
124+
},
125+
],
126+
}
127+
}

src/pages/Dashboard/AppDetails/AppMetadata.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useDownloadArtifact } from '../../../backend/api'
2424
import { cn } from '@oasisprotocol/ui-library/src/lib/utils'
2525
import { useAccount } from 'wagmi'
2626
import { getEvmBech32Address } from '../../../utils/helpers'
27+
import { useRoflAppDomains } from '../../../backend/useRoflAppDomains'
2728

2829
type AppMetadataProps = {
2930
id: string
@@ -61,6 +62,8 @@ export const AppMetadata: FC<AppMetadataProps> = ({
6162
const machines = machinesData?.data.instances.filter(machine => !isMachineRemoved(machine))
6263
const lastMachineToDuplicate = machinesData?.data.instances[0]
6364

65+
const appDomains = useRoflAppDomains(network, id)
66+
6467
return (
6568
<div className="space-y-4">
6669
{isMachineLoading && <Skeleton className="w-full h-60px]" />}
@@ -166,6 +169,25 @@ export const AppMetadata: FC<AppMetadataProps> = ({
166169
</a>
167170
) : undefined}
168171
</DetailsSectionRow>
172+
<DetailsSectionRow label="Proxy Domains">
173+
<div>
174+
{appDomains.isLoading && <Skeleton className="h-[20px] w-[80px]" />}
175+
{appDomains.isError && 'Error'}
176+
{appDomains.data?.map((port, i) => (
177+
<div key={i}>
178+
{port.ServiceName && `${port.ServiceName}: `}
179+
{isUrlSafe(port.Domain) ? (
180+
<a href={port.Domain} target="_blank" rel="noopener noreferrer" className="text-primary">
181+
{port.Domain}
182+
</a>
183+
) : (
184+
// `https://p<exposed ports>.m899.opf-testnet-rofl-25.rofl.app` is also considered invalid
185+
<span>{port.Domain}</span>
186+
)}
187+
</div>
188+
))}
189+
</div>
190+
</DetailsSectionRow>
169191
<div className="text-xl font-bold">Policy</div>
170192
<DetailsSectionRow label="Who can run this app">
171193
<Endorsements endorsements={policy.endorsements} />

0 commit comments

Comments
 (0)