diff options
author | Nick Thomas <nick@gitlab.com> | 2016-11-22 19:55:56 +0000 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2016-12-19 19:53:04 +0000 |
commit | c3d972f4e861059312c2708dacb57999416fcc70 (patch) | |
tree | 977d60c01a81da4157aa1961b244e9967c9168ee /app | |
parent | 5378302763e1a461bab5213aa379d5b9e6dc322c (diff) | |
download | gitlab-ce-c3d972f4e861059312c2708dacb57999416fcc70.tar.gz |
Add terminals to the Kubernetes deployment service
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/projects/environments_controller.rb | 25 | ||||
-rw-r--r-- | app/models/concerns/reactive_caching.rb | 4 | ||||
-rw-r--r-- | app/models/environment.rb | 8 | ||||
-rw-r--r-- | app/models/project_services/deployment_service.rb | 18 | ||||
-rw-r--r-- | app/models/project_services/kubernetes_service.rb | 67 | ||||
-rw-r--r-- | app/serializers/environment_entity.rb | 8 |
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 |