Skip to content

Commit 9191492

Browse files
committed
chore: add dev yarn command
1 parent 35baf8d commit 9191492

File tree

5 files changed

+358
-169
lines changed

5 files changed

+358
-169
lines changed

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*.test.tsx
55
.eslintrc.js
66
vite.config.mts
7-
secrets.js
7+
scripts/
88

99
# The following files have eslint errors/warnings
1010
src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"lint": "tsc --noEmit && eslint 'src/**/*.{js,jsx,ts,tsx}' --max-warnings 0",
5050
"lint-fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
5151
"start": "vite --open",
52+
"dev": "node scripts/dev-with-secrets.js",
5253
"build": "NODE_OPTIONS=--max_old_space_size=8192 vite build",
5354
"serve": "vite preview",
5455
"build-light": "NODE_OPTIONS=--max_old_space_size=8192 GENERATE_SOURCEMAP=false vite build",

scripts/dev-with-secrets.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env node
2+
3+
const { spawn } = require('child_process')
4+
const path = require('path')
5+
6+
// Get namespace from command line arguments
7+
const namespace = process.argv[2]
8+
9+
if (!namespace) {
10+
console.error('Please provide a namespace as an argument.')
11+
console.error('Usage: yarn dev:secrets <namespace>')
12+
process.exit(1)
13+
}
14+
15+
console.log(`🔄 Updating secrets for namespace: ${namespace}`)
16+
17+
// Run update-secret.js first
18+
const updateSecrets = spawn('node', [path.join(__dirname, 'update-secret.js'), namespace], {
19+
stdio: 'inherit',
20+
})
21+
22+
updateSecrets.on('close', (code) => {
23+
if (code !== 0) {
24+
console.error('❌ Failed to update secrets')
25+
process.exit(code)
26+
}
27+
28+
console.log('✅ Secrets updated successfully')
29+
console.log('🚀 Starting development server...')
30+
31+
// Start the development server
32+
const startDev = spawn('yarn', ['start'], {
33+
stdio: 'inherit',
34+
})
35+
36+
startDev.on('close', (code) => {
37+
process.exit(code)
38+
})
39+
})

scripts/update-secret.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
const fs = require('fs')
2+
3+
// Configuration constants
4+
const ENV_FILE_PATH = '.env.secrets'
5+
const DEVTRON_HOST = 'https://dev-staging.devtron.info'
6+
7+
/**
8+
* Helper for styled console logs with emojis
9+
*/
10+
const logger = {
11+
info: (message) => console.log(`ℹ️ ${message}`),
12+
success: (message) => console.log(`✅ ${message}`),
13+
warning: (message) => console.log(`⚠️ ${message}`),
14+
error: (message) => console.error(`❌ ${message}`),
15+
update: (message) => console.log(`🔄 ${message}`),
16+
}
17+
18+
/**
19+
* Reads environment variables from .env.secrets file
20+
* @returns {Object} Environment variables as key-value pairs
21+
*/
22+
const readEnvFile = () => {
23+
try {
24+
const envContent = fs.readFileSync(ENV_FILE_PATH, 'utf8')
25+
const envVars = {}
26+
27+
envContent.split('\n').forEach((line) => {
28+
const [key, value] = line.split('=')
29+
if (key && value) {
30+
envVars[key.trim()] = value.trim()
31+
}
32+
})
33+
34+
return envVars
35+
} catch (error) {
36+
logger.error(`Cannot read ${ENV_FILE_PATH}: ${error.message}`)
37+
return {}
38+
}
39+
}
40+
41+
/**
42+
* Wrapper for making API calls to Devtron
43+
* @param {string} endpoint - API endpoint
44+
* @param {object} body - Request body
45+
* @param {string} token - Authentication token
46+
* @returns {Promise<object>} Response data
47+
*/
48+
const callDevtronApi = async (endpoint, body, token, method = 'POST') => {
49+
try {
50+
const response = await fetch(`${DEVTRON_HOST}${endpoint}`, {
51+
method,
52+
headers: {
53+
'Content-Type': 'application/json',
54+
Cookie: `argocd.token=${token}`,
55+
},
56+
body: JSON.stringify(body),
57+
})
58+
59+
if (!response.ok) {
60+
throw new Error(`API call failed with status: ${response.status}`)
61+
}
62+
63+
return await response.json()
64+
} catch (error) {
65+
logger.error(`API call failed: ${error.message}`)
66+
throw error
67+
}
68+
}
69+
70+
/**
71+
* Finds a matching namespace in Devtron based on user input
72+
* @param {string} userInputNamespace - Partial namespace name provided by user
73+
* @param {string} token - Authentication token
74+
* @returns {Promise<string>} The fully matched namespace name
75+
* @throws {Error} If no matching namespace is found
76+
*/
77+
async function matchNamespace(userInputNamespace, token) {
78+
const { result } = await callDevtronApi('/orchestrator/cluster/namespaces/1', undefined, token, 'GET')
79+
80+
const validNamespacePattern = /(devtroncd-.*)|(shared-stage-dcd)/
81+
82+
const matchedNamespace = result
83+
.filter((namespace) => validNamespacePattern.test(namespace))
84+
// sort in reverse order to prioritize more specific namespaces
85+
// ex - if user inputs ent-3 it can match both ent-3 and ea-ent-3.
86+
// If I don't sort, it will always match ea-ent-3 when a better match is available
87+
.sort((a, b) => b.localeCompare(a))
88+
.find((namespace) => namespace.includes(userInputNamespace))
89+
90+
if (!matchedNamespace) {
91+
throw new Error(`Namespace "${userInputNamespace}" not found`)
92+
}
93+
94+
logger.info(`Found matching namespace: ${matchedNamespace}`)
95+
return matchedNamespace
96+
}
97+
98+
/**
99+
* Updates or adds an environment variable in the .env file
100+
* @param {string} key - Environment variable name
101+
* @param {string} value - Environment variable value
102+
* @returns {void}
103+
*/
104+
const updateEnvVariable = (key, value) => {
105+
// Read existing .env file or create empty string if it doesn't exist
106+
let envContent = ''
107+
try {
108+
envContent = fs.readFileSync(ENV_FILE_PATH, 'utf8')
109+
} catch (error) {
110+
// File doesn't exist, will create it
111+
}
112+
113+
if (envContent.includes(`${key}=`)) {
114+
// Replace existing value
115+
envContent = envContent.replace(new RegExp(`${key}=.*$`, 'm'), `${key}=${value}`)
116+
logger.success(`${key} updated in ${ENV_FILE_PATH}`)
117+
} else {
118+
// Add new entry
119+
const newLine = envContent.length > 0 && !envContent.endsWith('\n') ? '\n' : ''
120+
envContent += `${newLine}${key}=${value}\n`
121+
logger.success(`${key} added to ${ENV_FILE_PATH}`)
122+
}
123+
124+
fs.writeFileSync(ENV_FILE_PATH, envContent)
125+
}
126+
127+
/**
128+
* Fetches the ingress host and updates the target URL in env file
129+
* @param {string} token - Authentication token
130+
* @param {string} namespace - Kubernetes namespace
131+
*/
132+
const writeIngressHostToEnv = async (token, namespace) => {
133+
logger.update(`Fetching ingress details for namespace: ${namespace}`)
134+
135+
// Define the request body for ingress list
136+
const ingressBody = {
137+
appId: '',
138+
clusterId: 1,
139+
k8sRequest: {
140+
resourceIdentifier: {
141+
groupVersionKind: {
142+
Group: 'networking.k8s.io',
143+
Version: 'v1',
144+
Kind: 'Ingress',
145+
},
146+
namespace,
147+
},
148+
},
149+
}
150+
151+
// Get the list of ingresses
152+
const ingressListResponse = await callDevtronApi('/orchestrator/k8s/resource/list', ingressBody, token)
153+
const ingress = ingressListResponse.result.data[0]
154+
155+
if (!ingress) {
156+
logger.error(`No ingress found in namespace ${namespace}`)
157+
throw new Error('No ingress found')
158+
}
159+
160+
// Define the request body for ingress details
161+
const ingressDetailBody = {
162+
appId: '',
163+
clusterId: 1,
164+
k8sRequest: {
165+
resourceIdentifier: {
166+
groupVersionKind: {
167+
Group: 'networking.k8s.io',
168+
Version: 'v1',
169+
Kind: 'Ingress',
170+
},
171+
namespace,
172+
name: ingress.name,
173+
},
174+
},
175+
}
176+
177+
// Get the ingress details
178+
const ingressDetailResponse = await callDevtronApi('/orchestrator/k8s/resource', ingressDetailBody, token)
179+
const host = ingressDetailResponse.result.manifestResponse.manifest.spec.rules[0].host
180+
181+
// Update the target URL environment variable
182+
updateEnvVariable('VITE_TARGET_URL', `https://${host}`)
183+
logger.info(`Target URL set to https://${host}`)
184+
}
185+
186+
/**
187+
* Fetches the admin password from a secret and updates the env file
188+
* @param {string} token - Authentication token
189+
* @param {string} namespace - Kubernetes namespace
190+
*/
191+
const writeAdminPasswordToEnv = async (token, namespace) => {
192+
logger.update(`Fetching admin password for namespace: ${namespace}`)
193+
194+
const secretsBody = {
195+
appId: '',
196+
clusterId: 1,
197+
k8sRequest: {
198+
resourceIdentifier: {
199+
groupVersionKind: {
200+
Group: '',
201+
Version: 'v1',
202+
Kind: 'Secret',
203+
},
204+
namespace,
205+
name: 'orchestrator-secret',
206+
},
207+
},
208+
}
209+
210+
try {
211+
const secretResponse = await callDevtronApi('/orchestrator/k8s/resource', secretsBody, token)
212+
const ADMIN_PASSWORD = secretResponse.result.manifestResponse.manifest.data.ADMIN_PASSWORD
213+
214+
// Decode the base64 encoded ADMIN_PASSWORD
215+
const decodedPassword = Buffer.from(ADMIN_PASSWORD, 'base64').toString('utf8')
216+
217+
// Update the admin password environment variable
218+
updateEnvVariable('VITE_ADMIN_PASSWORD', decodedPassword)
219+
} catch (error) {
220+
logger.error(`Failed to fetch admin password: ${error.message}`)
221+
throw error
222+
}
223+
}
224+
/**
225+
* Displays help information for the script
226+
*/
227+
const showHelp = () => {
228+
console.log(`
229+
🔑 Devtron Secret Update Tool 🔑
230+
231+
Usage: node update-secret.js <namespace> [options]
232+
233+
Arguments:
234+
<namespace> Partial namespace name to match against available namespaces
235+
Examples: "ent-3", "stage-dcd", etc.
236+
237+
Options:
238+
--help Show this help message
239+
240+
Notes:
241+
- Namespace matching:
242+
• Valid namespaces follow the pattern: devtroncd-<env> or shared-stage-dcd
243+
• You only need to provide a substring of the namespace (e.g., "ent-3" instead of "devtroncd-ent-3")
244+
• For dev-staging environment, use "stage-dcd" to match "shared-stage-dcd"
245+
• When multiple matches exist, more specific matches are prioritized
246+
(e.g., "ent-3" will match "devtroncd-ent-3" over "devtroncd-ea-ent-3")
247+
248+
- Required environment variables:
249+
• DEV_STAGING_TOKEN must be set in your ${ENV_FILE_PATH} file
250+
251+
This script will:
252+
1. Find the matching namespace based on your input
253+
2. Fetch and update the admin password in your ${ENV_FILE_PATH} file
254+
3. Fetch and update the target URL in your ${ENV_FILE_PATH} file
255+
`)
256+
}
257+
258+
/**
259+
* Main execution function
260+
*/
261+
const main = async () => {
262+
// Check for required namespace argument
263+
if (!process.argv[2]) {
264+
logger.error('Please provide a namespace as an argument.')
265+
logger.info('Usage: node update-secret.js <namespace>')
266+
logger.info('For help: node update-secret.js --help')
267+
process.exit(1)
268+
}
269+
270+
// Check if help is requested at the beginning of the script
271+
if (process.argv.includes('--help')) {
272+
showHelp()
273+
process.exit(0)
274+
}
275+
276+
const userInputNamespace = process.argv[2]
277+
278+
// Read token from .env.secrets file
279+
const envVars = readEnvFile()
280+
const token = envVars.DEV_STAGING_TOKEN
281+
282+
if (!token) {
283+
logger.error('DEV_STAGING_TOKEN not found in .env.secrets file')
284+
logger.info('Make sure to add DEV_STAGING_TOKEN=your_token to .env.secrets')
285+
process.exit(1)
286+
}
287+
288+
try {
289+
logger.info('Starting secret update process')
290+
291+
// Execute both operations sequentially
292+
const namespace = await matchNamespace(userInputNamespace, token)
293+
await writeAdminPasswordToEnv(token, namespace)
294+
await writeIngressHostToEnv(token, namespace)
295+
296+
logger.success('All secrets updated successfully! 🎉')
297+
} catch (error) {
298+
logger.error(`Failed to update secrets: ${error.message}`)
299+
process.exit(1)
300+
}
301+
}
302+
303+
// Execute the script
304+
if (require.main === module) {
305+
main().catch((error) => {
306+
logger.error(`Unhandled error: ${error.message}`)
307+
process.exit(1)
308+
})
309+
}
310+
311+
// Export functions for potential reuse
312+
module.exports = {
313+
readEnvFile,
314+
updateEnvVariable,
315+
writeIngressHostToEnv,
316+
writeAdminPasswordToEnv,
317+
}

0 commit comments

Comments
 (0)