Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDylan Griffith <dyl.griffith@gmail.com>2018-12-06 21:08:49 +0300
committerMike Greiling <mike@pixelcog.com>2018-12-06 21:08:49 +0300
commit2c80a1c0de07877e6e2bf7ab20de2d4f43a0d97c (patch)
treeafa7f5f54e2491e0c08168b6f4ce47511da3012b
parente80f89337b4be31c5531448861cedb556d02c01e (diff)
Introduce Knative Serverless Tab
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js5
-rw-r--r--app/assets/javascripts/serverless/components/empty_state.vue40
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue40
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue123
-rw-r--r--app/assets/javascripts/serverless/event_hub.js3
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js106
-rw-r--r--app/assets/javascripts/serverless/services/get_functions_service.js11
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_store.js24
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb37
-rw-r--r--app/finders/projects/serverless/functions_finder.rb31
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/clusters/applications/knative.rb36
-rw-r--r--app/models/clusters/cluster.rb10
-rw-r--r--app/serializers/projects/serverless/service_entity.rb33
-rw-r--r--app/serializers/projects/serverless/service_serializer.rb9
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/projects/serverless/functions/index.html.haml15
-rw-r--r--changelogs/unreleased/triggermesh-phase2-serverless-list.yml5
-rw-r--r--config/routes/project.rb4
-rw-r--r--doc/user/project/clusters/serverless/img/install-knative.pngbin102861 -> 31222 bytes
-rw-r--r--doc/user/project/clusters/serverless/img/serverless-page.pngbin0 -> 31743 bytes
-rw-r--r--locale/gitlab.pot39
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb72
-rw-r--r--spec/features/projects/serverless/functions_spec.rb49
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb60
-rw-r--r--spec/models/clusters/applications/knative_spec.rb42
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb66
27 files changed, 866 insertions, 2 deletions
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
new file mode 100644
index 00000000000..7b08620773c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/serverless/index.js
@@ -0,0 +1,5 @@
+import ServerlessBundle from '~/serverless/serverless_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ServerlessBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/serverless/components/empty_state.vue b/app/assets/javascripts/serverless/components/empty_state.vue
new file mode 100644
index 00000000000..2683805f2f7
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/empty_state.vue
@@ -0,0 +1,40 @@
+<script>
+export default {
+ props: {
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row empty-state js-empty-state">
+ <div class="col-12">
+ <div class="text-content">
+ <h4 class="state-title text-center">
+ {{ s__('Serverless|Getting started with serverless') }}
+ </h4>
+ <p class="state-description">
+ {{
+ s__(`Serverless| In order to start using functions as a service,
+ you must first install Knative on your Kubernetes cluster.`)
+ }}
+
+ <a :href="helpPath"> {{ __('More information') }} </a>
+ </p>
+
+ <div class="text-center">
+ <a :href="clustersPath" class="btn btn-success">
+ {{ s__('Serverless|Install Knative') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
new file mode 100644
index 00000000000..31f5427c771
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -0,0 +1,40 @@
+<script>
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Timeago,
+ },
+ props: {
+ func: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ name() {
+ return this.func.name;
+ },
+ url() {
+ return this.func.url;
+ },
+ image() {
+ return this.func.image;
+ },
+ timestamp() {
+ return this.func.created_at;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row">
+ <div class="table-section section-20">{{ name }}</div>
+ <div class="table-section section-50">
+ <a :href="url">{{ url }}</a>
+ </div>
+ <div class="table-section section-20">{{ image }}</div>
+ <div class="table-section section-10"><timeago :time="timestamp" /></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
new file mode 100644
index 00000000000..7874a7b6b6a
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -0,0 +1,123 @@
+<script>
+import { GlSkeletonLoading } from '@gitlab/ui';
+import FunctionRow from './function_row.vue';
+import EmptyState from './empty_state.vue';
+
+export default {
+ components: {
+ FunctionRow,
+ EmptyState,
+ GlSkeletonLoading,
+ },
+ props: {
+ functions: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ installed: {
+ type: Boolean,
+ required: true,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ loadingData: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ hasFunctionData: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <section id="serverless-functions">
+ <div v-if="installed">
+ <div v-if="hasFunctionData">
+ <div class="ci-table js-services-list function-element">
+ <div class="gl-responsive-table-row table-row-header" role="row">
+ <div class="table-section section-20" role="rowheader">
+ {{ s__('Serverless|Function') }}
+ </div>
+ <div class="table-section section-50" role="rowheader">
+ {{ s__('Serverless|Domain') }}
+ </div>
+ <div class="table-section section-20" role="rowheader">
+ {{ s__('Serverless|Runtime') }}
+ </div>
+ <div class="table-section section-10" role="rowheader">
+ {{ s__('Serverless|Last Update') }}
+ </div>
+ </div>
+ <template v-if="loadingData">
+ <div v-for="j in 3" :key="j" class="gl-responsive-table-row">
+ <gl-skeleton-loading />
+ </div>
+ </template>
+ <template v-else>
+ <function-row v-for="f in functions" :key="f.name" :func="f" />
+ </template>
+ </div>
+ </div>
+ <div v-else class="empty-state js-empty-state">
+ <div class="text-content">
+ <h4 class="state-title text-center">{{ s__('Serverless|No functions available') }}</h4>
+ <p class="state-description">
+ {{
+ s__(`Serverless|There is currently no function data available from Knative.
+ This could be for a variety of reasons including:`)
+ }}
+ </p>
+ <ul>
+ <li>Your repository does not have a corresponding <code>serverless.yml</code> file.</li>
+ <li>Your <code>gitlab-ci.yml</code> file is not properly configured.</li>
+ <li>
+ The functions listed in the <code>serverless.yml</code> file don't match the namespace
+ of your cluster.
+ </li>
+ <li>The deploy job has not finished.</li>
+ </ul>
+
+ <p>
+ {{
+ s__(`Serverless|If you believe none of these apply, please check
+ back later as the function data may be in the process of becoming
+ available.`)
+ }}
+ </p>
+ <div class="text-center">
+ <a :href="helpPath" class="btn btn-success">
+ {{ s__('Serverless|Learn more about Serverless') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
+ </section>
+</template>
+
+<style>
+.top-area {
+ border-bottom: 0;
+}
+
+.function-element {
+ border-bottom: 1px solid #e5e5e5;
+ border-bottom-color: rgb(229, 229, 229);
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+}
+</style>
diff --git a/app/assets/javascripts/serverless/event_hub.js b/app/assets/javascripts/serverless/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/serverless/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
new file mode 100644
index 00000000000..3e3b81ba247
--- /dev/null
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -0,0 +1,106 @@
+import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import { s__ } from '../locale';
+import Flash from '../flash';
+import Poll from '../lib/utils/poll';
+import ServerlessStore from './stores/serverless_store';
+import GetFunctionsService from './services/get_functions_service';
+import Functions from './components/functions.vue';
+
+export default class Serverless {
+ constructor() {
+ const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ '.js-serverless-functions-page',
+ ).dataset;
+
+ this.service = new GetFunctionsService(statusPath);
+ this.knativeInstalled = installed !== undefined;
+ this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
+ this.initServerless();
+ this.functionLoadCount = 0;
+
+ if (statusPath && this.knativeInstalled) {
+ this.initPolling();
+ }
+ }
+
+ initServerless() {
+ const { store } = this;
+ const el = document.querySelector('#js-serverless-functions');
+
+ this.functions = new Vue({
+ el,
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement(Functions, {
+ props: {
+ functions: this.state.functions,
+ installed: this.state.installed,
+ clustersPath: this.state.clustersPath,
+ helpPath: this.state.helpPath,
+ loadingData: this.state.loadingData,
+ hasFunctionData: this.state.hasFunctionData,
+ },
+ });
+ },
+ });
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => this.handleError(),
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service
+ .fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => this.handleError());
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && !this.destroyed) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ handleSuccess(data) {
+ if (data.status === 200) {
+ this.store.updateFunctionsFromServer(data.data);
+ this.store.updateLoadingState(false);
+ } else if (data.status === 204) {
+ /* Time out after 3 attempts to retrieve data */
+ this.functionLoadCount += 1;
+ if (this.functionLoadCount === 3) {
+ this.poll.stop();
+ this.store.toggleNoFunctionData();
+ }
+ }
+ }
+
+ static handleError() {
+ Flash(s__('Serverless|An error occurred while retrieving serverless components'));
+ }
+
+ destroy() {
+ this.destroyed = true;
+
+ if (this.poll) {
+ this.poll.stop();
+ }
+
+ this.functions.$destroy();
+ }
+}
diff --git a/app/assets/javascripts/serverless/services/get_functions_service.js b/app/assets/javascripts/serverless/services/get_functions_service.js
new file mode 100644
index 00000000000..303b42dc66c
--- /dev/null
+++ b/app/assets/javascripts/serverless/services/get_functions_service.js
@@ -0,0 +1,11 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class GetFunctionsService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchData() {
+ return axios.get(this.endpoint);
+ }
+}
diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js
new file mode 100644
index 00000000000..774c15b5b12
--- /dev/null
+++ b/app/assets/javascripts/serverless/stores/serverless_store.js
@@ -0,0 +1,24 @@
+export default class ServerlessStore {
+ constructor(knativeInstalled = false, clustersPath, helpPath) {
+ this.state = {
+ functions: [],
+ hasFunctionData: true,
+ loadingData: true,
+ installed: knativeInstalled,
+ clustersPath,
+ helpPath,
+ };
+ }
+
+ updateFunctionsFromServer(functions = []) {
+ this.state.functions = functions;
+ }
+
+ updateLoadingState(loadingData) {
+ this.state.loadingData = loadingData;
+ }
+
+ toggleNoFunctionData() {
+ this.state.hasFunctionData = false;
+ }
+}
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
new file mode 100644
index 00000000000..0af2b7ef343
--- /dev/null
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class FunctionsController < Projects::ApplicationController
+ include ProjectUnauthorized
+
+ before_action :authorize_read_cluster!
+
+ INDEX_PRIMING_INTERVAL = 10_000
+ INDEX_POLLING_INTERVAL = 30_000
+
+ def index
+ finder = Projects::Serverless::FunctionsFinder.new(project.clusters)
+
+ respond_to do |format|
+ format.json do
+ functions = finder.execute
+
+ if functions.any?
+ Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
+ render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions)
+ else
+ Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
+ head :no_content
+ end
+ end
+
+ format.html do
+ @installed = finder.installed?
+ render
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
new file mode 100644
index 00000000000..2b5d67e79d7
--- /dev/null
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class FunctionsFinder
+ def initialize(clusters)
+ @clusters = clusters
+ end
+
+ def execute
+ knative_services.flatten.compact
+ end
+
+ def installed?
+ clusters_with_knative_installed.exists?
+ end
+
+ private
+
+ def knative_services
+ clusters_with_knative_installed.preload_knative.map do |cluster|
+ cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
+ end
+ end
+
+ def clusters_with_knative_installed
+ @clusters.with_knative_installed
+ end
+ end
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0a7f930110a..f6a45ef34e3 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -307,6 +307,7 @@ module ProjectsHelper
settings: :admin_project,
builds: :read_build,
clusters: :read_cluster,
+ serverless: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -545,6 +546,7 @@ module ProjectsHelper
%w[
environments
clusters
+ functions
user
gcp
]
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index c0aaa8dce20..168a24da738 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,6 +15,9 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
state_machine :status do
before_transition any => [:installed] do |application|
@@ -29,6 +32,8 @@ module Clusters
validates :hostname, presence: true, hostname: true
+ scope :for_cluster, -> (cluster) { where(cluster: cluster) }
+
def chart
'knative/knative'
end
@@ -55,12 +60,39 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
+ def client
+ cluster.kubeclient.knative_client
+ end
+
+ def services
+ with_reactive_cache do |data|
+ data[:services]
+ end
+ end
+
+ def calculate_reactive_cache
+ { services: read_services }
+ end
+
def ingress_service
cluster.kubeclient.get_service('knative-ingressgateway', 'istio-system')
end
- def client
- cluster.platform_kubernetes.kubeclient.knative_client
+ def services_for(ns: namespace)
+ return unless services
+ return [] unless ns
+
+ services.select do |service|
+ service.dig('metadata', 'namespace') == ns
+ end
+ end
+
+ private
+
+ def read_services
+ client.get_services.as_json
+ rescue Kubeclient::ResourceNotFoundError
+ []
end
end
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index c9bd1728dbd..7fe43cd2de0 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -93,6 +93,16 @@ module Clusters
where('NOT EXISTS (?)', subquery)
end
+ scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.installed) }
+
+ scope :preload_knative, -> {
+ preload(
+ :kubernetes_namespace,
+ :platform_kubernetes,
+ :application_knative
+ )
+ }
+
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
new file mode 100644
index 00000000000..4f1f62d145b
--- /dev/null
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Projects
+ 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 :created_at do |service|
+ service.dig('metadata', 'creationTimestamp')
+ end
+
+ expose :url do |service|
+ "http://#{service.dig('status', 'domain')}"
+ end
+
+ expose :description do |service|
+ service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description')
+ end
+
+ expose :image do |service|
+ service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name')
+ end
+ end
+ end
+end
diff --git a/app/serializers/projects/serverless/service_serializer.rb b/app/serializers/projects/serverless/service_serializer.rb
new file mode 100644
index 00000000000..adfd48a8c7d
--- /dev/null
+++ b/app/serializers/projects/serverless/service_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class ServiceSerializer < BaseSerializer
+ entity Projects::Serverless::ServiceEntity
+ end
+ end
+end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index ab15889a465..b89541a3c9f 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -222,6 +222,12 @@
%span
= _('Environments')
+ - if project_nav_tab? :serverless
+ = nav_link(controller: :functions) do
+ = link_to project_serverless_functions_path(@project), title: _('Serverless') do
+ %span
+ = _('Serverless')
+
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
new file mode 100644
index 00000000000..f650fa0f38f
--- /dev/null
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -0,0 +1,15 @@
+- @no_container = true
+- @content_class = "limit-container-width" unless fluid_layout
+- breadcrumb_title 'Serverless'
+- page_title 'Serverless'
+- status_path = project_serverless_functions_path(@project, format: :json)
+- clusters_path = project_clusters_path(@project)
+
+.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
+
+%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
+ .js-serverless-functions-notice
+ .flash-container
+
+ .top-area.adjust
+ .serverless-functions-table#js-serverless-functions
diff --git a/changelogs/unreleased/triggermesh-phase2-serverless-list.yml b/changelogs/unreleased/triggermesh-phase2-serverless-list.yml
new file mode 100644
index 00000000000..22e1a35dd90
--- /dev/null
+++ b/changelogs/unreleased/triggermesh-phase2-serverless-list.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Knative and Serverless Components
+merge_request: 23174
+author: Chris Baumbauer
+type: added
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 3f1ad90dfca..152d933c318 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -245,6 +245,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ namespace :serverless do
+ resources :functions, only: [:index]
+ end
+
scope '-' do
get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive'
diff --git a/doc/user/project/clusters/serverless/img/install-knative.png b/doc/user/project/clusters/serverless/img/install-knative.png
index dd576a9df35..a9fcc127240 100644
--- a/doc/user/project/clusters/serverless/img/install-knative.png
+++ b/doc/user/project/clusters/serverless/img/install-knative.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/img/serverless-page.png b/doc/user/project/clusters/serverless/img/serverless-page.png
new file mode 100644
index 00000000000..473ee801f10
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/serverless-page.png
Binary files differ
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 23ee90ff0dd..76e1c9349d8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5815,6 +5815,45 @@ msgstr ""
msgid "Server version"
msgstr ""
+msgid "Serverless"
+msgstr ""
+
+msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
+msgstr ""
+
+msgid "Serverless|An error occurred while retrieving serverless components"
+msgstr ""
+
+msgid "Serverless|Domain"
+msgstr ""
+
+msgid "Serverless|Function"
+msgstr ""
+
+msgid "Serverless|Getting started with serverless"
+msgstr ""
+
+msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
+msgstr ""
+
+msgid "Serverless|Install Knative"
+msgstr ""
+
+msgid "Serverless|Last Update"
+msgstr ""
+
+msgid "Serverless|Learn more about Serverless"
+msgstr ""
+
+msgid "Serverless|No functions available"
+msgstr ""
+
+msgid "Serverless|Runtime"
+msgstr ""
+
+msgid "Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
new file mode 100644
index 00000000000..284b582b1f5
--- /dev/null
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Serverless::FunctionsController do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:user) { create(:user) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.project}
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ def params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+ end
+
+ describe 'GET #index' do
+ context 'empty cache' do
+ it 'has no data' do
+ get :index, params({ format: :json })
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it 'renders an html page' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+
+ describe 'GET #index with data', :use_clean_rails_memory_store_caching do
+ before do
+ stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
+ end
+
+ it 'has data' do
+ get :index, params({ format: :json })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to contain_exactly(
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ )
+ end
+
+ it 'has data in html' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+end
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
new file mode 100644
index 00000000000..766c63725b3
--- /dev/null
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'Functions', :js do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ gitlab_sign_in(user)
+ end
+
+ context 'when user does not have a cluster and visits the serverless page' do
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty state' do
+ expect(page).to have_link('Install Knative')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+
+ context 'when the user does have a cluster and visits the serverless page' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty state' do
+ expect(page).to have_link('Install Knative')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+
+ context 'when the user has a cluster and knative installed and visits the serverless page' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let(:project) { knative.cluster.project }
+
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty listing of serverless functions' do
+ expect(page).to have_selector('.gl-responsive-table-row')
+ end
+ end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
new file mode 100644
index 00000000000..60d02b12054
--- /dev/null
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Serverless::FunctionsFinder do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:user) { create(:user) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.project}
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'retrieve data from knative' do
+ it 'does not have knative installed' do
+ expect(described_class.new(project.clusters).execute).to be_empty
+ end
+
+ context 'has knative installed' do
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+
+ it 'there are no functions' do
+ expect(described_class.new(project.clusters).execute).to be_empty
+ end
+
+ it 'there are functions', :use_clean_rails_memory_store_caching do
+ stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
+
+ expect(described_class.new(project.clusters).execute).not_to be_empty
+ end
+ end
+ end
+
+ describe 'verify if knative is installed' do
+ context 'knative is not installed' do
+ it 'does not have knative installed' do
+ expect(described_class.new(project.clusters).installed?).to be false
+ end
+ end
+
+ context 'knative is installed' do
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+
+ it 'does have knative installed' do
+ expect(described_class.new(project.clusters).installed?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d43d88c2924..a1579b90436 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -1,6 +1,9 @@
require 'rails_helper'
describe Clusters::Applications::Knative do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative
@@ -121,4 +124,43 @@ describe Clusters::Applications::Knative do
describe 'validations' do
it { is_expected.to validate_presence_of(:hostname) }
end
+
+ describe '#services' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ subject { knative.services }
+
+ before do
+ stub_kubeclient_discover(service.api_url)
+ stub_kubeclient_knative_services
+ end
+
+ it 'should have an unintialized cache' do
+ is_expected.to be_nil
+ end
+
+ context 'when using synchronous reactive cache' do
+ before do
+ stub_reactive_cache(knative, services: kube_response(kube_knative_services_body))
+ synchronous_reactive_cache(knative)
+ end
+
+ it 'should have cached services' do
+ is_expected.not_to be_nil
+ end
+
+ it 'should match our namespace' do
+ expect(knative.services_for(ns: namespace)).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index bef951e1517..39bd305d88a 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -34,6 +34,17 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
+ def stub_kubeclient_knative_services(**options)
+ options[:name] ||= "kubetest"
+ options[:namespace] ||= "default"
+ options[:domain] ||= "example.com"
+
+ stub_kubeclient_discover(service.api_url)
+ knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
+
+ WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+ end
+
def stub_kubeclient_get_secret(api_url, **options)
options[:metadata_name] ||= "default-token-1"
options[:namespace] ||= "default"
@@ -181,6 +192,13 @@ module KubernetesHelpers
}
end
+ def kube_knative_services_body(**options)
+ {
+ "kind" => "List",
+ "items" => [kube_service(options)]
+ }
+ end
+
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
@@ -224,6 +242,54 @@ module KubernetesHelpers
}
end
+ def kube_service(name: "kubetest", namespace: "default", domain: "example.com")
+ {
+ "metadata" => {
+ "creationTimestamp" => "2018-11-21T06:16:33Z",
+ "name" => name,
+ "namespace" => namespace,
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
+ },
+ "spec" => {
+ "generation" => 2
+ },
+ "status" => {
+ "domain" => "#{name}.#{namespace}.#{domain}",
+ "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "latestCreatedRevisionName" => "#{name}-00002",
+ "latestReadyRevisionName" => "#{name}-00002",
+ "observedGeneration" => 2
+ }
+ }
+ end
+
+ def kube_service_full(name: "kubetest", namespace: "kube-ns", domain: "example.com")
+ {
+ "metadata" => {
+ "creationTimestamp" => "2018-11-21T06:16:33Z",
+ "name" => name,
+ "namespace" => namespace,
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
+ "annotation" => {
+ "description" => "This is a test description"
+ }
+ },
+ "spec" => {
+ "generation" => 2,
+ "build" => {
+ "template" => "go-1.10.3"
+ }
+ },
+ "status" => {
+ "domain" => "#{name}.#{namespace}.#{domain}",
+ "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "latestCreatedRevisionName" => "#{name}-00002",
+ "latestReadyRevisionName" => "#{name}-00002",
+ "observedGeneration" => 2
+ }
+ }
+ end
+
def kube_terminals(service, pod)
pod_name = pod['metadata']['name']
containers = pod['spec']['containers']