Permalink
Browse files

Add kubernetes support

This adds a simple way to get a working kubernetes cluster up and
running. It deliberately doesn't require any special dependencies, which
makes it perfect for use in environments where these might be harder to
obtain, such as the atomic host. Since kubernetes development is moving
quite quickly, it allows you to specify your own kubernetes templates if
you'd like to use something different from what is supplied, or if you'd
like to test a variant. It will automatically kubectl create and update
your .json files, but it call also attempt to kubectl roll for a more
realistic experience. If you are clever to match the name of docker
containers that you are building with oh-my-vagrant to what you are
using in kubernetes, then you'll have a great development environment
where a vagrant rsync && vagrant provision is the only thing you'll have
to run to get a new version of your app running in kubernetes. happy
hacking!
  • Loading branch information...
1 parent e17254c commit 1f26e5bda5d7585f7b26babcb56a529cc34b96bd @purpleidea committed Apr 2, 2015
View
@@ -16,3 +16,6 @@
[submodule "vagrant/xdg"]
path = vagrant/gems/xdg
url = https://github.com/rubyworks/xdg
+[submodule "vagrant/kubernetes/templates/default"]
+ path = vagrant/kubernetes/templates/default
+ url = https://github.com/purpleidea/omv-ktemplates
View
@@ -0,0 +1,37 @@
+---
+:domain: example.com
+:network: 192.168.123.0/24
+:image: centos-7.0-docker
+:boxurlprefix: ''
+:sync: rsync
+:folder: ''
+:extern:
+- type: git
+ system: kubernetes
+ repository: ~/code/kubernetes-simple1
+ directory: fakeapp1
+- type: git
+ system: kubernetes
+ repository: ~/code/kubernetes-simple2
+ directory: fakeapp2
+:puppet: false
+:classes: []
+:docker: []
+:kubernetes:
+ master: omv0
+ applications:
+ - fakeapp1/foo1.json
+ - file: fakeapp1/foo2.json
+ roll: 'true'
+ - fakeapp2/foo.json
+:ansible: []
+:playbook: []
+:cachier: false
+:vms: []
+:namespace: omv
+:count: 3
+:username: ''
+:password: ''
+:poolid: true
+:repos: []
+:reallyrm: false
View
@@ -26,6 +26,9 @@ VAGRANTFILE_API_VERSION = '2'
require 'ipaddr'
require 'yaml'
+require 'erb'
+require 'ostruct'
+require 'base64'
#
# globals
@@ -73,6 +76,7 @@ extern = [] # default external module definitions
puppet = false # default use of puppet or not
classes = [] # default list or hash of classes to include
docker = false # default use of docker or not
+kubernetes = false # default use of kubernetes or not
ansible = [] # default ansible group list
playbook = [] # default ansible playbook
cachier = false # default cachier usage
@@ -96,6 +100,17 @@ def array_values_to_array_of_hashes(l)
return result
end
+# useful for unindenting heredoc's
+class String
+ def unindent
+ gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, '')
+ end
+end
+# eg:
+#test = <<-EOT.unindent
+# 42
+#EOT
+
projectdir = File.expand_path File.dirname(__FILE__) # vagrant project dir!!
#
@@ -163,6 +178,7 @@ if File.exist?(f)
puppet = settings[:puppet]
classes = settings[:classes]
docker = settings[:docker]
+ kubernetes = settings [:kubernetes]
ansible = settings[:ansible]
playbook = settings[:playbook]
cachier = settings[:cachier]
@@ -267,6 +283,17 @@ while skip < ARGV.length
end
end
+ elsif ARGV[skip].start_with?(arg='--vagrant-kubernetes=')
+ v = ARGV.delete_at(skip).dup
+ v.slice! arg
+
+ kubernetes = v.to_s # set kubernetes flag
+ if ['true', 'yes'].include?(kubernetes.downcase)
+ kubernetes = true
+ else
+ kubernetes = false
+ end
+
elsif ARGV[skip].start_with?(arg='--vagrant-ansible=')
v = ARGV.delete_at(skip).dup
v.slice! arg
@@ -389,6 +416,7 @@ settings = {
:puppet => puppet,
:classes => classes,
:docker => docker,
+ :kubernetes => kubernetes,
:ansible => ansible,
:playbook => playbook,
:cachier => cachier,
@@ -503,16 +531,22 @@ if load_folder
# 'roles' really, so it's 'modules' until something better comes along
ansible_basedir = File.join(projectdir, folder, 'ansible/', 'modules/')
docker_basedir = File.join(projectdir, folder, 'docker/')
+ kubernetes_basedir = File.join(projectdir, folder, 'kubernetes/', 'applications/')
+ ktemplates_basedir = File.join(projectdir, folder, 'kubernetes/', 'templates/')
else
puppet_basedir = File.join(projectdir, 'puppet/', 'modules/')
ansible_basedir = File.join(projectdir, 'ansible/', 'modules/')
docker_basedir = File.join(projectdir, 'docker/')
+ kubernetes_basedir = File.join(projectdir, 'kubernetes/', 'applications/')
+ ktemplates_basedir = File.join(projectdir, 'kubernetes/', 'templates/')
end
native = [] # native files
native += `cd "#{puppet_basedir}" && git ls-files`.strip.split("\n")
native += `cd "#{ansible_basedir}" && git ls-files`.strip.split("\n")
native += `cd "#{docker_basedir}" && git ls-files`.strip.split("\n")
+native += `cd "#{kubernetes_basedir}" && git ls-files`.strip.split("\n")
+native += `cd "#{ktemplates_basedir}" && git ls-files`.strip.split("\n")
if extern.length > 0
extern.each do |i|
@@ -524,6 +558,10 @@ if extern.length > 0
basedir = ansible_basedir
elsif s == 'docker'
basedir = docker_basedir
+ elsif s == 'kubernetes'
+ basedir = kubernetes_basedir
+ elsif s == 'ktemplates'
+ basedir = ktemplates_basedir
else
$stderr.puts "Can't process extern system: '#{s}'."
next
@@ -574,7 +612,13 @@ if extern.length > 0
end
# clean up any directories or files that shouldn't be present
-(Dir.glob(puppet_basedir+'*')+Dir.glob(ansible_basedir+'*')+Dir.glob(docker_basedir+'*')).uniq.each do |i|
+(
+ Dir.glob(puppet_basedir+'*') +
+ Dir.glob(ansible_basedir+'*') +
+ Dir.glob(docker_basedir+'*') +
+ Dir.glob(kubernetes_basedir+'*') +
+ Dir.glob(ktemplates_basedir+'*') +
+[]).uniq.each do |i|
b = File.basename(i)
if not native.include?(b)
if really_use_rm
@@ -631,17 +675,57 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# set vms ips and do other pre-processing
#
ansible_groups = {}
+ kubernetes_master = false # TODO: in future allow an array of masters ?
+ kubernetes_hosts = [] # list of every participating kubernetes host
+ kubernetes_templates = ''
+ kubernetes_json = []
vms.each_with_index do |x, i|
h = x[:name]
if not x.fetch(:ip, nil) # if vm ip was not set in omv.yaml...
x[:ip] = range[offset+i].to_s # ...generate it, and set vm ip
#vms[i][:ip] = range[offset+i].to_s # equivalent!
end
+ # kubernetes
+ vm_kubernetes = x.fetch(:kubernetes, kubernetes) # get value
+ if kubernetes.is_a?(Hash) and vm_kubernetes.is_a?(Hash)
+ vm_kubernetes = kubernetes.merge(vm_kubernetes) # inherit from global
+ end
+ if vm_kubernetes
+ kubernetes_hosts.push(h)
+ end
+
# build ansible_groups variable now, because needed in big loop
vm_ansible = x.fetch(:ansible, ansible) # get value
ansible_groups[h] = vm_ansible if vm_ansible != []
end
+ if kubernetes_hosts.length > 0
+ kubernetes_master = kubernetes_hosts[0] # default to first host
+ if kubernetes.is_a?(Hash)
+ # a master key can set which kubernetes host is the master one!
+ tmp = kubernetes.fetch('master', nil)
+ if kubernetes_hosts.include?(tmp)
+ kubernetes_master = tmp
+ end
+
+ # kubernetes templates base path inside repo, eg: v0.9.0
+ tmp = kubernetes.fetch('ktemplates', 'default/latest/') # pick!
+ if tmp.is_a?(String)
+ tmp = File.join(ktemplates_basedir, tmp)
+ if File.exists?(tmp)
+ kubernetes_templates = tmp
+ end
+ end
+
+ # json definition file
+ kubernetes_json = kubernetes.fetch('applications', []) # pick!
+ if not kubernetes_json.is_a?(Array)
+ kubernetes_json = []
+ end
+ end
+ end
+ #$stderr.puts "Kubernetes master: #{kubernetes_master}"
+ #$stderr.puts "Kubernetes others: #{kubernetes_hosts.join(',')}"
#
# vms mainloop to define all the machines
@@ -856,6 +940,129 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end
#
+ # kubernetes
+ #
+ # NOTE: one could rewrite this kubernetes portion as a
+ # vagrant plugin, but until this idea is more baked, i
+ # think this is a perfectly fine place to hack this in
+
+ # if we have a master host and if i am an allowed host
+ if kubernetes_master and kubernetes_hosts.include?(h)
+
+ # TODO: add support for non yum-based distros
+ vm.vm.provision 'shell', inline: 'which kubectl || yum install -y kubernetes'
+
+ # templates are in the kubernetes_templates dir
+ ktemplates_host = ((kubernetes_master == h) ? 'master' : 'minion')
+ ktemplates = {}
+ Dir.glob(File.join(kubernetes_templates, "#{ktemplates_host}/")+'*').each do |ktemplate|
+ kbdir = File.basename(ktemplate)
+ ktemplates[kbdir] = open(ktemplate, 'r') {|f| f.read}
+ end
+
+ kubernetes_ostruct = OpenStruct.new({
+ :master => kubernetes_master,
+ :hosts => kubernetes_hosts,
+ :diff => (kubernetes_hosts - [kubernetes_master]),
+ :me => h,
+ })
+
+ ktemplates.each do |kpath, template|
+ if kpath.end_with?('.erb')
+ kpath = File.join('/etc/kubernetes/', kpath[0, kpath.length-'.erb'.length])
+ value = ERB.new(template).result(kubernetes_ostruct.instance_eval {binding})
+ b64 = Base64.encode64(value)
+ else
+ # copy data, don't template it!
+ kpath = File.join('/etc/kubernetes/', kpath)
+ value = template # str
+ b64 = Base64.encode64(value)
+ end
+ # FIXME: add a diff check so this doesn't clobber unnecessarily
+ vm.vm.provision 'shell' do |s|
+ s.inline = "echo '#{b64}' | base64 -d > '#{kpath}'"
+ end
+ end
+
+ # systemctl changes for kubernetes
+ if kubernetes_master == h # master
+ services = ['etcd', 'kube-apiserver', 'kube-controller-manager', 'kube-scheduler']
+ else
+ # a dirty auth hack that scollier wants
+ vm.vm.provision 'shell', inline: 'echo "{}" > /var/lib/kubelet/auth'
+ services = ['kube-proxy', 'kubelet']
+ end
+ services.each do |service|
+ # a restart causes a start if stopped :)
+ vm.vm.provision 'shell', inline: "systemctl restart #{service}"
+ vm.vm.provision 'shell', inline: "systemctl enable #{service}"
+ end
+
+ # scollier and purpleidea decided against
+ # templating the kubernetes json files...
+ # run from an application/ dir instead :)
+ if kubernetes_master == h # master
+ kubernetes_json.each do |jsonfile|
+ roll = false
+ if jsonfile.is_a?(Hash)
+ roll = jsonfile.fetch('roll', false)
+ roll = (['true', 'yes'].include?(roll.downcase) ? true : false)
+ jsonfile = jsonfile.fetch('file', '')
+
+ if jsonfile == ''
+ $stderr.puts "Warning: Kubernetes application list element is an invalid hash!"
+ next
+ end
+ end
+
+ if not File.exists?(File.join(kubernetes_basedir, jsonfile))
+ $stderr.puts "Warning: Kubernetes .json file: '#{jsonfile}' doesn't exist!"
+ next
+ end
+
+ # get path to json file inside vm...
+ # FIXME: this base dir shouldn't be /vagrant when using atomic hosts...
+ jsonfile = File.join('/vagrant/kubernetes/applications/', jsonfile)
+ $script = <<-SCRIPT.unindent
+ # the sed strips off the quotes from the python string output
+ # unescaped sed is: sed 's/\(^"\|"$\)//g'
+ kind="`cat '#{jsonfile}' | python -c 'import json,sys;sys.stdout.write(json.dumps(json.load(sys.stdin)["kind"]))' | sed 's/\\(^"\\|"$\\)//g'`"
+ kid="`cat '#{jsonfile}' | python -c 'import json,sys;sys.stdout.write(json.dumps(json.load(sys.stdin)["id"]))' | sed 's/\\(^"\\|"$\\)//g'`"
+ roll='false'
+ case "$kind" in
+ 'ReplicationController')
+ # remap to command arg name
+ kind='replicationControllers'
+ roll='#{roll}'
+ ;;
+ 'Service')
+ ;;
+ 'Pod')
+ ;;
+ *)
+ echo "Kubernetes kind: '$kind' not yet supported in Oh-My-Vagrant."
+ ;;
+ esac
+ if [ "`kubectl get $kind | tail -n +2 | awk '{print $1}'`" = "$kid" ]; then
+ # exists...
+ # TODO: can we roll like this?
+ if [ "$roll" = 'true' ]; then
+ kubectl rollingupdate "$kid" -f '#{jsonfile}'
+ else
+ # update from existing json file...
+ kubectl update -f '#{jsonfile}'
+ fi
+ else
+ # new
+ kubectl create -f '#{jsonfile}'
+ fi
+ SCRIPT
+ config.vm.provision 'shell', inline: $script
+ end
+ end
+ end
+
+ #
# puppet agent - run on puppet client to set it up
#
if puppet and vm_puppet and h != 'puppet'
@@ -0,0 +1,2 @@
+Kubernetes applications go here. One per directory.
+
@@ -0,0 +1,5 @@
+Kubernetes templates for configuration go here.
+There is a maintained repository available at:
+
+https://github.com/purpleidea/omv-ktemplates
+
Submodule default added at f71298

0 comments on commit 1f26e5b

Please sign in to comment.