diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb index 4b0d001fca6a94fd52f3289cef50f1b1951b9e12..0b55414d3909afc66f1e37d4846ea794eb541c6e 100644 --- a/app/controllers/projects/serverless/functions_controller.rb +++ b/app/controllers/projects/serverless/functions_controller.rb @@ -8,11 +8,15 @@ class FunctionsController < Projects::ApplicationController def index respond_to do |format| format.json do - functions = finder.execute + functions = finder.execute.select do |function| + can?(@current_user, :read_cluster, function.cluster) + end + + serialized_functions = serialize_function(functions) render json: { knative_installed: finder.knative_installed, - functions: serialize_function(functions) + functions: serialized_functions }.to_json end @@ -23,11 +27,14 @@ def index end def show - @service = serialize_function(finder.service(params[:environment_id], params[:id])) - @prometheus = finder.has_prometheus?(params[:environment_id]) + function = finder.service(params[:environment_id], params[:id]) + return not_found unless function && can?(@current_user, :read_cluster, function.cluster) + @service = serialize_function(function) return not_found if @service.nil? + @prometheus = finder.has_prometheus?(params[:environment_id]) + respond_to do |format| format.json do render json: @service diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb index 4e0b69f47e593f1bb888b0d10be4d3de8c331167..3b4ecbb5387fed75656f81bcb3d1a516a5f05e45 100644 --- a/app/finders/projects/serverless/functions_finder.rb +++ b/app/finders/projects/serverless/functions_finder.rb @@ -93,24 +93,32 @@ def knative_service(environment_scope, name) .services .select { |svc| svc["metadata"]["name"] == name } - add_metadata(finder, services).first unless services.nil? + attributes = add_metadata(finder, services).first + next unless attributes + + Gitlab::Serverless::Service.new(attributes) end end def knative_services services_finders.map do |finder| - services = finder.services + attributes = add_metadata(finder, finder.services) - add_metadata(finder, services) unless services.nil? + attributes&.map do |attributes| + Gitlab::Serverless::Service.new(attributes) + end end end def add_metadata(finder, services) + return if services.nil? + add_pod_count = services.one? services.each do |s| s["environment_scope"] = finder.cluster.environment_scope - s["cluster_id"] = finder.cluster.id + s["environment"] = finder.environment + s["cluster"] = finder.cluster if add_pod_count s["podcount"] = finder diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7855db960c90bd736e6e9edbb6da7c2e686feb3c..7e76d324bdc417e4544f9852710aabd0da785826 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -290,6 +290,12 @@ def clusterable end end + def serverless_domain + strong_memoize(:serverless_domain) do + self.application_knative&.serverless_domain_cluster + end + end + private def unique_management_project_environment_scope diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb index 10360e575bbc409039a2461584e773a9f9471b8c..05beb562e40907d8ee36d0e6e898b505cbd50c0e 100644 --- a/app/serializers/projects/serverless/service_entity.rb +++ b/app/serializers/projects/serverless/service_entity.rb @@ -5,91 +5,31 @@ module Serverless class ServiceEntity < Grape::Entity include RequestAwareEntity - expose :name do |service| - service.dig('metadata', 'name') - end - - expose :namespace do |service| - service.dig('metadata', 'namespace') - end - - expose :environment_scope do |service| - service.dig('environment_scope') - end - - expose :cluster_id do |service| - service.dig('cluster_id') - end + expose :name + expose :namespace + expose :environment_scope + expose :podcount + expose :created_at + expose :image + expose :description + expose :url expose :detail_url do |service| project_serverless_path( request.project, - service.dig('environment_scope'), - service.dig('metadata', 'name')) - end - - expose :podcount do |service| - service.dig('podcount') + service.environment_scope, + service.name) end expose :metrics_url do |service| project_serverless_metrics_path( request.project, - service.dig('environment_scope'), - service.dig('metadata', 'name')) + ".json" - end - - expose :created_at do |service| - service.dig('metadata', 'creationTimestamp') - end - - expose :url do |service| - knative_06_07_url(service) || knative_05_url(service) - end - - expose :description do |service| - knative_07_description(service) || knative_05_06_description(service) + service.environment_scope, + service.name, format: :json) end - expose :image do |service| - service.dig( - 'spec', - 'runLatest', - 'configuration', - 'build', - 'template', - 'name') - end - - private - - def knative_07_description(service) - service.dig( - 'spec', - 'template', - 'metadata', - 'annotations', - 'Description' - ) - end - - def knative_05_url(service) - "http://#{service.dig('status', 'domain')}" - end - - def knative_06_07_url(service) - service.dig('status', 'url') - end - - def knative_05_06_description(service) - service.dig( - 'spec', - 'runLatest', - 'configuration', - 'revisionTemplate', - 'metadata', - 'annotations', - 'Description') + expose :cluster_id do |service| + service.cluster&.id end end end diff --git a/lib/gitlab/serverless/service.rb b/lib/gitlab/serverless/service.rb new file mode 100644 index 0000000000000000000000000000000000000000..643e076c5875032ccc58d481295cee66ed7a2098 --- /dev/null +++ b/lib/gitlab/serverless/service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class Gitlab::Serverless::Service + include Gitlab::Utils::StrongMemoize + + def initialize(attributes) + @attributes = attributes + end + + def name + @attributes.dig('metadata', 'name') + end + + def namespace + @attributes.dig('metadata', 'namespace') + end + + def environment_scope + @attributes.dig('environment_scope') + end + + def environment + @attributes.dig('environment') + end + + def podcount + @attributes.dig('podcount') + end + + def created_at + strong_memoize(:created_at) do + timestamp = @attributes.dig('metadata', 'creationTimestamp') + DateTime.parse(timestamp) if timestamp + end + end + + def image + @attributes.dig( + 'spec', + 'runLatest', + 'configuration', + 'build', + 'template', + 'name') + end + + def description + knative_07_description || knative_05_06_description + end + + def cluster + @attributes.dig('cluster') + end + + def url + proxy_url || knative_06_07_url || knative_05_url + end + + private + + def proxy_url + if cluster&.serverless_domain + Gitlab::Serverless::FunctionURI.new(function: name, cluster: cluster.serverless_domain, environment: environment) + end + end + + def knative_07_description + @attributes.dig( + 'spec', + 'template', + 'metadata', + 'annotations', + 'Description' + ) + end + + def knative_05_06_description + @attributes.dig( + 'spec', + 'runLatest', + 'configuration', + 'revisionTemplate', + 'metadata', + 'annotations', + 'Description') + end + + def knative_05_url + domain = @attributes.dig('status', 'domain') + return unless domain + + "http://#{domain}" + end + + def knative_06_07_url + @attributes.dig('status', 'url') + end +end diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index f0153ac37bff3f7dedff127103db1f9d01524dbc..db7533eb60952fc9b388da3c52912b19e014ae56 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -14,9 +14,11 @@ let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) } let(:knative_services_finder) { environment.knative_services_finder } let(:function_description) { 'A serverless function' } + let(:function_name) { 'some-function-name' } let(:knative_stub_options) do - { namespace: namespace.namespace, name: cluster.project.name, description: function_description } + { namespace: namespace.namespace, name: function_name, description: function_description } end + let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } let(:namespace) do create(:cluster_kubernetes_namespace, @@ -87,25 +89,65 @@ def params(opts = {}) end context 'when functions were found' do - let(:functions) { ["asdf"] } + let(:functions) { [{}, {}] } before do - stub_kubeclient_knative_services(namespace: namespace.namespace) - get :index, params: params({ format: :json }) + stub_kubeclient_knative_services(namespace: namespace.namespace, cluster_id: cluster.id, name: function_name) end it 'returns functions' do + get :index, params: params({ format: :json }) expect(json_response["functions"]).not_to be_empty end - it { expect(response).to have_gitlab_http_status(:ok) } + it 'filters out the functions whose cluster the user does not have permission to read' do + allow(controller).to receive(:can?).and_return(true) + expect(controller).to receive(:can?).with(user, :read_cluster, cluster).and_return(false) + + get :index, params: params({ format: :json }) + + expect(json_response["functions"]).to be_empty + end + + it 'returns a successful response status' do + get :index, params: params({ format: :json }) + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when there is serverless domain for a cluster' do + let!(:serverless_domain_cluster) do + create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id) + end + + it 'returns JSON with function details with serverless domain URL' do + get :index, params: params({ format: :json }) + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response["functions"]).not_to be_empty + + expect(json_response["functions"]).to all( + include( + 'url' => "https://#{function_name}-#{serverless_domain_cluster.uuid[0..1]}a1#{serverless_domain_cluster.uuid[2..-3]}f2#{serverless_domain_cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{serverless_domain_cluster.domain}" + ) + ) + end + end + + context 'when there is no serverless domain for a cluster' do + it 'keeps function URL as it was' do + expect(Gitlab::Serverless::Domain).not_to receive(:new) + + get :index, params: params({ format: :json }) + expect(response).to have_gitlab_http_status(:ok) + end + end end end end describe 'GET #show' do - context 'invalid data' do - it 'has a bad function name' do + context 'with function that does not exist' do + it 'returns 404' do get :show, params: params({ format: :json, environment_id: "*", id: "foo" }) expect(response).to have_gitlab_http_status(:not_found) end @@ -113,15 +155,50 @@ def params(opts = {}) context 'with valid data', :use_clean_rails_memory_store_caching do shared_examples 'GET #show with valid data' do - it 'has a valid function name' do - get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name }) + context 'when there is serverless domain for a cluster' do + let!(:serverless_domain_cluster) do + create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id) + end + + it 'returns JSON with function details with serverless domain URL' do + get :show, params: params({ format: :json, environment_id: "*", id: function_name }) + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response).to include( + 'url' => "https://#{function_name}-#{serverless_domain_cluster.uuid[0..1]}a1#{serverless_domain_cluster.uuid[2..-3]}f2#{serverless_domain_cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{serverless_domain_cluster.domain}" + ) + end + + it 'returns 404 when user does not have permission to read the cluster' do + allow(controller).to receive(:can?).and_return(true) + expect(controller).to receive(:can?).with(user, :read_cluster, cluster).and_return(false) + + get :show, params: params({ format: :json, environment_id: "*", id: function_name }) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when there is no serverless domain for a cluster' do + it 'keeps function URL as it was' do + get :show, params: params({ format: :json, environment_id: "*", id: function_name }) + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response).to include( + 'url' => "http://#{function_name}.#{namespace.namespace}.example.com" + ) + end + end + + it 'return json with function details' do + get :show, params: params({ format: :json, environment_id: "*", id: function_name }) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include( - 'name' => project.name, - 'url' => "http://#{project.name}.#{namespace.namespace}.example.com", + 'name' => function_name, + 'url' => "http://#{function_name}.#{namespace.namespace}.example.com", 'description' => function_description, - 'podcount' => 1 + 'podcount' => 0 ) end end @@ -180,8 +257,8 @@ def params(opts = {}) 'knative_installed' => 'checking', 'functions' => [ a_hash_including( - 'name' => project.name, - 'url' => "http://#{project.name}.#{namespace.namespace}.example.com", + 'name' => function_name, + 'url' => "http://#{function_name}.#{namespace.namespace}.example.com", 'description' => function_description ) ] diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb index 67eda297b918cc1d2729620ac6564f23b6bc98be..4e9f3d371cefd09910e2400b4cd4f4a59d930fe8 100644 --- a/spec/finders/projects/serverless/functions_finder_spec.rb +++ b/spec/finders/projects/serverless/functions_finder_spec.rb @@ -153,8 +153,8 @@ *knative_services_finder.cache_args) result = finder.service(cluster.environment_scope, cluster.project.name) - expect(result).not_to be_empty - expect(result["metadata"]["name"]).to be_eql(cluster.project.name) + expect(result).to be_present + expect(result.name).to be_eql(cluster.project.name) end it 'has metrics', :use_clean_rails_memory_store_caching do diff --git a/spec/lib/gitlab/serverless/service_spec.rb b/spec/lib/gitlab/serverless/service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f618dd02cdb3f8275619e9dfa84f1c66e894273a --- /dev/null +++ b/spec/lib/gitlab/serverless/service_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Serverless::Service do + let(:cluster) { create(:cluster) } + let(:environment) { create(:environment) } + let(:attributes) do + { + 'apiVersion' => 'serving.knative.dev/v1alpha1', + 'kind' => 'Service', + 'metadata' => { + 'creationTimestamp' => '2019-10-22T21:19:13Z', + 'name' => 'kubetest', + 'namespace' => 'project1-1-environment1' + }, + 'spec' => { + 'runLatest' => { + 'configuration' => { + 'build' => { + 'template' => { + 'name' => 'some-image' + } + } + } + } + }, + 'environment_scope' => '*', + 'cluster' => cluster, + 'environment' => environment, + 'podcount' => 0 + } + end + + it 'exposes methods extracting data from the attributes hash' do + service = Gitlab::Serverless::Service.new(attributes) + + expect(service.name).to eq('kubetest') + expect(service.namespace).to eq('project1-1-environment1') + expect(service.environment_scope).to eq('*') + expect(service.podcount).to eq(0) + expect(service.created_at).to eq(DateTime.parse('2019-10-22T21:19:13Z')) + expect(service.image).to eq('some-image') + expect(service.cluster).to eq(cluster) + expect(service.environment).to eq(environment) + end + + it 'returns nil for missing attributes' do + service = Gitlab::Serverless::Service.new({}) + + [:name, :namespace, :environment_scope, :cluster, :podcount, :created_at, :image, :description, :url, :environment].each do |method| + expect(service.send(method)).to be_nil + end + end + + describe '#description' do + it 'extracts the description in knative 7 format if available' do + attributes = { + 'spec' => { + 'template' => { + 'metadata' => { + 'annotations' => { + 'Description' => 'some description' + } + } + } + } + } + service = Gitlab::Serverless::Service.new(attributes) + + expect(service.description).to eq('some description') + end + + it 'extracts the description in knative 5/6 format if 7 is not available' do + attributes = { + 'spec' => { + 'runLatest' => { + 'configuration' => { + 'revisionTemplate' => { + 'metadata' => { + 'annotations' => { + 'Description' => 'some description' + } + } + } + } + } + } + } + service = Gitlab::Serverless::Service.new(attributes) + + expect(service.description).to eq('some description') + end + end + + describe '#url' do + it 'returns proxy URL if cluster has serverless domain' do + # cluster = create(:cluster) + knative = create(:clusters_applications_knative, :installed, cluster: cluster) + create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id) + service = Gitlab::Serverless::Service.new(attributes.merge('cluster' => cluster)) + + expect(Gitlab::Serverless::FunctionURI).to receive(:new).with( + function: service.name, + cluster: service.cluster.serverless_domain, + environment: service.environment + ).and_return('https://proxy.example.com') + + expect(service.url).to eq('https://proxy.example.com') + end + + it 'returns the URL from the knative 6/7 format' do + attributes = { + 'status' => { + 'url' => 'https://example.com' + } + } + service = Gitlab::Serverless::Service.new(attributes) + + expect(service.url).to eq('https://example.com') + end + + it 'returns the URL from the knative 5 format' do + attributes = { + 'status' => { + 'domain' => 'example.com' + } + } + service = Gitlab::Serverless::Service.new(attributes) + + expect(service.url).to eq('http://example.com') + end + end +end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 0d312575e916edbaaadd376cd4e67d5c8d07794d..e2d96db02be49462cee6ddaedf972413c9298f38 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -557,7 +557,7 @@ def kube_deployment(name: "kube-deployment", environment_slug: "production", pro end # noinspection RubyStringKeysInHashInspection - def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 9) { "apiVersion" => "serving.knative.dev/v1alpha1", "kind" => "Service", "metadata" => @@ -612,12 +612,12 @@ def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example. "url" => "http://#{name}.#{namespace}.#{domain}" }, "environment_scope" => environment, - "cluster_id" => 9, + "cluster_id" => cluster_id, "podcount" => 0 } end # noinspection RubyStringKeysInHashInspection - def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5) { "apiVersion" => "serving.knative.dev/v1alpha1", "kind" => "Service", "metadata" => @@ -664,12 +664,12 @@ def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example. "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }], "url" => "http://#{name}.#{namespace}.#{domain}" }, "environment_scope" => environment, - "cluster_id" => 5, + "cluster_id" => cluster_id, "podcount" => 0 } end # noinspection RubyStringKeysInHashInspection - def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5) { "apiVersion" => "serving.knative.dev/v1alpha1", "kind" => "Service", "metadata" => @@ -716,12 +716,12 @@ def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example. "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }], "url" => "http://#{name}.#{namespace}.#{domain}" }, "environment_scope" => environment, - "cluster_id" => 5, + "cluster_id" => cluster_id, "podcount" => 0 } end # noinspection RubyStringKeysInHashInspection - def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 8) { "apiVersion" => "serving.knative.dev/v1alpha1", "kind" => "Service", "metadata" => @@ -771,7 +771,7 @@ def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example. "observedGeneration" => 1, "traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] }, "environment_scope" => environment, - "cluster_id" => 8, + "cluster_id" => cluster_id, "podcount" => 0 } end