Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow configuring kubernetes clusters via DB columns #3307

Merged
merged 2 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/models/concerns/attr_encrypted_support.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'attr_encrypted'

# TODO: make as_json exclude the encryp* columns
module AttrEncryptedSupport
encryption_key_raw = (ENV['ATTR_ENCRYPTED_KEY'] || Rails.application.secrets.secret_key_base)
ENCRYPTION_KEY = encryption_key_raw[0...32]
Expand Down
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_03_26_183413) do
ActiveRecord::Schema.define(version: 2019_04_03_202316) do

create_table "audits" do |t|
t.integer "auditable_id", null: false
Expand Down Expand Up @@ -210,6 +210,14 @@
t.datetime "created_at"
t.datetime "updated_at"
t.string "ip_prefix"
t.string "auth_method", default: "context", null: false
t.string "api_endpoint"
t.text "encrypted_client_cert"
t.string "encrypted_client_cert_iv"
t.text "encrypted_client_key"
t.string "encrypted_client_key_iv"
t.string "encryption_key_sha"
t.boolean "verify_ssl", default: false, null: false
end

create_table "kubernetes_deploy_group_roles", id: :integer do |t|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Kubernetes::ClustersController < ResourceController
PUBLIC = [:index, :show].freeze
HIDDEN = "-- hidden --"
before_action :authorize_admin!, except: PUBLIC
before_action :authorize_super_admin!, except: PUBLIC + [:seed_ecr]
before_action :set_resource, only: [:show, :edit, :update, :destroy, :seed_ecr, :new, :create]
Expand All @@ -27,12 +28,22 @@ def seed_ecr
redirect_to({action: :index}, notice: "Seeded!")
end

def edit
@kubernetes_cluster.client_cert = HIDDEN if @kubernetes_cluster.client_cert?
@kubernetes_cluster.client_key = HIDDEN if @kubernetes_cluster.client_key?
super
end

private

def resource_params
super.permit(
:name, :config_filepath, :config_context, :description, :ip_prefix, deploy_group_ids: []
params = super.permit(
:name, :config_filepath, :config_context, :description, :ip_prefix,
:auth_method, :api_endpoint, :verify_ssl, :client_cert, :client_key,
deploy_group_ids: []
)
params.delete_if { |_, v| v == HIDDEN }
params
end

def new_config_filepath
Expand Down
114 changes: 81 additions & 33 deletions plugins/kubernetes/app/models/kubernetes/cluster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class Cluster < ActiveRecord::Base
self.table_name = 'kubernetes_clusters'
audited

include AttrEncryptedSupport
attr_encrypted :client_cert
attr_encrypted :client_key

IP_PREFIX_PATTERN = /\A(?:[\d]{1,3}\.){0,2}[\d]{1,3}\z/.freeze # also used in js

has_many :cluster_deploy_groups,
Expand All @@ -16,23 +20,51 @@ class Cluster < ActiveRecord::Base
has_many :deploy_groups, through: :cluster_deploy_groups, inverse_of: :kubernetes_cluster

validates :name, presence: true, uniqueness: true
validates :config_filepath, presence: true
validates :config_context, presence: true
validates :ip_prefix, format: IP_PREFIX_PATTERN, allow_blank: true
validate :test_client_connection

before_destroy :ensure_unused

def client(type)
(@client ||= {})[type] ||= build_client(type)
(@client ||= {})[type] ||= begin
case auth_method
when "context"
context = kubeconfig.context(config_context)
endpoint = context.api_endpoint
ssl_options = context.ssl_options
auth_options = context.auth_options
when "database"
endpoint = api_endpoint
ssl_options = {
client_cert: client_cert_object,
client_key: client_key_object,
verify_ssl: verify_ssl
}
auth_options = {}
else raise "Unsupported auth method #{auth_method}"
end

endpoint += '/apis' unless type.match? /^v\d+/ # TODO: remove by fixing via https://github.com/abonas/kubeclient/issues/284

Kubeclient::Client.new(
endpoint,
type,
ssl_options: ssl_options,
auth_options: auth_options,
timeouts: {open: 2, read: 10},
as: :parsed_symbolized
)
end
end

def namespaces
client('v1').get_namespaces.fetch(:items).map { |ns| ns.dig(:metadata, :name) } - %w[kube-system]
client('v1').get_namespaces.fetch(:items).map { |ns| ns.dig(:metadata, :name) } - ["kube-system"]
end

def kubeconfig
@kubeconfig ||= Kubeclient::Config.read(config_filepath)
def config_contexts
(config_filepath? ? kubeconfig.contexts : [])
rescue StandardError
[]
end

def schedulable_nodes
Expand All @@ -52,44 +84,60 @@ def server_version
Gem::Version.new(version)
end

def as_json(**options)
ignored = [
:encrypted_client_cert, :encrypted_client_cert_iv, :client_cert,
:encrypted_client_key, :encrypted_client_key_iv, :client_key,
:encryption_key_sha
]
super(except: ignored, **options)
end

private

def client_key_object
(OpenSSL::PKey::RSA.new(client_key) if client_key?)
end

def client_cert_object
(OpenSSL::X509::Certificate.new(client_cert) if client_cert?)
end

def kubeconfig
@kubeconfig ||= Kubeclient::Config.read(config_filepath)
end

def connection_valid?
client('v1').api_valid?
rescue *SamsonKubernetes.connection_errors
rescue StandardError
false
end

def build_client(type)
context = kubeconfig.context(config_context)
endpoint = context.api_endpoint
endpoint += '/apis' unless type.match? /^v\d+/ # TODO: remove by fixing via https://github.com/abonas/kubeclient/issues/284

Kubeclient::Client.new(
endpoint,
type,
ssl_options: context.ssl_options,
auth_options: context.auth_options,
timeouts: {open: 2, read: 10},
as: :parsed_symbolized
)
end

def test_client_connection
unless File.file?(config_filepath)
errors.add(:config_filepath, "File does not exist")
return
end
case auth_method
when "context"
return errors.add(:config_filepath, "must be set") unless config_filepath?
return errors.add(:config_filepath, "file does not exist") unless File.file?(config_filepath)
return errors.add(:config_context, "must be set") unless config_context?
return errors.add(:config_context, "not found") unless config_contexts.include?(config_context)
when "database"
return errors.add(:api_endpoint, "must be set") unless api_endpoint?
begin
client_cert_object
rescue StandardError
errors.add(:client_cert, "is invalid")
end

unless kubeconfig.contexts.include?(config_context)
errors.add(:config_context, "Context not found")
return
begin
client_key_object
rescue StandardError
errors.add(:client_key, "is invalid")
end
else
errors.add(:auth_method, "pick 'context' or 'database'")
end

unless connection_valid?
errors.add(:config_context, "Could not connect to API Server")
return
end
errors.add(:base, "Could not connect to API Server") unless connection_valid?
end

def ensure_unused
Expand Down
46 changes: 36 additions & 10 deletions plugins/kubernetes/app/views/kubernetes/clusters/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<% config_contexts = (@kubernetes_cluster.config_filepath ? @kubernetes_cluster.kubeconfig.contexts : []) rescue [] %>
<% config_contexts = @kubernetes_cluster.config_contexts %>

<section>
<%= form_for @kubernetes_cluster, html: { class: "form-horizontal" } do |form| %>
Expand All @@ -7,21 +7,35 @@
<fieldset>
<%= form.input :name, required: true %>
<%= form.input :description %>
<%= form.input :config_filepath, label: 'Config File path', pattern: /\A\/.*\z/, help: 'Absolute paths only', required: true %>
<%= form.input :config_context, label: 'Context', required: true do %>
<% if config_contexts.any? %>
<% config_contexts.unshift @kubernetes_cluster.config_context unless config_contexts.include?(@kubernetes_cluster.config_context) %>
<%= form.select :config_context, config_contexts, {}, { class: 'form-control' } %>
<% else %>
<%= form.text_field :config_context, class: "form-control" %>
<% end %>
<% end %>
<%= form.input :ip_prefix,
label: "IP prefix",
pattern: Kubernetes::Cluster::IP_PREFIX_PATTERN,
help: "First 1 to 3 sections of an IPv4 address to replace Service clusterIP, for example 123.231"
%>

<%= form.input :auth_method do %>
<%= form.select :auth_method, ["context", "database"] %>
<% end %>

<div class="auth_via_field" data-method="context">
<%= form.input :config_filepath, label: 'Config File path', pattern: /\A\/.*\z/, help: 'Absolute paths only' %>
<%= form.input :config_context, label: 'Context' do %>
<% if config_contexts.any? %>
<% config_contexts.unshift @kubernetes_cluster.config_context unless config_contexts.include?(@kubernetes_cluster.config_context) %>
<%= form.select :config_context, config_contexts, {}, { class: 'form-control' } %>
<% else %>
<%= form.text_field :config_context, class: "form-control" %>
<% end %>
<% end %>
</div>

<div class="auth_via_field" data-method="database">
<%= form.input :api_endpoint, help: "http://foo.bar" %>
<%= form.input :client_cert, as: :text_area, input_html: { rows: 3 } %>
<%= form.input :client_key, as: :text_area, input_html: { rows: 3 } %>
<%= form.input :verify_ssl, as: :check_box %>
</div>

<%= form.actions delete: true do %>
<% if Samson::Hooks.active_plugin?('aws_ecr') && SamsonAwsEcr::Engine.active? && @kubernetes_cluster.persisted? %>
<%= link_to "Seed ECR", seed_ecr_kubernetes_cluster_path(@kubernetes_cluster), class: "btn btn-default", data: {method: :post} %>
Expand All @@ -41,3 +55,15 @@
});
</script>
<% end %>

<script>
// show fields depending on what auth method user picks
// - timeout so field does not get shrunk to 3px after unfolding
setTimeout(function(){
$("#kubernetes_cluster_auth_method").change(function(){
var current = $(this).val();
$(".auth_via_field[data-method=" + current + "]").show();
$(".auth_via_field[data-method!=" + current + "]").hide();
}).trigger("change");
}, 0)
</script>
68 changes: 54 additions & 14 deletions plugins/kubernetes/app/views/kubernetes/clusters/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Config filepath</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.config_filepath %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Description</label>
<div class="col-lg-4">
Expand All @@ -23,18 +16,65 @@
</div>

<div class="form-group">
<label class="col-lg-2 control-label">URL</label>
<label class="col-lg-2 control-label">IP prefix</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.client('v1').api_endpoint rescue "Error finding api endpoint: #{$!}" %></p>
<p class="form-control-static"><%= @kubernetes_cluster.ip_prefix %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Context</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.config_context %></p>
<% case @kubernetes_cluster.auth_method %>
<% when "context" %>
<div class="form-group">
<label class="col-lg-2 control-label">Context</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.config_context %></p>
</div>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Config filepath</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.config_filepath %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">URL</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.client('v1').api_endpoint rescue "Error finding api endpoint: #{$!}" %></p>
</div>
</div>
<% when "database" %>
<div class="form-group">
<label class="col-lg-2 control-label">URL</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.api_endpoint %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Client Cert</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.client_cert? ? "YES" : "NO" %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Client Key</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.client_key? ? "YES" : "NO" %></p>
</div>
</div>

<div class="form-group">
<label class="col-lg-2 control-label">Verify SSL</label>
<div class="col-lg-4">
<p class="form-control-static"><%= @kubernetes_cluster.verify_ssl? ? "YES" : "NO" %></p>
</div>
</div>
<% else %>
Unsupported auth method
<% end %>

<div class="form-group">
<label class="col-lg-2 control-label">Deploy Groups</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddClusterConfig < ActiveRecord::Migration[5.2]
def change
add_column :kubernetes_clusters, :auth_method, :string, null: false, default: "context"
add_column :kubernetes_clusters, :api_endpoint, :string
add_column :kubernetes_clusters, :encrypted_client_cert, :text
add_column :kubernetes_clusters, :encrypted_client_cert_iv, :string
add_column :kubernetes_clusters, :encrypted_client_key, :text
add_column :kubernetes_clusters, :encrypted_client_key_iv, :string
add_column :kubernetes_clusters, :encryption_key_sha, :string
add_column :kubernetes_clusters, :verify_ssl, :boolean, null: false, default: false
end
end
Loading