#!/usr/bin/env ruby # frozen_string_literal: true require 'rubygems' require 'optparse' require 'yaml' require 'net/http' require 'json' require 'net_http_unix' require 'securerandom' require 'erb' require 'uri' require 'fileutils' class InvalidParametersException < Exception attr_reader :command def initialize(cause, command = nil) super(cause) @command = command end end class CommandPalette def schema global_args = [ { short: '-v', long: '--verbose', help: 'verbose', mandatory: false, value: nil } ] [ { cmd: 'init', help: 'init mkit client', request: { } }, { cmd: 'ps', args: [ { name: 'id', mandatory: false, uri: '/<%=id%>' } ], help: 'show services status (alias for status)', usage: ['[service_id_or_name]'], request: { verb: :get, uri: '/services' } }, { cmd: 'status', args: [ { name: 'id', mandatory: false, uri: '/<%=id%>' } ], help: 'show services status', usage: ['[service_id_or_name]'], request: { verb: :get, uri: '/services' } }, { cmd: 'logs', args: [ { name: 'id', mandatory: true } ], help: 'prints service logs', usage: [''], request: { verb: :get, uri: '/services/<%=id%>/logs' } }, { cmd: 'start', args: [ { name: 'id', mandatory: true } ], help: 'start service', usage: [''], request: { verb: :put, uri: '/services/<%=id%>/start' } }, { cmd: 'stop', args: [ { name: 'id', mandatory: true } ], help: 'stop service', usage: [''], request: { verb: :put, uri: '/services/<%=id%>/stop' } }, { cmd: 'restart', args: [ { name: 'id', mandatory: true } ], help: 'restart service', usage: [''], request: { verb: :put, uri: '/services/<%=id%>/restart' } }, { cmd: 'create', args: [ { name: 'file', mandatory: true } ], help: 'create new service', usage: [''], request: { verb: :post, uri: '/services' } }, { cmd: 'update', args: [ { name: 'file', mandatory: true } ], help: 'update service', usage: [''], request: { verb: :put, uri: '/services/<%=id%>' } }, { cmd: 'rm', args: [ { name: 'id', mandatory: true } ], help: 'remove service', usage: [''], request: { verb: :delete, uri: '/services/<%=id%>' } }, { cmd: 'version', help: 'prints mkit server version', request: { verb: :get, uri: '/mkit/version' } }, { cmd: 'proxy', options: [ { cmd: 'start', request: { verb: :put, uri: '/mkit/proxy/start' }, help: 'start proxy service' }, { cmd: 'stop', request: { verb: :put, uri: '/mkit/proxy/stop' }, help: 'stop proxy service' }, { cmd: 'restart', request: { verb: :put, uri: '/mkit/proxy/restart' }, help: 'restarts proxy service' }, { cmd: 'status', request: { verb: :get, uri: '/mkit/proxy/status' }, help: 'proxy service status' } ], help: 'haproxy status and control', usage: [''] }, { cmd: 'profile', options: [ { cmd: 'set', request: { verb: 'set' }, args: [ { name: 'profile_name', mandatory: true } ], help: 'set mkit client configuration profile' }, { cmd: 'show', request: { verb: 'show' }, help: 'show mkit client current profile' } ], help: 'mkit client configuration profile', usage: ['<[set ]|[show]>'] } ] end end class MKItClient def initialize @root = File.expand_path('..', __dir__) @config_dir = "#{ENV['HOME']}/.mkit" @profile_file = "#{@config_dir}/current" @commands = CommandPalette.new @config_file = "#{@config_dir}/mkitc_config.yml" create_default_config end def create_default_config unless File.exist?(@config_dir) puts "Creating config directory on '#{@config_dir}'..." FileUtils.mkdir_p(@config_dir) end FileUtils.cp("#{@root}/config/mkitc_config.yml", @config_dir) unless File.exist?(@config_file) profile({ verb: 'set' }, { profile_name: 'local' }) unless File.exist?(@profile_file) end def read_configuration(init_call = false) current_profile = File.read(@profile_file) if current_profile.nil? || current_profile.empty? # force set default profile({ verb: 'set' }, { profile_name: 'local' }) current_profile = 'local' end cfg = YAML.load_file(@config_file) if cfg['mkit'].nil? || cfg['mkit'][current_profile.lstrip].nil? raise InvalidParametersException, "invalid configuration found on '~/.mkit' or profile not found" end @configuration = cfg['mkit'][current_profile.lstrip] if !init_call && cfg['my_id'].nil? raise InvalidParametersException.new("Please run ' mkit init' to initialize mkit client.", find_command('init')) end @my_id = cfg['my_id'] cfg end def client(req) read_configuration req['X-API-KEY'] = @my_id uri = URI(@configuration['server.uri']) case uri.scheme when 'https' @client = NetX::HTTPUnix.new(uri.host, uri.port) @client.use_ssl = true @client.verify_mode = OpenSSL::SSL::VERIFY_NONE when 'http' @client = NetX::HTTPUnix.new(uri.host, uri.port) when 'sock' @client = NetX::HTTPUnix.new("unix://#{uri.path}") else raise InvalidParametersException, 'Invalid mkit server uri. Please check configuration' end @client.request(req) end def dict @commands.schema end def find_command(cmd) dict.select { |k| k[:cmd] == cmd }.first end def parse_args(args) cmd = args[0] c = nil # short circuit for help if cmd == 'help' || args.empty? if args.size > 1 c = find_command(args[1]) raise InvalidParametersException, "'#{args[1]}' is not a valid help topic." if c.nil? end return help(cmd: c) else c = find_command(cmd) end raise InvalidParametersException, 'Command not found' if c.nil? command = c myargs = args.dup myargs.delete(cmd) request_hash = {} request = command[:request] unless myargs.empty? # options unless c[:options].nil? command = c[:options].select { |o| o[:cmd] == myargs[0] }.first raise InvalidParametersException.new('Invalid parameters found.', c) if command.nil? || command.empty? myargs.delete_at(0) request = command[:request] end fill_cmd_args(command[:args], myargs, request, request_hash) end raise InvalidParametersException.new('Invalid command or parameters.', c) if request.nil? validate_command(command, request_hash) if respond_to? c[:cmd] send(c[:cmd], request, request_hash) else request(request, request_hash) end end def fill_cmd_args(args, myargs, request, request_hash) return if args.nil? idx = 0 args.each do |a| request_hash[a[:name].to_sym] = myargs[idx] request[:uri] = request[:uri] + a[:uri] unless a[:uri].nil? idx += 1 end end def validate_command(command, request_hash) return if command[:args].nil? command[:args].select { |a| a[:mandatory] == true }.each do |a| if request_hash[a[:name].to_sym].nil? raise InvalidParametersException.new("Missing mandatory parameter: #{a[:name]}", command) end end end def request(request, request_args = nil) req = nil uri = ERB.new(request[:uri]).result_with_hash(request_args) request[:file] = request_args[:file] unless request[:params].nil? || request[:params].empty? uri = uri + '?' + request[:params].map { |k, v| "#{k}=#{v}" }.join('&') end case request[:verb] when :post req = Net::HTTP::Post.new(uri) unless request[:file].nil? (body, boundary) = attach(request[:file]) req.body = body req['Content-Type'] = "multipart/form-data, boundary=#{boundary}" end when :put req = Net::HTTP::Put.new(uri) unless request[:file].nil? (body, boundary) = attach(request[:file]) req.body = body req['Content-Type'] = "multipart/form-data, boundary=#{boundary}" end when :patch req = Net::HTTP::Patch.new(uri) when :get req = Net::HTTP::Get.new(uri) when :delete req = Net::HTTP::Delete.new(uri) end client(req).body end def attach(file) boundary = SecureRandom.alphanumeric body = [] body << "--#{boundary}\r\n" body << "Content-Disposition: form-data; name=file; filename='#{File.basename(file)}'\r\n" body << "Content-Type: text/plain\r\n" body << "\r\n" body << File.read(file) body << "\r\n--#{boundary}--\r\n" [body.join, boundary] end def doIt(args) result = parse_args(args) puts result rescue InvalidParametersException => e help(cause: e) end def help(cause: nil, cmd: nil) msg = '' if cause.nil? my_cmd = cmd else msg += "MKItc: #{cause.message}\n" my_cmd = cause.command end if my_cmd.nil? msg += "\nUsage: mkit [options]\n\n" msg += "Micro k8s on Ruby - a simple tool to mimic a (very) minimalistic k8 cluster\n\n" msg += "Commands:\n\n" dict.each do |c| msg += format("%-10s %s\n", c[:cmd], c[:help]) end msg += "\n" msg += "Run ' mkit help ' for specific command information.\n\n" else msg += format("\nUsage: mkit %s %s\n\n", my_cmd[:cmd], my_cmd[:usage].nil? ? '' : my_cmd[:usage].join(' ')) msg += format("%s\n", my_cmd[:help]) unless my_cmd[:options].nil? msg += "\nOptions:\n" my_cmd[:options].each do |c| msg += format("%-10s %s\n", c[:cmd], c[:help]) end end msg += "\n" end puts msg exit 1 end def init(request, request_hash = nil) cfg = read_configuration(true) if cfg['my_id'].nil? my_id = SecureRandom.uuid.gsub('-','') cfg['my_id'] = my_id File.write(@config_file, cfg.to_yaml) puts "Please check if your api-key is on mkitd server allowed keys" else my_id = cfg['my_id'] end puts "Your api-key is #{my_id}" end def create(request, request_hash = nil) unless File.file?(request_hash[:file]) raise InvalidParametersException.new('File not found.', find_command('create')) end yaml = YAML.load_file(request_hash[:file]) if yaml['service'].nil? raise InvalidParametersException.new('Invalid configuration file', find_command('create')) else request(request, request_hash) end end def update(request, request_hash = nil) unless File.file?(request_hash[:file]) raise InvalidParametersException.new('File not found.', find_command('update')) end yaml = YAML.load_file(request_hash[:file]) if yaml['service'].nil? raise InvalidParametersException.new('Invalid configuration file', find_command('update')) else id = yaml['service']['name'] request_hash[:id] = id request(request, request_hash) end end def profile(request, request_hash = {}) cfg = YAML.load_file("#{@config_dir}/mkitc_config.yml") cmd = find_command('profile') if cfg['mkit'].nil? raise InvalidParametersException.new( "Invalid configuration on '~/.mkit'\nPlease fix or clean up for defaults apply", cmd ) end case request[:verb] when 'set' profile = request_hash[:profile_name] if cfg['mkit'][profile.lstrip].nil? raise InvalidParametersException.new("Profile not found on '~/.mkit' configuration", cmd) end puts "Setting current profile to #{profile}." File.write(@profile_file, request_hash[:profile_name]) '' when 'show' active = File.read("#{@config_dir}/current") cfg['mkit'].map do |k, _v| if k == active "*#{k}" else k end end.join(' ') else raise InvalidParametersException.new("Invalid 'profile' operation", cmd) end end end # # go # client = MKItClient.new client.doIt(ARGV.dup) # # if ARGV.any? # parse args # host, socket, config file # end