summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2016-11-22 19:55:56 +0000
committerNick Thomas <nick@gitlab.com>2016-12-19 19:53:04 +0000
commitc3d972f4e861059312c2708dacb57999416fcc70 (patch)
tree977d60c01a81da4157aa1961b244e9967c9168ee /app
parent5378302763e1a461bab5213aa379d5b9e6dc322c (diff)
downloadgitlab-ce-c3d972f4e861059312c2708dacb57999416fcc70.tar.gz
Add terminals to the Kubernetes deployment service
Diffstat (limited to 'app')
-rw-r--r--app/controllers/projects/environments_controller.rb25
-rw-r--r--app/models/concerns/reactive_caching.rb4
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/project_services/deployment_service.rb18
-rw-r--r--app/models/project_services/kubernetes_service.rb67
-rw-r--r--app/serializers/environment_entity.rb8
6 files changed, 118 insertions, 12 deletions
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 6bd4cb3f2f5..a1b39c6a78a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -9,7 +9,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
@environments = project.environments
-
+
respond_to do |format|
format.html
format.json do
@@ -56,6 +56,29 @@ class Projects::EnvironmentsController < Projects::ApplicationController
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end
+ def terminal
+ # Currently, this acts as a hint to load the terminal details into the cache
+ # if they aren't there already. In the future, users will need these details
+ # to choose between terminals to connect to.
+ @terminals = environment.terminals
+ end
+
+ # GET .../terminal.ws : implemented in gitlab-workhorse
+ def terminal_websocket_authorize
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+
+ # Just return the first terminal for now. If the list is in the process of
+ # being looked up, this may result in a 404 response, so the frontend
+ # should retry
+ terminal = environment.terminals.try(:first)
+ if terminal
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.terminal_websocket(terminal)
+ else
+ render text: 'Not found', status: 404
+ end
+ end
+
private
def environment_params
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 2db67a3b57f..944519a3070 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -55,6 +55,10 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
+ def calculate_reactive_cache
+ raise NotImplementedError
+ end
+
def with_reactive_cache(&blk)
within_reactive_cache_lifetime do
data = Rails.cache.read(full_reactive_cache_key)
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 8ef1c841ea3..5cde94b3509 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base
end
end
+ def has_terminals?
+ project.deployment_service.present? && available? && last_deployment.present?
+ end
+
+ def terminals
+ project.deployment_service.terminals(self) if has_terminals?
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index da6be9dd7b7..ab353a1abe6 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -12,4 +12,22 @@ class DeploymentService < Service
def predefined_variables
[]
end
+
+ # Environments may have a number of terminals. Should return an array of
+ # hashes describing them, e.g.:
+ #
+ # [{
+ # :selectors => {"a" => "b", "foo" => "bar"},
+ # :url => "wss://external.example.com/exec",
+ # :headers => {"Authorization" => "Token xxx"},
+ # :subprotocols => ["foo"],
+ # :ca_pem => "----BEGIN CERTIFICATE...", # optional
+ # :created_at => Time.now.utc
+ # }]
+ #
+ # Selectors should be a set of values that uniquely identify a particular
+ # terminal
+ def terminals(environment)
+ raise NotImplementedError
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f5fbf8b353b..085125ca9dc 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,4 +1,9 @@
class KubernetesService < DeploymentService
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
prop_accessor :namespace
@@ -25,6 +30,8 @@ class KubernetesService < DeploymentService
length: 1..63
end
+ after_save :clear_reactive_cache!
+
def initialize_properties
if properties.nil?
self.properties = {}
@@ -41,7 +48,8 @@ class KubernetesService < DeploymentService
end
def help
- ''
+ 'To enable terminal access to Kubernetes environments, label your ' \
+ 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
def to_param
@@ -75,9 +83,9 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
- kubeclient = build_kubeclient
- kubeclient.discover
+ kubeclient = build_kubeclient!
+ kubeclient.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
@@ -93,20 +101,48 @@ class KubernetesService < DeploymentService
variables
end
- private
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = data.fetch(:pods, nil)
+ filter_pods(pods, app: environment.slug).
+ flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
+ map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ end
+ end
- def build_kubeclient(api_path = '/api', api_version = 'v1')
- return nil unless api_url && namespace && token
+ # Caches all pods in the namespace so other calls don't need to block on
+ # network access.
+ def calculate_reactive_cache
+ return unless active? && project && !project.pending_delete?
- url = URI.parse(api_url)
- url.path = url.path[0..-2] if url.path[-1] == "/"
- url.path += api_path
+ kubeclient = build_kubeclient!
+
+ # Store as hashes, rather than as third-party types
+ pods = begin
+ kubeclient.get_pods(namespace: namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+ []
+ end
+
+ # We may want to cache extra things in the future
+ { pods: pods }
+ end
+
+ private
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && namespace && token
::Kubeclient::Client.new(
- url,
+ join_api_url(api_path),
api_version,
- ssl_options: kubeclient_ssl_options,
auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
)
end
@@ -125,4 +161,13 @@ class KubernetesService < DeploymentService
def kubeclient_auth_options
{ bearer_token: token }
end
+
+ def join_api_url(*parts)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [ prefix, *parts ].join("/")
+
+ url.to_s
+ end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 7e0fc9c071e..e7ef01258ef 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -8,6 +8,7 @@ class EnvironmentEntity < Grape::Entity
expose :environment_type
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
+ expose :has_terminals?, as: :has_terminals
expose :environment_path do |environment|
namespace_project_environment_path(
@@ -23,5 +24,12 @@ class EnvironmentEntity < Grape::Entity
environment)
end
+ expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment|
+ terminal_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
expose :created_at, :updated_at
end