@salefronts/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/bin/sf +41 -0
- package/commands/help.js +25 -0
- package/commands/jwt.js +39 -0
- package/commands/keypair.js +63 -0
- package/commands/passphrase.js +18 -0
- package/commands/seal.sh +163 -0
- package/commands/secret.js +10 -0
- package/commands/ssh.js +30 -0
- package/commands/tag.js +118 -0
- package/commands/unseal.sh +78 -0
- package/commands/uuidv4.js +11 -0
- package/commands/uuidv7.js +11 -0
- package/package.json +16 -0
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v22.12.0
|
package/bin/sf
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
|
|
10
|
+
if (!command) {
|
|
11
|
+
console.log('Usage: sf <command>');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const jsCommandPath = path.join(__dirname, '../commands', `${command}.js`);
|
|
16
|
+
const shCommandPath = path.join(__dirname, '../commands', `${command}.sh`);
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(jsCommandPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const commandModule = require(jsCommandPath);
|
|
21
|
+
if (typeof commandModule === 'function') {
|
|
22
|
+
commandModule(args.slice(1));
|
|
23
|
+
} else {
|
|
24
|
+
console.error('Error: Command module is not a function.');
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Error:', err.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
} else if (fs.existsSync(shCommandPath)) {
|
|
31
|
+
const bash = spawn('bash', [shCommandPath, ...args.slice(1)], {
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
bash.on('exit', (code) => {
|
|
36
|
+
process.exit(code);
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
console.error(`Unknown command: ${command}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
package/commands/help.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Helper function to display the general help
|
|
7
|
+
function showHelp() {
|
|
8
|
+
console.log('Usage: sf <command>\n');
|
|
9
|
+
console.log('Available commands:');
|
|
10
|
+
console.log(' jwt Generate JWT tokens');
|
|
11
|
+
console.log(' keypair Generate public/private key pairs');
|
|
12
|
+
console.log(' passphrase Generate secure passphrases');
|
|
13
|
+
console.log(' secret Generate a random secret');
|
|
14
|
+
console.log(' ssh Generate SSH key');;
|
|
15
|
+
console.log(' uuidv4 Generate a UUID v4');
|
|
16
|
+
console.log(' uuidv7 Generate a UUID v7');
|
|
17
|
+
console.log(' seal Seal Kubernetes secrets');
|
|
18
|
+
console.log(' unseal Unseal Kubernetes secrets');
|
|
19
|
+
console.log(' tag Tagging version for the latest commit on main')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = async function () {
|
|
23
|
+
showHelp();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
package/commands/jwt.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
|
|
4
|
+
function ask(question) {
|
|
5
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
7
|
+
rl.close();
|
|
8
|
+
resolve(answer.trim());
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = async function () {
|
|
13
|
+
try {
|
|
14
|
+
const sub = await ask('Subject (sub): ');
|
|
15
|
+
if (!sub) {
|
|
16
|
+
console.error('Aborted: subject is required.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const secret = await ask('Secret: ');
|
|
21
|
+
if (!secret) {
|
|
22
|
+
console.error('Aborted: secret is required.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const expInput = await ask('Expiration in seconds (default 3600): ');
|
|
27
|
+
const exp = expInput ? parseInt(expInput, 10) : 3600;
|
|
28
|
+
|
|
29
|
+
const payload = {
|
|
30
|
+
sub,
|
|
31
|
+
exp: Math.floor(Date.now() / 1000) + exp,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const token = jwt.sign(payload, secret);
|
|
35
|
+
console.log('\nJWT:\n' + token);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Error:', err.message);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const niceware = require('niceware');
|
|
5
|
+
|
|
6
|
+
function ask(question) {
|
|
7
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
9
|
+
rl.close();
|
|
10
|
+
resolve(answer.trim());
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = async function () {
|
|
15
|
+
try {
|
|
16
|
+
// 1. Ask if passphrase is needed
|
|
17
|
+
const passphraseAnswer = (await ask('Use passphrase? (y/N): ')).toLowerCase();
|
|
18
|
+
const usePassphrase = passphraseAnswer === 'y' || passphraseAnswer === 'yes';
|
|
19
|
+
|
|
20
|
+
let passphrase = null;
|
|
21
|
+
const tempPrivate = 'temp_private.pem';
|
|
22
|
+
const tempPublic = 'temp_public.pem';
|
|
23
|
+
|
|
24
|
+
// 2. Generate private key
|
|
25
|
+
if (usePassphrase) {
|
|
26
|
+
const words = niceware.generatePassphrase(6);
|
|
27
|
+
passphrase = words.join('-');
|
|
28
|
+
console.log(`Generated passphrase: ${passphrase}\n`);
|
|
29
|
+
execSync(`openssl genpkey -algorithm RSA -out ${tempPrivate} -aes256 -pass pass:${passphrase}`, { stdio: 'ignore' });
|
|
30
|
+
} else {
|
|
31
|
+
execSync(`openssl genpkey -algorithm RSA -out ${tempPrivate}`, { stdio: 'ignore' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. Generate public key
|
|
35
|
+
if (usePassphrase) {
|
|
36
|
+
execSync(`openssl rsa -pubout -in ${tempPrivate} -out ${tempPublic} -passin pass:${passphrase}`, { stdio: 'ignore' });
|
|
37
|
+
} else {
|
|
38
|
+
execSync(`openssl rsa -pubout -in ${tempPrivate} -out ${tempPublic}`, { stdio: 'ignore' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. Ask for file name
|
|
42
|
+
const name = await ask('File name (without extension): ');
|
|
43
|
+
if (!name) {
|
|
44
|
+
fs.unlinkSync(tempPrivate);
|
|
45
|
+
fs.unlinkSync(tempPublic);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 5. Rename files
|
|
50
|
+
const privateOut = `${name}_private_key.pem`;
|
|
51
|
+
const publicOut = `${name}_public_key.pem`;
|
|
52
|
+
fs.renameSync(tempPrivate, privateOut);
|
|
53
|
+
fs.renameSync(tempPublic, publicOut);
|
|
54
|
+
|
|
55
|
+
// 6. Write passphrase if needed
|
|
56
|
+
if (usePassphrase && passphrase) {
|
|
57
|
+
const passphraseFile = `${name}_passphrase.txt`;
|
|
58
|
+
fs.writeFileSync(passphraseFile, passphrase + '\n', { mode: 0o600 }); // restrict permissions
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Error:', err.message);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const niceware = require('niceware');
|
|
2
|
+
|
|
3
|
+
module.exports = function (args) {
|
|
4
|
+
// Default values
|
|
5
|
+
let wordCount = 5;
|
|
6
|
+
let separator = '-';
|
|
7
|
+
|
|
8
|
+
args.forEach(arg => {
|
|
9
|
+
if (arg.startsWith('-W=')) {
|
|
10
|
+
wordCount = parseInt(arg.split('=')[1], 10);
|
|
11
|
+
} else if (arg.startsWith('-S=')) {
|
|
12
|
+
separator = arg.split('=')[1];
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const passphrase = niceware.generatePassphrase(wordCount * 2);
|
|
17
|
+
console.log(passphrase.join(separator));
|
|
18
|
+
};
|
package/commands/seal.sh
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail # Safe mode
|
|
4
|
+
|
|
5
|
+
# Ensure required dependencies are installed
|
|
6
|
+
for cmd in kubectl yq jq kubeseal; do
|
|
7
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
8
|
+
echo "❌ Error: $cmd is required but not installed."
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
done
|
|
12
|
+
|
|
13
|
+
# Check if a file path is provided, otherwise prompt for it
|
|
14
|
+
if [ -z "${1:-}" ]; then
|
|
15
|
+
echo -n "📄 Enter path to the secret file (e.g., secret.yaml): "
|
|
16
|
+
read SECRET_FILE
|
|
17
|
+
else
|
|
18
|
+
SECRET_FILE=$1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Ensure the file exists
|
|
22
|
+
if [ ! -f "$SECRET_FILE" ]; then
|
|
23
|
+
echo "❌ Error: File not found: $SECRET_FILE"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Ensure the file is a Kubernetes Secret
|
|
28
|
+
KIND=$(yq eval '.kind' "$SECRET_FILE")
|
|
29
|
+
if [ "$KIND" != "Secret" ]; then
|
|
30
|
+
echo "❌ Error: File is not a Kubernetes Secret (found kind: $KIND)"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Extract namespace and secret name
|
|
35
|
+
NAMESPACE=$(yq eval '.metadata.namespace // "default"' "$SECRET_FILE")
|
|
36
|
+
SECRET_NAME=$(yq eval '.metadata.name' "$SECRET_FILE")
|
|
37
|
+
|
|
38
|
+
# Validate extracted values
|
|
39
|
+
if [ -z "$SECRET_NAME" ]; then
|
|
40
|
+
echo "❌ Error: Could not extract secret name from $SECRET_FILE"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Get the current Kubernetes cluster name
|
|
45
|
+
CLUSTER_NAME=$(kubectl config current-context)
|
|
46
|
+
|
|
47
|
+
if ! kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
|
|
48
|
+
echo "❗ Secret does not exist in cluster."
|
|
49
|
+
changes_detected=true
|
|
50
|
+
else
|
|
51
|
+
# Extract and decode Kubernetes secret
|
|
52
|
+
K8S_SECRET_JSON=$(kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" -o json)
|
|
53
|
+
K8S_SECRET_DATA=$(echo "$K8S_SECRET_JSON" | jq -r '.data | to_entries | map("\(.key) \(.value | @base64d)") | .[]')
|
|
54
|
+
|
|
55
|
+
# Extract `stringData` from input file while preserving order
|
|
56
|
+
FILE_SECRET_JSON=$(yq eval -o=json '.stringData // {}' "$SECRET_FILE")
|
|
57
|
+
FILE_SECRET_DATA=$(echo "$FILE_SECRET_JSON" | jq -r 'to_entries | map("\(.key) \(.value)") | .[]')
|
|
58
|
+
|
|
59
|
+
# Convert to associative arrays for easier comparison and maintain order
|
|
60
|
+
declare -A k8s_secret_map
|
|
61
|
+
while IFS= read -r line; do
|
|
62
|
+
key=$(echo "$line" | awk '{print $1}')
|
|
63
|
+
value=$(echo "$line" | cut -d' ' -f2-)
|
|
64
|
+
k8s_secret_map["$key"]="$value"
|
|
65
|
+
done <<< "$K8S_SECRET_DATA"
|
|
66
|
+
|
|
67
|
+
declare -A file_secret_map
|
|
68
|
+
declare -a file_secret_keys
|
|
69
|
+
while IFS= read -r line; do
|
|
70
|
+
key=$(echo "$line" | awk '{print $1}')
|
|
71
|
+
value=$(echo "$line" | cut -d' ' -f2-)
|
|
72
|
+
file_secret_map["$key"]="$value"
|
|
73
|
+
file_secret_keys+=("$key")
|
|
74
|
+
done <<< "$FILE_SECRET_DATA"
|
|
75
|
+
|
|
76
|
+
RED="\e[31m"
|
|
77
|
+
GREEN="\e[32m"
|
|
78
|
+
YELLOW="\e[33m"
|
|
79
|
+
MAGENTA="\e[35m"
|
|
80
|
+
BLUE="\e[34m"
|
|
81
|
+
RESET="\e[0m"
|
|
82
|
+
|
|
83
|
+
# Get terminal width
|
|
84
|
+
COLUMNS=$(tput cols)
|
|
85
|
+
|
|
86
|
+
# Function to truncate text with ellipses
|
|
87
|
+
truncate_text() {
|
|
88
|
+
local text="$1"
|
|
89
|
+
local max_length=$((COLUMNS - 20)) # Adjust based on your needs
|
|
90
|
+
if [ ${#text} -gt $max_length ]; then
|
|
91
|
+
echo "${text:0:$max_length}..."
|
|
92
|
+
else
|
|
93
|
+
echo "$text"
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Compare and print secrets
|
|
98
|
+
changes_detected=false
|
|
99
|
+
for key in "${file_secret_keys[@]}"; do
|
|
100
|
+
if [[ -v k8s_secret_map["$key"] ]]; then
|
|
101
|
+
if [[ "${file_secret_map[$key]}" != "${k8s_secret_map[$key]}" ]]; then
|
|
102
|
+
changes_detected=true
|
|
103
|
+
break
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
changes_detected=true
|
|
107
|
+
break
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
for key in "${!k8s_secret_map[@]}"; do
|
|
112
|
+
if [[ ! -v file_secret_map["$key"] ]]; then
|
|
113
|
+
changes_detected=true
|
|
114
|
+
break
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
if $changes_detected; then
|
|
119
|
+
echo -e "Comparing secrets...\n"
|
|
120
|
+
for key in "${file_secret_keys[@]}"; do
|
|
121
|
+
if [[ -v k8s_secret_map["$key"] ]]; then
|
|
122
|
+
if [[ "${file_secret_map[$key]}" == "${k8s_secret_map[$key]}" ]]; then
|
|
123
|
+
:
|
|
124
|
+
# truncated_value=$(truncate_text "${file_secret_map[$key]}")
|
|
125
|
+
# printf "• %s: %s\n" "$key" "$truncated_value"
|
|
126
|
+
else
|
|
127
|
+
printf "${YELLOW}🔄 %s:${RESET}\n" "$key"
|
|
128
|
+
printf " ${MAGENTA}Old:${RESET} %s\n" "${k8s_secret_map[$key]}"
|
|
129
|
+
printf " ${BLUE}New:${RESET} %s\n" "${file_secret_map[$key]}"
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
printf "${GREEN}➕ %s:${RESET} %s\n" "$key" "${file_secret_map[$key]}"
|
|
133
|
+
fi
|
|
134
|
+
done
|
|
135
|
+
|
|
136
|
+
for key in "${!k8s_secret_map[@]}"; do
|
|
137
|
+
if [[ ! -v file_secret_map["$key"] ]]; then
|
|
138
|
+
printf "${RED}➖ %s:${RESET} %s\n" "$key" "${k8s_secret_map[$key]}"
|
|
139
|
+
fi
|
|
140
|
+
done
|
|
141
|
+
else
|
|
142
|
+
echo "😶 No changes detected!"
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Ask for confirmation to create a sealed secret
|
|
147
|
+
echo
|
|
148
|
+
echo -e "📂 Input File ($SECRET_FILE)"
|
|
149
|
+
echo -e "🌎 K8s Secret ($SECRET_NAME in $NAMESPACE)"
|
|
150
|
+
echo -e "🔗 Cluster: $CLUSTER_NAME"
|
|
151
|
+
echo "Press Enter to create a Sealed Secret or Ctrl+C to cancel"
|
|
152
|
+
read
|
|
153
|
+
|
|
154
|
+
# Ensure output is a valid YAML
|
|
155
|
+
OUTPUT_FILE="${SECRET_FILE/secret.yaml/sealed-secret.yaml}"
|
|
156
|
+
kubeseal < "$SECRET_FILE" | yq eval -P > "$OUTPUT_FILE"
|
|
157
|
+
|
|
158
|
+
if [ $? -eq 0 ]; then
|
|
159
|
+
echo "✅ Sealed secret successfully created: $OUTPUT_FILE (YAML format)"
|
|
160
|
+
else
|
|
161
|
+
echo "❌ Error: Failed to create sealed secret."
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
package/commands/ssh.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function ask(question) {
|
|
5
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
7
|
+
rl.close();
|
|
8
|
+
resolve(answer.trim());
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = async function () {
|
|
13
|
+
try {
|
|
14
|
+
const filename = await ask('File name (without extension): ');
|
|
15
|
+
if (!filename) {
|
|
16
|
+
console.error('Cancelled: No file name provided.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const username = await ask('Username/Email (for comment): ');
|
|
21
|
+
if (!username) {
|
|
22
|
+
console.error('Cancelled: No username provided.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
execSync(`ssh-keygen -t rsa -b 4096 -f ${filename} -C "${username}"`, { stdio: 'inherit' });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Error generating SSH key:', err.message);
|
|
29
|
+
}
|
|
30
|
+
};
|
package/commands/tag.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const readline = require("readline");
|
|
5
|
+
|
|
6
|
+
function runCommand(cmd, allowError = false) {
|
|
7
|
+
try {
|
|
8
|
+
return execSync(cmd, { encoding: "utf-8" }).trim();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (!allowError) {
|
|
11
|
+
console.error(`❌ Error running command: ${cmd}`);
|
|
12
|
+
}
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getLatestGlobalTag() {
|
|
18
|
+
const tags = runCommand("git tag", true)
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter(tag => /^v\d{6}\.\d+$/.test(tag)) // match vYYMMDD.N
|
|
21
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
|
|
24
|
+
return tags[tags.length - 1] || "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getNextGlobalTag() {
|
|
28
|
+
const today = new Intl.DateTimeFormat("vi-VN", {
|
|
29
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
30
|
+
year: "2-digit",
|
|
31
|
+
month: "2-digit",
|
|
32
|
+
day: "2-digit",
|
|
33
|
+
})
|
|
34
|
+
.format(new Date())
|
|
35
|
+
.split("/")
|
|
36
|
+
.reverse()
|
|
37
|
+
.join(""); // YYMMDD
|
|
38
|
+
|
|
39
|
+
const lastTag = getLatestGlobalTag();
|
|
40
|
+
|
|
41
|
+
let nextNumber = 1;
|
|
42
|
+
if (lastTag) {
|
|
43
|
+
const lastNumber = parseInt(lastTag.split(".")[1], 10);
|
|
44
|
+
nextNumber = lastNumber + 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `v${today}.${nextNumber}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getLastTaggableCommitOnMain() {
|
|
51
|
+
const commits = runCommand(`git log origin/main --pretty=format:"%h%x09%s"`).split("\n");
|
|
52
|
+
|
|
53
|
+
for (const line of commits) {
|
|
54
|
+
const [hash, message] = line.trim().split("\t");
|
|
55
|
+
if (!/skip-ci/i.test(message)) {
|
|
56
|
+
return { hash, message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.error("❌ No suitable commit found on origin/main without 'skip-ci' in the message.");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tagLatestCommitOnMain(tag) {
|
|
65
|
+
const result = getLastTaggableCommitOnMain();
|
|
66
|
+
if (!result) return;
|
|
67
|
+
|
|
68
|
+
const { hash, message } = result;
|
|
69
|
+
|
|
70
|
+
const tagsOnCommit = runCommand(`git tag --contains ${hash}`, true)
|
|
71
|
+
.split("\n")
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
|
|
74
|
+
if (tagsOnCommit.length > 0) {
|
|
75
|
+
console.error(`❌ Commit ${hash} - ${message} is already tagged with: ${tagsOnCommit.join(", ")}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
rl.question(
|
|
85
|
+
`👉 New tag will be: ${tag}\nTag commit: ${hash} - ${message}\nPress Enter to confirm, or Ctrl+C to cancel: `,
|
|
86
|
+
() => {
|
|
87
|
+
runCommand(`git tag ${tag} ${hash}`);
|
|
88
|
+
runCommand(`git push origin ${tag}`);
|
|
89
|
+
console.log(`✅ Tagged ${hash} on origin/main with ${tag}`);
|
|
90
|
+
rl.close();
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function isGitRepo() {
|
|
95
|
+
try {
|
|
96
|
+
const result = execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' })
|
|
97
|
+
.toString()
|
|
98
|
+
.trim();
|
|
99
|
+
return result === 'true';
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function fetchOriginMain() {
|
|
105
|
+
if (!isGitRepo()) {
|
|
106
|
+
console.error("❌ Not a git repository.");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("📥 Fetching latest from origin...");
|
|
111
|
+
runCommand("git fetch origin main");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = function () {
|
|
115
|
+
fetchOriginMain();
|
|
116
|
+
const tag = getNextGlobalTag();
|
|
117
|
+
tagLatestCommitOnMain(tag);
|
|
118
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
fetch_secret() {
|
|
4
|
+
local sealed_secret_path=$1
|
|
5
|
+
|
|
6
|
+
# Prompt if no path provided
|
|
7
|
+
while [[ -z "$sealed_secret_path" ]]; do
|
|
8
|
+
echo -n "📄 Enter path to the SealedSecret YAML file: "
|
|
9
|
+
read sealed_secret_path
|
|
10
|
+
done
|
|
11
|
+
|
|
12
|
+
# Ensure file exists
|
|
13
|
+
if [[ ! -f "$sealed_secret_path" ]]; then
|
|
14
|
+
echo "❌ File not found: $sealed_secret_path"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if ! cat "$sealed_secret_path" | kubeseal --validate; then
|
|
19
|
+
echo "❌ SealedSecret validation failed for: $sealed_secret_path"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Check if the kind of the file is SealedSecret
|
|
24
|
+
local kind=$(yq eval '.kind' "$sealed_secret_path")
|
|
25
|
+
if [[ "$kind" != "SealedSecret" ]]; then
|
|
26
|
+
echo "❌ The file is not a SealedSecret."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Derive the secret_output_path by removing 'sealed-' from the sealed_secret_path
|
|
31
|
+
local secret_output_path="${sealed_secret_path/sealed-/}"
|
|
32
|
+
|
|
33
|
+
# Extract name and namespace from spec.template.metadata
|
|
34
|
+
local secret_name=$(yq eval '.spec.template.metadata.name // ""' "$sealed_secret_path")
|
|
35
|
+
local secret_namespace=$(yq eval '.spec.template.metadata.namespace // ""' "$sealed_secret_path")
|
|
36
|
+
|
|
37
|
+
if [[ -z "$secret_name" || -z "$secret_namespace" ]]; then
|
|
38
|
+
echo "❌ Missing name or namespace in spec.template.metadata."
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "📡 Fetching secret: $secret_name from namespace: $secret_namespace"
|
|
43
|
+
|
|
44
|
+
# Fetch the secret from Kubernetes
|
|
45
|
+
raw_secret=$(kubectl get secret "$secret_name" -n "$secret_namespace" -o yaml 2>/dev/null)
|
|
46
|
+
|
|
47
|
+
if [[ -z "$raw_secret" ]]; then
|
|
48
|
+
echo "❌ Failed to fetch the secret from Kubernetes."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Extract the encryptedData keys into a list of strings
|
|
53
|
+
encrypted_data_keys=$(yq eval -o=json '.spec.encryptedData | keys' "$sealed_secret_path" | jq -c '.')
|
|
54
|
+
|
|
55
|
+
# Extract metadata from the sealed secret and convert it to JSON
|
|
56
|
+
metadata_json=$(yq eval '.spec.template.metadata' "$sealed_secret_path" | yq eval -o=json)
|
|
57
|
+
|
|
58
|
+
# Construct new Secret YAML
|
|
59
|
+
echo "$raw_secret" | yq eval "
|
|
60
|
+
.apiVersion = \"v1\" |
|
|
61
|
+
.kind = \"Secret\" |
|
|
62
|
+
.metadata = $metadata_json |
|
|
63
|
+
del(.metadata.creationTimestamp) |
|
|
64
|
+
.stringData = (.data | with_entries(select(.key as \$k | ($encrypted_data_keys | map(select(. == \$k))) | length > 0) | .value |= @base64d)) |
|
|
65
|
+
del(.data)
|
|
66
|
+
" - > "$secret_output_path"
|
|
67
|
+
|
|
68
|
+
if [[ $? -ne 0 ]]; then
|
|
69
|
+
echo "❌ Failed to process the secret."
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
echo "✅ Secret has been written to: $secret_output_path"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Accept path as optional argument
|
|
77
|
+
sealed_secret_path="$1"
|
|
78
|
+
fetch_secret "$sealed_secret_path"
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@salefronts/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"sf": "bin/sf"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"postinstall": "chmod +x bin/sf",
|
|
9
|
+
"publish": "publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"jsonwebtoken": "^9.0.2",
|
|
13
|
+
"niceware": "^4.0.0",
|
|
14
|
+
"uuidv7": "~1.0.2"
|
|
15
|
+
}
|
|
16
|
+
}
|