cmd_remote.v

6.9 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.fmt14import src.machine15import src.remote1617pub fn cmd_remote(raw []string) int {18	args := parse('remote', raw, Options{19		flags: ['json', 'forget']20	}) or { return usage_error(err.msg()) }21	if args.flag('forget') {22		if !machine.has_remote() {23			return refuse(args.json(), 'this machine has no remote to forget')24		}25		machine.forget_remote() or { return refuse(args.json(), err.msg()) }26		if args.json() {27			emit_json(json.encode(RemoteJson{28				configured: false29			}))30			return 031		}32		ok("forgot this machine's remote")33		return 034	}35	if args.positional.len == 0 {36		if !machine.has_remote() {37			if args.json() {38				emit_json(json.encode(RemoteJson{39					configured: false40				}))41				return 042			}43			return setup_wizard()44		}45		settings := machine.load_remote() or { return refuse(args.json(), err.msg()) }46		if args.json() {47			emit_json(json.encode(RemoteJson{48				configured: true49				kind:       settings.kind50				target:     describe(settings)51			}))52			return 053		}54		ok('remote: ${describe(settings)}')55		note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')56		return 057	}58	// A non-interactive form, still tested before it is saved.59	return set_remote(args)60}6162fn set_remote(args Args) int {63	kind := args.positional_at(0)64	match kind {65		machine.kind_folder {66			path := args.positional_at(1)67			if path == '' {68				return usage_error('remote folder needs a path: koh remote folder /Volumes/usb/koh')69			}70			settings := machine.RemoteSettings{71				kind: machine.kind_folder72				path: os.real_path(path)73			}74			return save_after_check(settings, args, '', '')75		}76		machine.kind_s3 {77			endpoint := args.positional_at(1)78			bucket := args.positional_at(2)79			region := args.positional_at(3)80			if endpoint == '' || bucket == '' || region == '' {81				return usage_error('remote s3 needs an endpoint, bucket and region: koh remote s3 https://s3.fr-par.scw.cloud my-bucket fr-par')82			}83			access_key, secret_key := s3_keys() or { return refuse(args.json(), err.msg()) }84			settings := machine.RemoteSettings{85				kind:     machine.kind_s386				endpoint: endpoint87				bucket:   bucket88				region:   region89			}90			return save_after_check(settings, args, access_key, secret_key)91		}92		else {93			return usage_error('remote takes "folder" or "s3", not "${kind}"')94		}95	}96}9798// save_after_check is the whole point: nothing is written to ~/.config/koh99// until the remote has passed the test.100fn save_after_check(settings machine.RemoteSettings, args Args, access_key string, secret_key string) int {101	mut backend := build_backend(settings, access_key, secret_key) or {102		return refuse(args.json(), err.msg())103	}104	remote.check(mut backend) or {105		return refuse(args.json(), 'the remote did not pass its test: ${err.msg()}')106	}107	machine.save_remote(settings) or { return refuse(args.json(), err.msg()) }108	if settings.kind == machine.kind_s3 {109		machine.save_credentials(machine.Credentials{110			access_key: access_key111			secret_key: secret_key112		}) or { return refuse(args.json(), err.msg()) }113	}114	if args.json() {115		emit_json(json.encode(RemoteJson{116			configured: true117			kind:       settings.kind118			target:     describe(settings)119		}))120		return 0121	}122	ok('remote set to ${describe(settings)}')123	note('every repo on this machine now offers to it')124	return 0125}126127fn build_backend(settings machine.RemoteSettings, access_key string, secret_key string) !remote.Backend {128	if settings.kind == machine.kind_folder {129		return remote.new_folder(settings.path)!130	}131	return remote.new_s3(settings.endpoint, settings.bucket, settings.region, access_key,132		secret_key)!133}134135fn setup_wizard() int {136	if !interactive() {137		return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')138	}139	println('  choose a remote:')140	println('    ${fmt.amber('1')} folder   a directory, on a disk or a NAS')141	println('    ${fmt.amber('2')} s3       an S3-compatible bucket')142	print('  which? [1] ')143	if os.get_line().trim_space() == '2' {144		return wizard_s3()145	}146	print('  folder path: ')147	path := os.get_line().trim_space()148	if path == '' {149		return fail('no path given — nothing was saved')150	}151	settings := machine.RemoteSettings{152		kind: machine.kind_folder153		path: os.real_path(path)154	}155	return save_after_check(settings, Args{}, '', '')156}157158fn wizard_s3() int {159	print('  endpoint [https://s3.fr-par.scw.cloud]: ')160	endpoint := or_default(os.get_line().trim_space(), 'https://s3.fr-par.scw.cloud')161	print('  bucket: ')162	bucket := os.get_line().trim_space()163	print('  region [fr-par]: ')164	region := or_default(os.get_line().trim_space(), 'fr-par')165	if bucket == '' {166		return fail('no bucket given — nothing was saved')167	}168	access_key, secret_key := s3_keys() or { return fail(err.msg()) }169	settings := machine.RemoteSettings{170		kind:     machine.kind_s3171		endpoint: endpoint172		bucket:   bucket173		region:   region174	}175	return save_after_check(settings, Args{}, access_key, secret_key)176}177178// s3_keys takes the keys from the environment when a script supplies them, and179// otherwise asks, so they never have to be typed into a shell history.180fn s3_keys() !(string, string) {181	access_key := os.getenv('KOH_ACCESS_KEY')182	secret_key := os.getenv('KOH_SECRET_KEY')183	if access_key != '' && secret_key != '' {184		return access_key, secret_key185	}186	if !interactive() {187		return error('S3 keys are missing — set KOH_ACCESS_KEY and KOH_SECRET_KEY, or run koh remote on a terminal')188	}189	print('  access key: ')190	access_key_typed := os.get_line().trim_space()191	// Hidden input needs a terminal on both ends; with output redirected, the192	// key is read like any other line.193	mut secret_key_typed := ''194	if os.is_atty(1) != 0 {195		secret_key_typed = os.input_password('  secret key: ') or {196			// A terminal that cannot hide input still takes a plain line.197			print('  secret key: ')198			os.get_line()199		}200	} else {201		print('  secret key: ')202		secret_key_typed = os.get_line()203	}204	secret_key_typed = secret_key_typed.trim_space()205	if access_key_typed == '' || secret_key_typed == '' {206		return error('both keys are needed — nothing was saved')207	}208	return access_key_typed, secret_key_typed209}210211fn describe(settings machine.RemoteSettings) string {212	if settings.kind == machine.kind_folder {213		return 'folder ${settings.path}'214	}215	return 's3 ${settings.endpoint}/${settings.bucket} (${settings.region})'216}217218fn or_default(value string, fallback string) string {219	return if value == '' { fallback } else { value }220}221222struct RemoteJson {223	configured bool224	kind       string225	target     string226}