cmd_remote.v

6.5 KB · raw

1//! `koh remote` — the machine's remote, set up once and used by every repo.2//!3//! Setup is interactive by default, because it ends with a live connection4//! test: a remote is only saved once it has proved it can store an object and5//! refuse to overwrite it. `folder` and `s3` forms are available for a machine6//! being set up by a script, and an agent can read the current setting with7//! `--json` (which never prints the secret key).89module cli1011import json12import os13import src.machine14import src.remote1516pub fn cmd_remote(raw []string) int {17	args := parse('remote', raw, Options{18		flags: ['json', 'forget']19	}) or { return usage_error(err.msg()) }20	if args.flag('forget') {21		if !machine.has_remote() {22			return refuse(args.json(), 'this machine has no remote to forget')23		}24		machine.forget_remote() or { return refuse(args.json(), err.msg()) }25		if args.json() {26			emit_json(json.encode(RemoteJson{27				configured: false28			}))29			return 030		}31		ok("forgot this machine's remote")32		return 033	}34	if args.positional.len == 0 {35		if !machine.has_remote() {36			if args.json() {37				emit_json(json.encode(RemoteJson{38					configured: false39				}))40				return 041			}42			return setup_wizard()43		}44		settings := machine.load_remote() or { return refuse(args.json(), err.msg()) }45		if args.json() {46			emit_json(json.encode(RemoteJson{47				configured: true48				kind:       settings.kind49				target:     describe(settings)50			}))51			return 052		}53		ok('remote: ${describe(settings)}')54		note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')55		return 056	}57	// A non-interactive form, still tested before it is saved.58	return set_remote(args)59}6061fn set_remote(args Args) int {62	kind := args.positional_at(0)63	match kind {64		machine.kind_folder {65			path := args.positional_at(1)66			if path == '' {67				return usage_error('remote folder needs a path: koh remote folder /Volumes/usb/koh')68			}69			settings := machine.RemoteSettings{70				kind: machine.kind_folder71				path: os.real_path(path)72			}73			return save_after_check(settings, args, '', '')74		}75		machine.kind_s3 {76			endpoint := args.positional_at(1)77			bucket := args.positional_at(2)78			region := args.positional_at(3)79			if endpoint == '' || bucket == '' || region == '' {80				return usage_error('remote s3 needs an endpoint, bucket and region: koh remote s3 https://s3.fr-par.scw.cloud my-bucket fr-par')81			}82			access_key, secret_key := s3_keys() or { return refuse(args.json(), err.msg()) }83			settings := machine.RemoteSettings{84				kind:     machine.kind_s385				endpoint: endpoint86				bucket:   bucket87				region:   region88			}89			return save_after_check(settings, args, access_key, secret_key)90		}91		else {92			return usage_error('remote takes "folder" or "s3", not "${kind}"')93		}94	}95}9697// save_after_check is the whole point: nothing is written to ~/.config/koh98// until the remote has passed the test.99fn save_after_check(settings machine.RemoteSettings, args Args, access_key string, secret_key string) int {100	mut backend := build_backend(settings, access_key, secret_key) or {101		return refuse(args.json(), err.msg())102	}103	remote.check(mut backend) or {104		return refuse(args.json(), 'the remote did not pass its test: ${err.msg()}')105	}106	machine.save_remote(settings) or { return refuse(args.json(), err.msg()) }107	if settings.kind == machine.kind_s3 {108		machine.save_credentials(machine.Credentials{109			access_key: access_key110			secret_key: secret_key111		}) or { return refuse(args.json(), err.msg()) }112	}113	if args.json() {114		emit_json(json.encode(RemoteJson{115			configured: true116			kind:       settings.kind117			target:     describe(settings)118		}))119		return 0120	}121	ok('remote set to ${describe(settings)}')122	note('every repo on this machine now offers to it')123	return 0124}125126fn build_backend(settings machine.RemoteSettings, access_key string, secret_key string) !remote.Backend {127	if settings.kind == machine.kind_folder {128		return remote.new_folder(settings.path)!129	}130	return remote.new_s3(settings.endpoint, settings.bucket, settings.region, access_key,131		secret_key)!132}133134fn setup_wizard() int {135	if !interactive() {136		return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')137	}138	println('  choose a remote:')139	println('    ${amber('1')} folder   a directory, on a disk or a NAS')140	println('    ${amber('2')} s3       an S3-compatible bucket')141	print('  which? [1] ')142	if os.get_line().trim_space() == '2' {143		return wizard_s3()144	}145	print('  folder path: ')146	path := os.get_line().trim_space()147	if path == '' {148		return fail('no path given — nothing was saved')149	}150	settings := machine.RemoteSettings{151		kind: machine.kind_folder152		path: os.real_path(path)153	}154	return save_after_check(settings, Args{}, '', '')155}156157fn wizard_s3() int {158	print('  endpoint [https://s3.fr-par.scw.cloud]: ')159	endpoint := or_default(os.get_line().trim_space(), 'https://s3.fr-par.scw.cloud')160	print('  bucket: ')161	bucket := os.get_line().trim_space()162	print('  region [fr-par]: ')163	region := or_default(os.get_line().trim_space(), 'fr-par')164	if bucket == '' {165		return fail('no bucket given — nothing was saved')166	}167	access_key, secret_key := s3_keys() or { return fail(err.msg()) }168	settings := machine.RemoteSettings{169		kind:     machine.kind_s3170		endpoint: endpoint171		bucket:   bucket172		region:   region173	}174	return save_after_check(settings, Args{}, access_key, secret_key)175}176177// s3_keys takes the keys from the environment when a script supplies them, and178// otherwise asks, so they never have to be typed into a shell history.179fn s3_keys() !(string, string) {180	access_key := os.getenv('KOH_ACCESS_KEY')181	secret_key := os.getenv('KOH_SECRET_KEY')182	if access_key != '' && secret_key != '' {183		return access_key, secret_key184	}185	if !interactive() {186		return error('S3 keys are missing — set KOH_ACCESS_KEY and KOH_SECRET_KEY, or run koh remote on a terminal')187	}188	print('  access key: ')189	access_key_typed := os.get_line().trim_space()190	print('  secret key: ')191	secret_key_typed := os.get_line().trim_space()192	if access_key_typed == '' || secret_key_typed == '' {193		return error('both keys are needed — nothing was saved')194	}195	return access_key_typed, secret_key_typed196}197198fn describe(settings machine.RemoteSettings) string {199	if settings.kind == machine.kind_folder {200		return 'folder ${settings.path}'201	}202	return 's3 ${settings.endpoint}/${settings.bucket} (${settings.region})'203}204205fn or_default(value string, fallback string) string {206	return if value == '' { fallback } else { value }207}208209struct RemoteJson {210	configured bool211	kind       string212	target     string213}