-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
export command #54
base: main
Are you sure you want to change the base?
export command #54
Changes from 13 commits
e84ef9b
087cc4a
3d80656
9ab222d
dcfeb1e
8239bff
117d7ec
5eeae2e
7709b39
9d2b9c0
ca44af2
de8b8b9
2478730
cc061d9
19ef6c2
c346833
3b1b3b3
da5a34d
ff22083
8219d7d
bce2cbb
e1231c4
508c3a3
39d159b
b2a591c
ebd8172
8b466ff
a5dc8fe
009c4c6
f403813
dda9be6
307dfea
e8f9eb3
e7ddd4c
187ebb0
7aabb4b
66d60e7
bcc22cb
f0a4c4b
8e6cf90
e3e0030
7873495
bbe5eb7
b9cfb90
7c625a7
8060b54
870214f
3ced990
332bafa
b06de7f
cb46671
a887622
767b1eb
375cfe8
31ba2c0
4e72b85
f43889e
65554df
3a034e1
e5a026b
0b11a8d
fc80788
3ac73bb
4c1c3de
4a65034
e983afc
8b17a2a
697f4d9
b37b076
ac0c5c2
f12c434
86a6c3a
cd01ac7
050f48c
1705ea3
3ffba6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import React from 'react'; | ||
import { useApiKeyApi } from '../../../../hooks/useApiKeyApi.js'; | ||
import { useAuth } from '../../../../components/AuthProvider.js'; | ||
import { ExportOptions } from '../types.js'; | ||
import { ExportStatus } from './ExportStatus.js'; | ||
import { useExport } from './hooks/useExport.js'; | ||
import fs from 'node:fs/promises'; | ||
|
||
export const ExportContent: React.FC<{ options: ExportOptions }> = ({ | ||
options: { key: apiKey, file }, | ||
}) => { | ||
const { validateApiKeyScope } = useApiKeyApi(); | ||
const { authToken } = useAuth(); | ||
const key = apiKey || authToken; | ||
const { state, setState, exportConfig } = useExport(key); | ||
|
||
React.useEffect(() => { | ||
let isSubscribed = true; | ||
|
||
const runExport = async () => { | ||
if (!key) { | ||
setState({ | ||
status: '', | ||
error: 'No API key provided. Please provide a key or login first.', | ||
isComplete: true, | ||
warnings: [], | ||
}); | ||
return; | ||
} | ||
|
||
try { | ||
setState(prev => ({ ...prev, status: 'Validating API key...' })); | ||
const { | ||
valid, | ||
error: scopeError, | ||
scope, | ||
} = await validateApiKeyScope(key, 'environment'); | ||
|
||
if (!valid || scopeError || !scope) { | ||
setState({ | ||
status: '', | ||
error: `Invalid API key: ${scopeError || 'No scope found'}`, | ||
isComplete: true, | ||
warnings: [], | ||
}); | ||
return; | ||
} | ||
|
||
// Normalize the environment_id and project_id to match ExportScope | ||
const normalizedScope = { | ||
...scope, | ||
environment_id: scope.environment_id || undefined, | ||
project_id: scope.project_id || undefined, | ||
}; | ||
|
||
if (!isSubscribed) return; | ||
|
||
setState(prev => ({ | ||
...prev, | ||
status: 'Initializing export...', | ||
})); | ||
|
||
const { hcl, warnings } = await exportConfig(normalizedScope); | ||
|
||
if (!isSubscribed) return; | ||
|
||
if (file) { | ||
setState(prev => ({ ...prev, status: 'Saving to file...' })); | ||
try { | ||
await fs.writeFile(file, hcl); | ||
} catch (error) { | ||
const errorMessage = | ||
error instanceof Error ? error.message : String(error); | ||
setState({ | ||
status: '', | ||
error: `Failed to export configuration: ${errorMessage}`, | ||
isComplete: true, | ||
warnings: [], | ||
}); | ||
return; | ||
} | ||
} else { | ||
console.log(hcl); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be printed as part of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
if (!isSubscribed) return; | ||
|
||
setState({ | ||
status: '', | ||
error: null, | ||
isComplete: true, | ||
warnings, | ||
}); | ||
} catch (err) { | ||
if (!isSubscribed) return; | ||
const errorMsg = err instanceof Error ? err.message : String(err); | ||
setState({ | ||
status: '', | ||
error: `Failed to export configuration: ${errorMsg}`, | ||
isComplete: true, | ||
warnings: [], | ||
}); | ||
} | ||
}; | ||
|
||
runExport(); | ||
|
||
return () => { | ||
isSubscribed = false; | ||
}; | ||
}, [key, file, validateApiKeyScope]); | ||
Check warning on line 111 in source/commands/env/export/components/ExportContent.tsx
|
||
|
||
return <ExportStatus state={state} file={file} />; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React from 'react'; | ||
import { Text } from 'ink'; | ||
import Spinner from 'ink-spinner'; | ||
import { ExportState } from '../types.js'; | ||
|
||
interface ExportStatusProps { | ||
state: ExportState; | ||
file?: string; | ||
} | ||
|
||
export const ExportStatus: React.FC<ExportStatusProps> = ({ state, file }) => { | ||
if (state.error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please replace the |
||
return ( | ||
<> | ||
<Text color="red">Error: {state.error}</Text> | ||
{state.warnings.length > 0 && ( | ||
<> | ||
<Text color="yellow">Warnings:</Text> | ||
{state.warnings.map((warning, i) => ( | ||
<Text key={i} color="yellow"> | ||
- {warning} | ||
</Text> | ||
))} | ||
</> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
if (!state.isComplete) { | ||
return ( | ||
<> | ||
<Text> | ||
<Spinner type="dots" />{' '} | ||
{state.status || 'Exporting environment configuration...'} | ||
</Text> | ||
</> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<Text color="green">Export completed successfully!</Text> | ||
{file && <Text>HCL content has been saved to: {file}</Text>} | ||
{state.warnings.length > 0 && ( | ||
<> | ||
<Text color="yellow">Warnings during export:</Text> | ||
{state.warnings.map((warning, i) => ( | ||
<Text key={i} color="yellow"> | ||
- {warning} | ||
</Text> | ||
))} | ||
</> | ||
)} | ||
</> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Permit } from 'permitio'; | ||
import React from 'react'; | ||
|
||
export const PermitSDK = (token: string) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use the hook naming convention |
||
return React.useMemo( | ||
() => | ||
new Permit({ | ||
token, | ||
pdp: 'http://localhost:7766', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable should be configurable from the function arguments to allow configurable pdp URL for further usages |
||
}), | ||
[token], | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { useState } from 'react'; | ||
import { PermitSDK } from './PermitSDK.js'; | ||
import { ExportState } from '../../types.js'; | ||
import { createWarningCollector, generateProviderBlock } from '../../utils.js'; | ||
import { ResourceGenerator } from '../../generators/ResourceGenerator.js'; | ||
import { RoleGenerator } from '../../generators/RoleGenerator.js'; | ||
import { UserAttributesGenerator } from '../../generators/UserAttributesGenerator.js'; | ||
import { RelationGenerator } from '../../generators/RelationGenerator.js'; | ||
import { ConditionSetGenerator } from '../../generators/ConditionSetGenerator.js'; | ||
|
||
// Define a type for the `scope` parameter | ||
interface ExportScope { | ||
environment_id?: string; | ||
project_id?: string; | ||
organization_id?: string; | ||
} | ||
|
||
export const useExport = (apiKey: string) => { | ||
const [state, setState] = useState<ExportState>({ | ||
status: '', | ||
isComplete: false, | ||
error: null, | ||
warnings: [], | ||
}); | ||
|
||
const permit = PermitSDK(apiKey); | ||
|
||
const exportConfig = async (scope: ExportScope) => { | ||
try { | ||
const warningCollector = createWarningCollector(); | ||
|
||
let hcl = `# Generated by Permit CLI | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move all the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is up to you to decide on the import way of this text and use the templates in it |
||
# Environment: ${scope?.environment_id || 'unknown'} | ||
# Project: ${scope?.project_id || 'unknown'} | ||
# Organization: ${scope?.organization_id || 'unknown'} | ||
${generateProviderBlock(apiKey)}`; | ||
|
||
const generators = [ | ||
new ResourceGenerator(permit, warningCollector), | ||
new RoleGenerator(permit, warningCollector), | ||
new UserAttributesGenerator(permit, warningCollector), | ||
new RelationGenerator(permit, warningCollector), | ||
new ConditionSetGenerator(permit, warningCollector), | ||
]; | ||
|
||
for (const generator of generators) { | ||
setState(prev => ({ | ||
...prev, | ||
status: `Exporting ${generator.name}...`, | ||
})); | ||
|
||
const generatedHCL = await generator.generateHCL(); | ||
if (generatedHCL) { | ||
hcl += generatedHCL; | ||
} | ||
} | ||
|
||
return { hcl, warnings: warningCollector.getWarnings() }; | ||
} catch (error) { | ||
console.error('Export error:', error); | ||
throw error; | ||
} | ||
}; | ||
|
||
return { | ||
state, | ||
setState, | ||
exportConfig, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Permit } from 'permitio'; | ||
import { HCLGenerator, WarningCollector } from '../types.js'; | ||
import { createSafeId } from '../utils.js'; | ||
|
||
export class ConditionSetGenerator implements HCLGenerator { | ||
name = 'condition sets'; | ||
|
||
constructor( | ||
private permit: Permit, | ||
private warningCollector: WarningCollector, | ||
) {} | ||
|
||
async generateHCL(): Promise<string> { | ||
try { | ||
const conditionSets = await this.permit.api.conditionSets.list(); | ||
if ( | ||
!conditionSets || | ||
!Array.isArray(conditionSets) || | ||
conditionSets.length === 0 | ||
) { | ||
return ''; | ||
} | ||
|
||
let hcl = '\n# Condition Sets\n'; | ||
|
||
for (const set of conditionSets) { | ||
try { | ||
const isResourceSet = set.type === 'resourceset'; | ||
const resourceType = isResourceSet ? 'resource_set' : 'user_set'; | ||
|
||
// Handle conditions - ensure they are properly stringified | ||
const conditions = | ||
typeof set.conditions === 'string' | ||
? set.conditions | ||
: JSON.stringify(set.conditions || ''); | ||
|
||
hcl += `resource "permitio_${resourceType}" "${createSafeId(set.key)}" { | ||
key = "${set.key}" | ||
name = "${set.name}"${ | ||
set.description ? `\n description = "${set.description}"` : '' | ||
} | ||
conditions = ${conditions}${ | ||
set.resource ? `\n resource = "${set.resource}"` : '' | ||
} | ||
}\n`; | ||
} catch (setError) { | ||
this.warningCollector.addWarning( | ||
`Failed to export condition set ${set.key}: ${setError}`, | ||
); | ||
continue; | ||
} | ||
} | ||
|
||
return hcl; | ||
} catch (error) { | ||
this.warningCollector.addWarning( | ||
`Failed to export condition sets: ${error}`, | ||
); | ||
return ''; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the convention of importing the function and use them without the
React.