This commit is contained in:
mik-tf 2024-10-05 18:31:50 -04:00
parent b7669954d6
commit 3947c93e59

757
flist.v
View File

@ -4,484 +4,495 @@ import term
import json import json
import x.json2 import x.json2
const ( const token_file = os.join_path(os.home_dir(), '.config', 'tfhubtoken')
token_file = os.join_path(os.home_dir(), '.config', 'tfhubtoken') const binary_location = if os.user_os() == 'windows' {
binary_location = if os.user_os() == 'windows' { 'C:\\Program Files\\flist\\flist.exe'
'C:\\Program Files\\flist\\flist.exe' } else {
} else { '/usr/local/bin/flist'
'/usr/local/bin/flist' }
}
)
struct FlistItem { struct FlistItem {
name string name string
} }
struct Payload { struct Payload {
username string username string
} }
struct Response { struct Response {
payload Payload payload Payload
} }
fn error_message(msg string) { fn error_message(msg string) {
println(term.red('\nError: ') + msg) println(term.red('\nError: ') + msg)
println(term.yellow('Run \'flist help\' for usage information.\n')) println(term.yellow("Run 'flist help' for usage information.\n"))
} }
fn success_message(msg string) { fn success_message(msg string) {
println(term.green('\n' + msg + '\n')) println(term.green('\n' + msg + '\n'))
} }
fn info_message(msg string) { fn info_message(msg string) {
println(term.blue('\n' + msg + '\n')) println(term.blue('\n' + msg + '\n'))
} }
fn create_box(content []string, padding int) string { fn create_box(content []string, padding int) string {
mut max_width := 0 mut max_width := 0
for line in content { for line in content {
clean_line := term.strip_ansi(line) clean_line := term.strip_ansi(line)
if clean_line.len > max_width { if clean_line.len > max_width {
max_width = clean_line.len max_width = clean_line.len
} }
} }
max_width += padding * 2 max_width += padding * 2
separator := '━'.repeat(max_width + 2) // +2 for left and right borders separator := '━'.repeat(max_width + 2) // +2 for left and right borders
mut box_content := term.blue('$separator') + '\n' mut box_content := term.blue('${separator}') + '\n'
for line in content { for line in content {
clean_line := term.strip_ansi(line) clean_line := term.strip_ansi(line)
padding_left := ' '.repeat(padding) padding_left := ' '.repeat(padding)
padding_right := ' '.repeat(max_width - clean_line.len) padding_right := ' '.repeat(max_width - clean_line.len)
box_content += term.blue('┃') + padding_left + line + padding_right + term.blue('┃') + '\n' box_content += term.blue('┃') + padding_left + line + padding_right + term.blue('┃') +
} '\n'
}
box_content += term.blue('$separator') box_content += term.blue('${separator}')
return box_content return box_content
} }
fn install() { fn install() {
info_message('Installing Flist CLI...') info_message('Installing Flist CLI...')
current_exe := os.executable() current_exe := os.executable()
if os.exists(current_exe) { if os.exists(current_exe) {
os.mkdir_all(os.dir(binary_location)) or { panic(err) } os.mkdir_all(os.dir(binary_location)) or { panic(err) }
os.cp(current_exe, binary_location) or { panic(err) } os.cp(current_exe, binary_location) or { panic(err) }
os.chmod(binary_location, 0o755) or { panic(err) } os.chmod(binary_location, 0o755) or { panic(err) }
success_message('Flist CLI has been installed to ' + binary_location) success_message('Flist CLI has been installed to ' + binary_location)
info_message('You can now use it by running \'flist help\'') info_message("You can now use it by running 'flist help'")
} else { } else {
error_message('Cannot find the executable file') error_message('Cannot find the executable file')
exit(1) exit(1)
} }
} }
fn uninstall() { fn uninstall() {
info_message('Uninstalling Flist CLI...') info_message('Uninstalling Flist CLI...')
if os.exists(binary_location) { if os.exists(binary_location) {
os.rm(binary_location) or { panic(err) } os.rm(binary_location) or { panic(err) }
success_message('Flist CLI has been removed from ' + binary_location) success_message('Flist CLI has been removed from ' + binary_location)
} else { } else {
info_message('Flist CLI is not installed at ' + binary_location) info_message('Flist CLI is not installed at ' + binary_location)
} }
} }
fn login() { fn login() {
mut token_exists := os.exists(token_file) mut token_exists := os.exists(token_file)
if !token_exists { if !token_exists {
tfhub_token := os.input('Please enter your tfhub token: ') tfhub_token := os.input('Please enter your tfhub token: ')
os.write_file(token_file, tfhub_token) or { panic(err) } os.write_file(token_file, tfhub_token) or { panic(err) }
success_message('Token saved in ' + token_file) success_message('Token saved in ' + token_file)
} else { } else {
info_message('Your Flist Hub token is already saved.') info_message('Your Flist Hub token is already saved.')
} }
result := os.system('sudo docker login') result := os.system('sudo docker login')
if result == 0 {
info_message('\nYou are already logged in to Docker.')
}
success_message('Login process completed.') if result == 0 {
info_message('\nYou are already logged in to Docker.')
}
success_message('Login process completed.')
} }
fn logout() { fn logout() {
if os.exists(token_file) { if os.exists(token_file) {
os.rm(token_file) or {panic(err)} os.rm(token_file) or { panic(err) }
success_message('Your Flist Hub Token has been removed') success_message('Your Flist Hub Token has been removed')
} else { } else {
info_message('Your Flist Hub Token was already not present.') info_message('Your Flist Hub Token was already not present.')
} }
exit_code := os.system('sudo docker logout') exit_code := os.system('sudo docker logout')
if exit_code != 0 { if exit_code != 0 {
error_message('Failed to log out from Docker Hub.') error_message('Failed to log out from Docker Hub.')
} }
success_message('You are now logged out of Docker Hub and your Flist Hub token has been removed.') success_message('You are now logged out of Docker Hub and your Flist Hub token has been removed.')
} }
fn get_docker_credential() !string { fn get_docker_credential() !string {
// Try to get the Docker credential automatically // Try to get the Docker credential automatically
credential := get_docker_credential_auto() or { credential := get_docker_credential_auto() or {
// If automatic retrieval fails, prompt the user for input // If automatic retrieval fails, prompt the user for input
println(term.yellow('\nCouldn\'t find your Docker username automatically.')) println(term.yellow("\nCouldn't find your Docker username automatically."))
username := os.input('Please enter your Docker username and press ENTER: ') username := os.input('Please enter your Docker username and press ENTER: ')
if username.trim_space() == '' { if username.trim_space() == '' {
return error('No Docker username provided') return error('No Docker username provided')
} }
return username.trim_space() return username.trim_space()
} }
return credential return credential
} }
fn get_docker_credential_auto() !string { fn get_docker_credential_auto() !string {
// First, try to get the Docker username using the system info command // First, try to get the Docker username using the system info command
system_info_result := os.execute('sudo docker system info | grep \'Username\' | cut -d \' \' -f 3') system_info_result := os.execute("sudo docker system info | grep 'Username' | cut -d ' ' -f 3")
if system_info_result.exit_code == 0 && system_info_result.output.trim_space() != '' { if system_info_result.exit_code == 0 && system_info_result.output.trim_space() != '' {
return system_info_result.output.trim_space() return system_info_result.output.trim_space()
} }
// If the above method fails, proceed with the current method // If the above method fails, proceed with the current method
// Read the Docker config file // Read the Docker config file
config_path := os.join_path(os.home_dir(), '.docker', 'config.json') config_path := os.join_path(os.home_dir(), '.docker', 'config.json')
config_content := os.read_file(config_path) or { config_content := os.read_file(config_path) or {
return error('Failed to read Docker config file: $err') return error('Failed to read Docker config file: ${err}')
} }
// Parse the JSON content // Parse the JSON content
config := json2.raw_decode(config_content) or { config := json2.raw_decode(config_content) or {
return error('Failed to parse Docker config: $err') return error('Failed to parse Docker config: ${err}')
} }
// Extract the credsStore value // Extract the credsStore value
creds_store := config.as_map()['credsStore'] or { creds_store := config.as_map()['credsStore'] or {
return error('credsStore not found in Docker config') return error('credsStore not found in Docker config')
}.str() }.str()
// Execute the docker-credential command // Execute the docker-credential command
cred_helper := 'docker-credential-$creds_store' cred_helper := 'docker-credential-${creds_store}'
cred_output := os.execute('$cred_helper list') cred_output := os.execute('${cred_helper} list')
if cred_output.exit_code != 0 { if cred_output.exit_code != 0 {
return error('Failed to execute $cred_helper: ${cred_output.output}') return error('Failed to execute ${cred_helper}: ${cred_output.output}')
} }
// Parse the credential list // Parse the credential list
cred_list := json2.raw_decode(cred_output.output) or { cred_list := json2.raw_decode(cred_output.output) or {
return error('Failed to parse credential list: $err') return error('Failed to parse credential list: ${err}')
} }
// Find the first docker.io entry // Find the first docker.io entry
for key, value in cred_list.as_map() { for key, value in cred_list.as_map() {
if key.contains('docker.io') { if key.contains('docker.io') {
return value.str() return value.str()
} }
} }
return error('No docker.io credential found') return error('No docker.io credential found')
} }
fn push(tag string) { fn push(tag string) {
docker_user := get_docker_credential() or { docker_user := get_docker_credential() or {
error_message('Failed to get Docker username: $err') error_message('Failed to get Docker username: ${err}')
exit(1) exit(1)
} }
info_message('Docker username: $docker_user') info_message('Docker username: ${docker_user}')
full_tag := '${docker_user}/${tag}' full_tag := '${docker_user}/${tag}'
tfhub_token := os.read_file(token_file) or { tfhub_token := os.read_file(token_file) or {
error_message('No token found. Please run \'flist login\' first.') error_message("No token found. Please run 'flist login' first.")
exit(1) exit(1)
} }
info_message('Starting Docker build') info_message('Starting Docker build')
if os.system('sudo docker buildx build -t ${full_tag} .') != 0 { if os.system('sudo docker buildx build -t ${full_tag} .') != 0 {
error_message('Docker build failed') error_message('Docker build failed')
exit(1) exit(1)
} }
info_message('Finished local Docker build, now pushing to Docker Hub') info_message('Finished local Docker build, now pushing to Docker Hub')
if os.system('sudo docker push ${full_tag}') != 0 { if os.system('sudo docker push ${full_tag}') != 0 {
error_message('Docker push failed') error_message('Docker push failed')
exit(1) exit(1)
} }
info_message('Converting Docker image to Flist now...') info_message('Converting Docker image to Flist now...')
url := 'https://hub.grid.tf/api/flist/me/docker' url := 'https://hub.grid.tf/api/flist/me/docker'
data := 'image=$full_tag' data := 'image=${full_tag}'
mut config := http.FetchConfig{ mut config := http.FetchConfig{
url: url url: url
method: .post method: .post
data: data data: data
header: http.new_header( header: http.new_header(
key: .authorization key: .authorization
value: 'bearer $tfhub_token' value: 'bearer ${tfhub_token}'
) )
} }
config.header.add_custom('Content-Type', 'application/x-www-form-urlencoded') or { config.header.add_custom('Content-Type', 'application/x-www-form-urlencoded') or {
error_message('Add custom failed: $err') error_message('Add custom failed: ${err}')
exit(1) exit(1)
} }
response := http.fetch(config) or { response := http.fetch(config) or {
error_message('HTTP POST request failed: $err') error_message('HTTP POST request failed: ${err}')
exit(1) exit(1)
} }
if response.status_code == 200 { if response.status_code == 200 {
hub_user := get_hub_username(tfhub_token) or { hub_user := get_hub_username(tfhub_token) or {
error_message('Failed to get hub username') error_message('Failed to get hub username')
exit(1) exit(1)
} }
flist_name := tag.replace(':', '-') + '.flist' flist_name := tag.replace(':', '-') + '.flist'
flist_url := 'https://hub.grid.tf/$hub_user/$flist_name' flist_url := 'https://hub.grid.tf/${hub_user}/${flist_name}'
success_content := [ success_content := [
term.bold(term.green('Success!') + ' Your Flist has been created and pushed to the TF Hub.'), term.bold(term.green('Success!') +
'', ' Your Flist has been created and pushed to the TF Hub.'),
term.bold('Flist Details:'), '',
term.yellow('Name: ') + flist_name, term.bold('Flist Details:'),
term.yellow('User: ') + hub_user, term.yellow('Name: ') + flist_name,
term.yellow('URL: ') + flist_url, term.yellow('User: ') + hub_user,
'', term.yellow('URL: ') + flist_url,
'You can access your Flist using the URL above.', '',
'To manage your Flists, use the following commands:', 'You can access your Flist using the URL above.',
term.yellow(' flist ls ') + '- List all your Flists', 'To manage your Flists, use the following commands:',
term.yellow(' flist delete') + '- Delete an Flist', term.yellow(' flist ls ') + '- List all your Flists',
term.yellow(' flist rename') + '- Rename an Flist' term.yellow(' flist delete') + '- Delete an Flist',
] term.yellow(' flist rename') + '- Rename an Flist',
]
println(create_box(success_content, 2))
} else { println(create_box(success_content, 2))
error_message('Request failed with status code: ${response.status_code}') } else {
println('Response body:') error_message('Request failed with status code: ${response.status_code}')
println(response.body) println('Response body:')
exit(1) println(response.body)
} exit(1)
}
} }
fn delete(flist_name string) { fn delete(flist_name string) {
tfhub_token := os.read_file(token_file) or { tfhub_token := os.read_file(token_file) or {
error_message('No token found. Please run \'flist login\' first.') error_message("No token found. Please run 'flist login' first.")
exit(1) exit(1)
} }
info_message('Deleting Flist: ' + flist_name) info_message('Deleting Flist: ' + flist_name)
url := 'https://hub.grid.tf/api/flist/me/' + flist_name url := 'https://hub.grid.tf/api/flist/me/' + flist_name
config := http.FetchConfig{ config := http.FetchConfig{
url: url url: url
method: .delete method: .delete
header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token) header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token)
} }
response := http.fetch(config) or { response := http.fetch(config) or {
error_message('Failed to send delete request: ' + err.msg()) error_message('Failed to send delete request: ' + err.msg())
exit(1) exit(1)
} }
if response.status_code == 200 { if response.status_code == 200 {
success_message('Deletion request sent successfully.') success_message('Deletion request sent successfully.')
} else { } else {
error_message('Deletion request failed with status code: ' + response.status_code.str()) error_message('Deletion request failed with status code: ' + response.status_code.str())
} }
} }
fn rename(flist_name string, new_flist_name string) { fn rename(flist_name string, new_flist_name string) {
tfhub_token := os.read_file(token_file) or { tfhub_token := os.read_file(token_file) or {
error_message('No token found. Please run \'flist login\' first.') error_message("No token found. Please run 'flist login' first.")
exit(1) exit(1)
} }
info_message('Renaming Flist: ' + flist_name + ' to ' + new_flist_name) info_message('Renaming Flist: ' + flist_name + ' to ' + new_flist_name)
url := 'https://hub.grid.tf/api/flist/me/' + flist_name + '/rename/' + new_flist_name url := 'https://hub.grid.tf/api/flist/me/' + flist_name + '/rename/' + new_flist_name
config := http.FetchConfig{ config := http.FetchConfig{
url: url url: url
method: .get method: .get
header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token) header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token)
} }
response := http.fetch(config) or { response := http.fetch(config) or {
error_message('Failed to send rename request: ' + err.msg()) error_message('Failed to send rename request: ' + err.msg())
exit(1) exit(1)
} }
if response.status_code == 200 { if response.status_code == 200 {
success_message('Rename request sent successfully.') success_message('Rename request sent successfully.')
} else { } else {
error_message('Rename request failed with status code: ' + response.status_code.str()) error_message('Rename request failed with status code: ' + response.status_code.str())
} }
} }
fn get_hub_username(tfhub_token string) ?string { fn get_hub_username(tfhub_token string) ?string {
url := 'https://hub.grid.tf/api/flist/me' url := 'https://hub.grid.tf/api/flist/me'
config := http.FetchConfig{ config := http.FetchConfig{
url: url url: url
method: .get method: .get
header: http.new_header( header: http.new_header(
key: .authorization key: .authorization
value: 'bearer $tfhub_token' value: 'bearer ${tfhub_token}'
) )
} }
response := http.fetch(config) or { response := http.fetch(config) or {
error_message('Failed to fetch hub username: $err') error_message('Failed to fetch hub username: ${err}')
return none return none
} }
if response.status_code != 200 { if response.status_code != 200 {
error_message('Failed to fetch hub username. Status code: $response.status_code') error_message('Failed to fetch hub username. Status code: ${response.status_code}')
return none return none
} }
parsed_response := json.decode(Response, response.body) or { parsed_response := json.decode(Response, response.body) or {
error_message('Failed to parse JSON response: $err') error_message('Failed to parse JSON response: ${err}')
return none return none
} }
if parsed_response.payload.username != '' { if parsed_response.payload.username != '' {
return parsed_response.payload.username return parsed_response.payload.username
} }
error_message('Username not found in response') error_message('Username not found in response')
return none return none
} }
fn ls(show_url bool) { fn ls(show_url bool) {
tfhub_token := os.read_file(token_file) or { tfhub_token := os.read_file(token_file) or {
error_message('No token found. Please run \'flist login\' first.') error_message("No token found. Please run 'flist login' first.")
exit(1) exit(1)
} }
hub_user := get_hub_username(tfhub_token) or { hub_user := get_hub_username(tfhub_token) or {
error_message('Failed to get hub username') error_message('Failed to get hub username')
exit(1) exit(1)
} }
url := 'https://hub.grid.tf/api/flist/$hub_user' url := 'https://hub.grid.tf/api/flist/${hub_user}'
config := http.FetchConfig{ config := http.FetchConfig{
url: url url: url
method: .get method: .get
header: http.new_header( header: http.new_header(
key: .authorization key: .authorization
value: 'bearer $tfhub_token' value: 'bearer ${tfhub_token}'
) )
} }
response := http.fetch(config) or { response := http.fetch(config) or {
error_message('Failed to fetch data: $err') error_message('Failed to fetch data: ${err}')
exit(1) exit(1)
} }
if response.status_code != 200 { if response.status_code != 200 {
error_message('Failed to fetch data. Status code: $response.status_code') error_message('Failed to fetch data. Status code: ${response.status_code}')
exit(1) exit(1)
} }
data := json.decode([]FlistItem, response.body) or { data := json.decode([]FlistItem, response.body) or {
error_message('Failed to parse JSON: $err') error_message('Failed to parse JSON: ${err}')
exit(1) exit(1)
} }
mut content := [term.bold('Flists for user ' + term.green(hub_user) + ':')] mut content := [term.bold('Flists for user ' + term.green(hub_user) + ':')]
for item in data { for item in data {
if show_url { if show_url {
content << term.yellow('> ') + 'https://hub.grid.tf/' + hub_user + '/' + item.name content << term.yellow('> ') + 'https://hub.grid.tf/' + hub_user + '/' + item.name
} else { } else {
content << term.yellow('> ') + item.name content << term.yellow('> ') + item.name
} }
} }
println(create_box(content, 2)) println(create_box(content, 2))
} }
fn help() { fn help() {
welcome_msg := term.bold(term.green('Welcome to the Flist CLI!')) welcome_msg := term.bold(term.green('Welcome to the Flist CLI!'))
println(create_box([welcome_msg], 2)) println(create_box([welcome_msg], 2))
println('This tool turns Dockerfiles and Docker images directly into Flists on the TF Flist Hub, passing by the Docker Hub.\n') println('This tool turns Dockerfiles and Docker images directly into Flists on the TF Flist Hub, passing by the Docker Hub.\n')
println(term.bold('Available commands:')) println(term.bold('Available commands:'))
println(term.blue(' install ') + '- Install the Flist CLI') println(term.blue(' install ') + '- Install the Flist CLI')
println(term.blue(' uninstall ') + '- Uninstall the Flist CLI') println(term.blue(' uninstall ') + '- Uninstall the Flist CLI')
println(term.blue(' login ') + '- Log in to Docker Hub and save the Flist Hub token') println(term.blue(' login ') + '- Log in to Docker Hub and save the Flist Hub token')
println(term.blue(' logout ') + '- Log out of Docker Hub and remove the Flist Hub token') println(term.blue(' logout ') + '- Log out of Docker Hub and remove the Flist Hub token')
println(term.blue(' push ') + '- Build and push a Docker image to Docker Hub, then convert and push it as an Flist to Flist Hub') println(term.blue(' push ') +
println(term.blue(' delete ') + '- Delete an Flist from Flist Hub') '- Build and push a Docker image to Docker Hub, then convert and push it as an Flist to Flist Hub')
println(term.blue(' rename ') + '- Rename an Flist in Flist Hub') println(term.blue(' delete ') + '- Delete an Flist from Flist Hub')
println(term.blue(' ls ') + '- List all Flists of the current user') println(term.blue(' rename ') + '- Rename an Flist in Flist Hub')
println(term.blue(' ls url ') + '- List all Flists of the current user with full URLs') println(term.blue(' ls ') + '- List all Flists of the current user')
println(term.blue(' help ') + '- Display this help message\n') println(term.blue(' ls url ') + '- List all Flists of the current user with full URLs')
println(term.bold('Usage:')) println(term.blue(' help ') + '- Display this help message\n')
println(term.yellow(' sudo ./flist install')) println(term.bold('Usage:'))
println(term.yellow(' sudo flist uninstall')) println(term.yellow(' sudo ./flist install'))
println(term.yellow(' flist login')) println(term.yellow(' sudo flist uninstall'))
println(term.yellow(' flist logout')) println(term.yellow(' flist login'))
println(term.yellow(' flist push <image>:<tag>')) println(term.yellow(' flist logout'))
println(term.yellow(' flist delete <flist_name>')) println(term.yellow(' flist push <image>:<tag>'))
println(term.yellow(' flist rename <flist_name> <new_flist_name>')) println(term.yellow(' flist delete <flist_name>'))
println(term.yellow(' flist ls')) println(term.yellow(' flist rename <flist_name> <new_flist_name>'))
println(term.yellow(' flist ls url')) println(term.yellow(' flist ls'))
println(term.yellow(' flist help\n')) println(term.yellow(' flist ls url'))
println(term.yellow(' flist help\n'))
} }
fn main() { fn main() {
if os.args.len == 1 { if os.args.len == 1 {
help() help()
return return
} }
match os.args[1] { match os.args[1] {
'install' { install() } 'install' {
'uninstall' { uninstall() } install()
'push' { }
if os.args.len == 3 { 'uninstall' {
push(os.args[2]) uninstall()
} else { }
error_message('Incorrect number of arguments for \'push\'.') 'push' {
exit(1) if os.args.len == 3 {
} push(os.args[2])
} } else {
'login' { login() } error_message("Incorrect number of arguments for 'push'.")
'logout' { logout() } exit(1)
'delete' { }
if os.args.len == 3 { }
delete(os.args[2]) 'login' {
} else { login()
error_message('Incorrect number of arguments for \'delete\'.') }
exit(1) 'logout' {
} logout()
} }
'rename' { 'delete' {
if os.args.len == 4 { if os.args.len == 3 {
rename(os.args[2], os.args[3]) delete(os.args[2])
} else { } else {
error_message('Incorrect number of arguments for \'rename\'.') error_message("Incorrect number of arguments for 'delete'.")
exit(1) exit(1)
} }
} }
'ls' { 'rename' {
if os.args.len == 2 { if os.args.len == 4 {
ls(false) rename(os.args[2], os.args[3])
} else if os.args.len == 3 && os.args[2] == 'url' { } else {
ls(true) error_message("Incorrect number of arguments for 'rename'.")
} else { exit(1)
error_message('Incorrect usage of \'ls\'. Use \'ls\' or \'ls url\'.') }
exit(1) }
} 'ls' {
} if os.args.len == 2 {
'help' { help() } ls(false)
else { } else if os.args.len == 3 && os.args[2] == 'url' {
error_message('Unknown command: ' + os.args[1]) ls(true)
exit(1) } else {
} error_message("Incorrect usage of 'ls'. Use 'ls' or 'ls url'.")
} exit(1)
} }
}
'help' {
help()
}
else {
error_message('Unknown command: ' + os.args[1])
exit(1)
}
}
}