Skip to content

Commit

Permalink
move all template logic into TemplateFiller
Browse files Browse the repository at this point in the history
moves logic into a single place and makes it easier to test
  • Loading branch information
grosser committed Sep 15, 2017
1 parent 13b3ecf commit c5ba126
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 183 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Style/StringLiteralsInInterpolation:
Style/NumericLiterals:
Enabled: false

Style/NumericPredicate:
Enabled: false

Style/FirstParameterIndentation:
Enabled: false

Expand Down
69 changes: 4 additions & 65 deletions plugins/kubernetes/app/models/kubernetes/release_doc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def revert
# this create a bit of duplicated work, but fails the deploy fast
def verify_template
primary_config = raw_template.detect { |e| Kubernetes::RoleConfigFile::PRIMARY_KINDS.include?(e.fetch(:kind)) }
template = Kubernetes::TemplateFiller.new(self, primary_config)
template = Kubernetes::TemplateFiller.new(self, primary_config, index: 0)
template.set_secrets
template.verify_env
end
Expand Down Expand Up @@ -64,74 +64,13 @@ def resource_template=(value)

# dynamically fill out the templates and store the result
def store_resource_template
counter = Hash.new(-1)
self.resource_template = raw_template.map do |resource|
update_namespace resource

case resource[:kind]
when 'Service'
resource[:metadata][:name] = generate_service_name(resource[:metadata][:name])

prefix_service_cluster_ip(resource)

# For now, create a NodePort for each service, so we can expose any
# apps running in the Kubernetes cluster to traffic outside the cluster.
resource[:spec][:type] = 'NodePort'
resource
when *Kubernetes::RoleConfigFile::PRIMARY_KINDS
make_stateful_set_match_service(resource)
TemplateFiller.new(self, resource).to_hash
else
resource
end
index = (counter[resource.fetch(:kind)] += 1)
TemplateFiller.new(self, resource, index: index).to_hash
end
end

# If the user renames the service the StatefulSet will not match it, so we fix.
# Will not work with multiple services ... but that usecase hopefully does not exist.
def make_stateful_set_match_service(resource)
return unless resource[:kind] == "StatefulSet"
return unless resource[:spec][:serviceName]
return unless service_name = kubernetes_role.service_name.presence
resource[:spec][:serviceName] = service_name
end

def generate_service_name(config_name)
return config_name unless name = kubernetes_role.service_name.presence
if name.include?(Kubernetes::Role::GENERATED)
raise(
Samson::Hooks::UserError,
"Service name for role #{kubernetes_role.name} was generated and needs to be changed before deploying."
)
end

# users can only enter a single service-name so for each additional service we make up a name
# unless the given name already fits the pattern ... slight chance that it might end up being not unique
return config_name if config_name.start_with?(name)

@service_names_generated ||= 0
@service_names_generated += 1
name += "-#{@service_names_generated}" if @service_names_generated >= 2
name
end

# no ipv6 support
def prefix_service_cluster_ip(resource)
return unless ip = resource[:spec][:clusterIP]
return if ip == "None"
return unless prefix = deploy_group.kubernetes_cluster.ip_prefix.presence
ip = ip.split('.')
prefix = prefix.split('.')
ip[0...prefix.size] = prefix
resource[:spec][:clusterIP] = ip.join('.')
end

def update_namespace(resource)
system_namespaces = ["default", "kube-system"]
return if system_namespaces.include?(resource[:metadata][:namespace]) &&
(resource[:metadata][:labels] || {})[:'kubernetes.io/cluster-service'] == 'true'
resource[:metadata][:namespace] = deploy_group.kubernetes_namespace
end

def validate_config_file
return unless kubernetes_role
raw_template # trigger RoleConfigFile validations
Expand Down
97 changes: 82 additions & 15 deletions plugins/kubernetes/app/models/kubernetes/template_filler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,43 @@ class TemplateFiller
CUSTOM_UNIQUE_LABEL_KEY = 'rc_unique_identifier'
SECRET_PULLER_IMAGE = ENV['SECRET_PULLER_IMAGE'].presence

def initialize(release_doc, template)
def initialize(release_doc, template, index:)
@doc = release_doc
@template = template
@index = index
end

def to_hash
@to_hash ||= begin
if template[:kind] != 'Pod'
set_rc_unique_label_key
set_history_limit
end
kind = template[:kind]

set_namespace

case kind
when *Kubernetes::RoleConfigFile::SERVICE_KINDS
set_service_name
set_service_node_port
prefix_service_cluster_ip
when *Kubernetes::RoleConfigFile::PRIMARY_KINDS
if kind != 'Pod'
set_rc_unique_label_key
set_history_limit
end

set_replica_target unless ['DaemonSet', 'Pod'].include?(template[:kind])
set_replica_target unless ['DaemonSet', 'Pod'].include?(kind)

set_name
set_deployer
set_spec_template_metadata
set_docker_image
set_resource_usage
set_env
set_secrets
set_image_pull_secrets
set_vault_env
make_stateful_set_match_service if kind == 'StatefulSet'

set_name
set_deployer
set_spec_template_metadata
set_docker_image
set_resource_usage
set_env
set_secrets
set_image_pull_secrets
set_vault_env
end

hash = template
Rails.logger.info "Created Kubernetes hash: #{hash.to_json}"
Expand All @@ -52,6 +66,59 @@ def set_secrets

private

def set_service_name
template[:metadata][:name] = generate_service_name(template[:metadata][:name])
end

# For now, create a NodePort for each service, so we can expose any
# apps running in the Kubernetes cluster to traffic outside the cluster.
def set_service_node_port
template[:spec][:type] = 'NodePort'
end

def generate_service_name(config_name)
return config_name unless name = @doc.kubernetes_role.service_name.presence
if name.include?(Kubernetes::Role::GENERATED)
raise(
Samson::Hooks::UserError,
"Service name for role #{@doc.kubernetes_role.name} was generated and needs to be changed before deploying."
)
end

# users can only enter a single service-name so for each additional service we make up a name
# unless the given name already fits the pattern ... slight chance that it might end up being not unique
return config_name if config_name.start_with?(name) && config_name.size > name.size

name += "-#{@index + 1}" if @index > 0
name
end

# no ipv6 support
def prefix_service_cluster_ip
return unless ip = template[:spec][:clusterIP]
return if ip == "None"
return unless prefix = @doc.deploy_group.kubernetes_cluster.ip_prefix.presence
ip = ip.split('.')
prefix = prefix.split('.')
ip[0...prefix.size] = prefix
template[:spec][:clusterIP] = ip.join('.')
end

def set_namespace
system_namespaces = ["default", "kube-system"]
return if system_namespaces.include?(template.dig(:metadata, :namespace)) &&
template.dig(:metadata, :labels, :'kubernetes.io/cluster-service') == 'true'
template[:metadata][:namespace] = @doc.deploy_group.kubernetes_namespace
end

# If the user renames the service the StatefulSet will not match it, so we fix.
# Will not work with multiple services ... but that usecase hopefully does not exist.
def make_stateful_set_match_service
return unless template[:spec][:serviceName]
return unless service_name = @doc.kubernetes_role.service_name.presence
template[:spec][:serviceName] = service_name
end

# make sure we clean up old replicasets
# we only ever do rollback to latest release ... and the default is infinite
# see discussion in https://github.com/kubernetes/kubernetes/issues/23597
Expand Down
96 changes: 0 additions & 96 deletions plugins/kubernetes/test/models/kubernetes/release_doc_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,115 +67,19 @@ def create!
create!.resource_template[0][:kind].must_equal 'Deployment'
end

it "does not store blank service name" do
doc.kubernetes_role.update_column(:service_name, '') # user left field empty
create!.resource_template[1][:metadata][:name].must_equal 'some-project'
end

it "fails to create with missing config file" do
Kubernetes::ReleaseDoc.any_instance.unstub(:raw_template)
GitRepository.any_instance.expects(:file_content).returns(nil) # File not found
assert_raises(ActiveRecord::RecordInvalid) { create! }
end

it "fails when trying to create for a generated service" do
doc.kubernetes_role.update_column(:service_name, "app-server#{Kubernetes::Role::GENERATED}1211212")
e = assert_raises Samson::Hooks::UserError do
create!
end
e.message.must_equal "Service name for role app-server was generated and needs to be changed before deploying."
end

it "adds counter to service names when using multiple services" do
doc.kubernetes_role.update_column(:service_name, 'foo')
template = Kubernetes::ReleaseDoc.new.send(:raw_template) # stubs makes all docs share the same template
template.push template[1].deep_dup # 2 Services
create!.resource_template[1][:metadata][:name].must_equal 'foo'
create!.resource_template[2][:metadata][:name].must_equal 'foo-2'
end

it "keeps prefixed service names when using multiple services" do
doc.kubernetes_role.update_column(:service_name, 'foo')
template = Kubernetes::ReleaseDoc.new.send(:raw_template) # stubs makes all docs share the same template
template.push template[1].deep_dup # 2 Services
template[2][:metadata][:name] = 'foo-other'
create!.resource_template[1][:metadata][:name].must_equal 'foo'
create!.resource_template[2][:metadata][:name].must_equal 'foo-other'
end

it "keeps default service namespace because it is a unique system namespace" do
doc.send(:raw_template)[0][:metadata][:namespace] = "default"
doc.send(:raw_template)[0][:metadata][:labels] = {"kubernetes.io/cluster-service": 'true'}
create!.resource_template[0][:metadata][:namespace].must_equal 'default'
end

it "keeps the kube-system namespace because it's valid for cluster services " do
doc.send(:raw_template)[0][:metadata][:namespace] = "kube-system"
doc.send(:raw_template)[0][:metadata][:labels] = {"kubernetes.io/cluster-service": 'true'}
create!.resource_template[0][:metadata][:namespace].must_equal 'kube-system'
end

it "configures ConfigMap" do
doc.send(:raw_template)[1][:kind] = "ConfigMap"
create!.resource_template[1][:metadata][:namespace].must_equal 'pod1'
end

describe 'service clusterIP' do
let(:result) { create!.resource_template[1][:spec][:clusterIP] }

before do
doc.deploy_group.kubernetes_cluster.update_column(:ip_prefix, '123.34')
doc.send(:raw_template)[1][:spec][:clusterIP] = "1.2.3.4"
end

it "replaces ip prefix" do
result.must_equal '123.34.3.4'
end

it "replaces with trailing ." do
doc.deploy_group.kubernetes_cluster.update_column(:ip_prefix, '123.34.')
result.must_equal '123.34.3.4'
end

it "does nothing when service has no clusterIP" do
doc.send(:raw_template)[1][:spec].delete(:clusterIP)
result.must_be_nil
end

it "does nothing when ip prefix is blank" do
doc.deploy_group.kubernetes_cluster.update_column(:ip_prefix, '')
result.must_equal '1.2.3.4'
end

it "leaves None alone" do
doc.send(:raw_template)[1][:spec][:clusterIP] = "None"
result.must_equal 'None'
end
end

describe "statefulset serviceName" do
let(:result) { create!.resource_template[0][:spec][:serviceName] }

before do
doc.kubernetes_role.update_column(:service_name, 'changed')
doc.send(:raw_template)[0][:kind] = "StatefulSet"
doc.send(:raw_template)[0][:spec][:serviceName] = "unchanged"
end

it "changes the set serviceName" do
result.must_equal 'changed'
end

it "does nothing when service_name was not set" do
doc.kubernetes_role.update_column(:service_name, '')
result.must_equal 'unchanged'
end

it "does nothing when serviceName was not used" do
doc.send(:raw_template)[0][:spec].delete :serviceName
result.must_be_nil
end
end
end

describe "#deploy" do
Expand Down
Loading

0 comments on commit c5ba126

Please sign in to comment.