Commit 1102deb0 authored by Chris Baumbauer's avatar Chris Baumbauer

Initial Serverless Functions detailed view

parent 71026ffd
<script>
import PodBox from './pod_box.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
PodBox,
ClipboardButton,
},
props: {
func: {
type: Object,
required: true,
},
},
computed: {
name() {
return this.func.name;
},
description() {
return this.func.description;
},
funcUrl() {
return this.func.url;
},
podCount() {
return this.func.podcount || 0;
},
},
};
</script>
<template>
<section id="serverless-function-details">
<h3>{{ name }}</h3>
<div class="append-bottom-default">
<div v-for="line in description.split('\n')" :key="line">{{ line }}<br /></div>
</div>
<div class="clipboard-group append-bottom-default">
<div class="label label-monospace">{{ funcUrl }}</div>
<clipboard-button
:text="String(funcUrl)"
:title="s__('ServerlessDetails|Copy URL to clipboard')"
class="input-group-text js-clipboard-btn"
/>
<a
:href="funcUrl"
target="_blank"
rel="noopener noreferrer nofollow"
class="input-group-text btn btn-default"
>
<icon name="external-link" />
</a>
</div>
<h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4>
<div v-if="podCount > 0">
<p>
<b v-if="podCount == 1">{{ podCount }} {{ s__('ServerlessDetails|pod in use') }}</b>
<b v-else>{{ podCount }} {{ s__('ServerlessDetails|pods in use') }}</b>
</p>
<pod-box :count="podCount" />
<p>
{{
s__('ServerlessDetails|Number of Kubernetes pods in use over time based on necessity.')
}}
</p>
</div>
<div v-else><p>No pods loaded at this time.</p></div>
</section>
</template>
...@@ -15,8 +15,14 @@ export default { ...@@ -15,8 +15,14 @@ export default {
name() { name() {
return this.func.name; return this.func.name;
}, },
url() { description() {
return this.func.url; return this.func.description;
},
detailUrl() {
return this.func.detail_url;
},
environment() {
return this.func.environment_scope;
}, },
image() { image() {
return this.func.image; return this.func.image;
...@@ -30,11 +36,20 @@ export default { ...@@ -30,11 +36,20 @@ export default {
<template> <template>
<div class="gl-responsive-table-row"> <div class="gl-responsive-table-row">
<div class="table-section section-20">{{ name }}</div> <div class="table-section section-20 section-wrap">
<div class="table-section section-50"> <a :href="detailUrl">{{ name }}</a>
<a :href="url">{{ url }}</a> </div>
<div class="table-section section-10">{{ environment }}</div>
<div class="table-section section-40 section-wrap">
<span class="line-break">{{ description }}</span>
</div> </div>
<div class="table-section section-20">{{ image }}</div> <div class="table-section section-20">{{ image }}</div>
<div class="table-section section-10"><timeago :time="timestamp" /></div> <div class="table-section section-10"><timeago :time="timestamp" /></div>
</div> </div>
</template> </template>
<style>
.line-break {
white-space: pre;
}
</style>
...@@ -50,8 +50,11 @@ export default { ...@@ -50,8 +50,11 @@ export default {
<div class="table-section section-20" role="rowheader"> <div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Function') }} {{ s__('Serverless|Function') }}
</div> </div>
<div class="table-section section-50" role="rowheader"> <div class="table-section section-10" role="rowheader">
{{ s__('Serverless|Domain') }} {{ s__('Serverless|Cluster Env') }}
</div>
<div class="table-section section-40" role="rowheader">
{{ s__('Serverless|Description') }}
</div> </div>
<div class="table-section section-20" role="rowheader"> <div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Runtime') }} {{ s__('Serverless|Runtime') }}
......
<script>
export default {
props: {
count: {
type: Number,
required: true,
},
color: {
type: String,
required: false,
default: 'green',
},
},
methods: {
boxOffset(i) {
return 20 * (i - 1);
},
},
};
</script>
<template>
<svg :width="boxOffset(count + 1)" :height="20">
<rect
v-for="i in count"
:key="i"
width="15"
height="15"
rx="5"
ry="5"
:fill="color"
:x="boxOffset(i)"
y="0"
/>
</svg>
</template>
...@@ -4,11 +4,52 @@ import { s__ } from '../locale'; ...@@ -4,11 +4,52 @@ import { s__ } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import ServerlessStore from './stores/serverless_store'; import ServerlessStore from './stores/serverless_store';
import ServerlessDetailsStore from './stores/serverless_details_store';
import GetFunctionsService from './services/get_functions_service'; import GetFunctionsService from './services/get_functions_service';
import Functions from './components/functions.vue'; import Functions from './components/functions.vue';
import FunctionDetails from './components/function_details.vue';
export default class Serverless { export default class Serverless {
constructor() { constructor() {
if (document.querySelector('.js-serverless-function-details-page') != null) {
const {
serviceName,
serviceDescription,
serviceEnvironment,
serviceUrl,
serviceNamespace,
servicePodcount,
} = document.querySelector('.js-serverless-function-details-page').dataset;
const el = document.querySelector('#js-serverless-function-details');
this.store = new ServerlessDetailsStore();
const { store } = this;
const service = {
name: serviceName,
description: serviceDescription,
environment: serviceEnvironment,
url: serviceUrl,
namespace: serviceNamespace,
podcount: servicePodcount,
};
this.store.updateDetailedFunction(service);
this.functionDetails = new Vue({
el,
data() {
return {
state: store.state,
};
},
render(createElement) {
return createElement(FunctionDetails, {
props: {
func: this.state.functionDetail,
},
});
},
});
} else {
const { statusPath, clustersPath, helpPath, installed } = document.querySelector( const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
'.js-serverless-functions-page', '.js-serverless-functions-page',
).dataset; ).dataset;
...@@ -23,6 +64,7 @@ export default class Serverless { ...@@ -23,6 +64,7 @@ export default class Serverless {
this.initPolling(); this.initPolling();
} }
} }
}
initServerless() { initServerless() {
const { store } = this; const { store } = this;
...@@ -55,7 +97,7 @@ export default class Serverless { ...@@ -55,7 +97,7 @@ export default class Serverless {
resource: this.service, resource: this.service,
method: 'fetchData', method: 'fetchData',
successCallback: data => this.handleSuccess(data), successCallback: data => this.handleSuccess(data),
errorCallback: () => this.handleError(), errorCallback: () => Serverless.handleError(),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -64,7 +106,7 @@ export default class Serverless { ...@@ -64,7 +106,7 @@ export default class Serverless {
this.service this.service
.fetchData() .fetchData()
.then(data => this.handleSuccess(data)) .then(data => this.handleSuccess(data))
.catch(() => this.handleError()); .catch(() => Serverless.handleError());
} }
Visibility.change(() => { Visibility.change(() => {
...@@ -102,5 +144,6 @@ export default class Serverless { ...@@ -102,5 +144,6 @@ export default class Serverless {
} }
this.functions.$destroy(); this.functions.$destroy();
this.functionDetails.$destroy();
} }
} }
export default class ServerlessDetailsStore {
constructor() {
this.state = {
functionDetail: {},
};
}
updateDetailedFunction(func) {
this.state.functionDetail = func;
}
}
...@@ -7,19 +7,17 @@ module Projects ...@@ -7,19 +7,17 @@ module Projects
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
INDEX_PRIMING_INTERVAL = 10_000 INDEX_PRIMING_INTERVAL = 15_000
INDEX_POLLING_INTERVAL = 30_000 INDEX_POLLING_INTERVAL = 60_000
def index def index
finder = Projects::Serverless::FunctionsFinder.new(project.clusters)
respond_to do |format| respond_to do |format|
format.json do format.json do
functions = finder.execute functions = finder.execute
if functions.any? if functions.any?
Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL) Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions) render json: serialize_function(functions)
else else
Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL) Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
head :no_content head :no_content
...@@ -32,6 +30,29 @@ module Projects ...@@ -32,6 +30,29 @@ module Projects
end end
end end
end end
def show
@service = serialize_function(finder.service(params[:environment_id], params[:id]))
return not_found if @service.nil?
respond_to do |format|
format.json do
render json: @service
end
format.html
end
end
private
def finder
Projects::Serverless::FunctionsFinder.new(project.clusters)
end
def serialize_function(function)
Projects::Serverless::ServiceSerializer.new(current_user: @current_user, project: project).represent(function)
end
end end
end end
end end
...@@ -15,11 +15,40 @@ module Projects ...@@ -15,11 +15,40 @@ module Projects
clusters_with_knative_installed.exists? clusters_with_knative_installed.exists?
end end
def service(environment_scope, name)
knative_service(environment_scope, name)&.first
end
private private
def knative_service(environment_scope, name)
clusters_with_knative_installed.preload_knative.map do |cluster|
next if environment_scope != cluster.environment_scope
services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
.select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil?
end
end
def knative_services def knative_services
clusters_with_knative_installed.preload_knative.map do |cluster| clusters_with_knative_installed.preload_knative.map do |cluster|
cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace) services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
add_metadata(cluster, services) unless services.nil?
end
end
def add_metadata(cluster, services)
services.each do |s|
s["environment_scope"] = cluster.environment_scope
s["cluster_id"] = cluster.id
if services.length == 1
s["podcount"] = cluster.application_knative.service_pod_details(
cluster.platform_kubernetes&.actual_namespace,
s["metadata"]["name"]).length
end
end end
end end
......
...@@ -41,6 +41,8 @@ module Clusters ...@@ -41,6 +41,8 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) } scope :for_cluster, -> (cluster) { where(cluster: cluster) }
after_save :clear_reactive_cache!
def chart def chart
'knative/knative' 'knative/knative'
end end
...@@ -79,7 +81,7 @@ module Clusters ...@@ -79,7 +81,7 @@ module Clusters
end end
def calculate_reactive_cache def calculate_reactive_cache
{ services: read_services } { services: read_services, pods: read_pods }
end end
def ingress_service def ingress_service
...@@ -87,7 +89,7 @@ module Clusters ...@@ -87,7 +89,7 @@ module Clusters
end end
def services_for(ns: namespace) def services_for(ns: namespace)
return unless services return [] unless services
return [] unless ns return [] unless ns
services.select do |service| services.select do |service|
...@@ -95,8 +97,22 @@ module Clusters ...@@ -95,8 +97,22 @@ module Clusters
end end
end end
def service_pod_details(ns, service)
with_reactive_cache do |data|
data[:pods].select { |pod| filter_pods(pod, ns, service) }
end
end
private private
def read_pods
cluster.kubeclient.core_client.get_pods.as_json
end
def filter_pods(pod, namespace, service)
pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
end
def read_services def read_services
client.get_services.as_json client.get_services.as_json
rescue Kubeclient::ResourceNotFoundError rescue Kubeclient::ResourceNotFoundError
......
...@@ -13,6 +13,25 @@ module Projects ...@@ -13,6 +13,25 @@ module Projects
service.dig('metadata', 'namespace') service.dig('metadata', 'namespace')
end end
expose :environment_scope do |service|
service.dig('environment_scope')
end
expose :cluster_id do |service|
service.dig('cluster_id')
end
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')
end
expose :created_at do |service| expose :created_at do |service|
service.dig('metadata', 'creationTimestamp') service.dig('metadata', 'creationTimestamp')
end end
...@@ -22,11 +41,24 @@ module Projects ...@@ -22,11 +41,24 @@ module Projects
end end
expose :description do |service| expose :description do |service|
service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description') service.dig(
'spec',
'runLatest',
'configuration',
'revisionTemplate',
'metadata',
'annotations',
'Description')
end end
expose :image do |service| expose :image do |service|
service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name') service.dig(
'spec',
'runLatest',
'configuration',
'build',
'template',
'name')
end end
end end
end end
......
- @no_container = true
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project))
- page_title @service[:name]
.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json } }
%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
.top-area.adjust
.serverless-function-details#js-serverless-function-details
.js-serverless-function-notice
.flash-container
.function-holder.js-function-holder.input-group
---
title: Add Knative detailed view
merge_request: 23863
author: Chris Baumbauer
type: added
...@@ -247,6 +247,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -247,6 +247,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
namespace :serverless do namespace :serverless do
get '/functions/:environment_id/:id', to: 'functions#show'
resources :functions, only: [:index] resources :functions, only: [:index]
end end
......
...@@ -167,8 +167,8 @@ appear under **Operations > Serverless**. ...@@ -167,8 +167,8 @@ appear under **Operations > Serverless**.
![serverless page](img/serverless-page.png) ![serverless page](img/serverless-page.png)
This page contains all functions available for the project, the URL for This page contains all functions available for the project, the description for
accessing the function, and if available, the function's runtime information. accessing the function, and, if available, the function's runtime information.
The details are derived from the Knative installation inside each of the project's The details are derived from the Knative installation inside each of the project's
Kubernetes cluster. Kubernetes cluster.
...@@ -184,6 +184,12 @@ The sample function can now be triggered from any HTTP client using a simple `PO ...@@ -184,6 +184,12 @@ The sample function can now be triggered from any HTTP client using a simple `PO
Currently, the Serverless page presents all functions available in all clusters registered for the project with Knative installed. Currently, the Serverless page presents all functions available in all clusters registered for the project with Knative installed.
Clicking on the function name will provide additional details such as the
function's URL as well as runtime statistics such as the number of active pods
available to service the request based on load.
![serverless function details](img/serverless-details.png)
## Deploying Serverless applications ## Deploying Serverless applications
> Introduced in GitLab 11.5. > Introduced in GitLab 11.5.
......
...@@ -6057,13 +6057,31 @@ msgstr "" ...@@ -6057,13 +6057,31 @@ msgstr ""
msgid "Serverless" msgid "Serverless"
msgstr "" msgstr ""
msgid "ServerlessDetails|Copy URL to clipboard"
msgstr ""
msgid "ServerlessDetails|Kubernetes Pods"
msgstr ""
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
msgstr ""
msgid "ServerlessDetails|pod in use"
msgstr ""
msgid "ServerlessDetails|pods in use"
msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster." msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr "" msgstr ""
msgid "Serverless|An error occurred while retrieving serverless components" msgid "Serverless|An error occurred while retrieving serverless components"
msgstr "" msgstr ""
msgid "Serverless|Domain" msgid "Serverless|Cluster Env"
msgstr ""
msgid "Serverless|Description"
msgstr "" msgstr ""
msgid "Serverless|Function" msgid "Serverless|Function"
......
...@@ -45,9 +45,45 @@ describe Projects::Serverless::FunctionsController do ...@@ -45,9 +45,45 @@ describe Projects::Serverless::FunctionsController do
end end
end end
describe 'GET #show' do
context 'invalid data' do
it 'has a bad function name' do
get :show, params: params({ format: :json, environment_id: "*", id: "foo" })
expect(response).to have_gitlab_http_status(404)
end
end
context 'valid data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
stub_reactive_cache(knative,
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
})
end
it 'has a valid function name' do
get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
expect(response).to have_gitlab_http_status(200)
expect(json_response).to include(
"name" => project.name,
"url" => "http://#{project.name}.#{namespace.namespace}.example.com",
"podcount" => 1
)
end
end
end
describe 'GET #index with data', :use_clean_rails_memory_store_caching do describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do before do
stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"]) stub_kubeclient_service_pods
stub_reactive_cache(knative,
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
})
end end
it 'has data' do it 'has data' do
......
...@@ -29,15 +29,34 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -29,15 +29,34 @@ describe Projects::Serverless::FunctionsFinder do
context 'has knative installed' do context 'has knative installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:finder) { described_class.new(project.clusters) }
it 'there are no functions' do it 'there are no functions' do
expect(described_class.new(project.clusters).execute).to be_empty expect(finder.execute).to be_empty
end end
it 'there are functions', :use_clean_rails_memory_store_caching do 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"]) stub_kubeclient_service_pods
stub_reactive_cache(knative,
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
})
expect(finder.execute).not_to be_empty
end
it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
stub_reactive_cache(knative,
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
})
expect(described_class.new(project.clusters).execute).not_to be_empty 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)
end end
end end
end end
......
...@@ -149,6 +149,35 @@ describe Clusters::Applications::Knative do ...@@ -149,6 +149,35 @@ describe Clusters::Applications::Knative do
it { is_expected.to validate_presence_of(:hostname) } it { is_expected.to validate_presence_of(:hostname) }
end end
describe '#service_pod_details' 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
before do
stub_kubeclient_discover(service.api_url)
stub_kubeclient_knative_services
stub_kubeclient_service_pods
stub_reactive_cache(knative,
{
services: kube_response(kube_knative_services_body),
pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
})
synchronous_reactive_cache(knative)
end
it 'should be able k8s core for pod details' do
expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
end
end
describe '#services' do describe '#services' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
...@@ -166,6 +195,7 @@ describe Clusters::Applications::Knative do ...@@ -166,6 +195,7 @@ describe Clusters::Applications::Knative do
before do before do
stub_kubeclient_discover(service.api_url) stub_kubeclient_discover(service.api_url)
stub_kubeclient_knative_services stub_kubeclient_knative_services
stub_kubeclient_service_pods
end end
it 'should have an unintialized cache' do it 'should have an unintialized cache' do
...@@ -174,7 +204,11 @@ describe Clusters::Applications::Knative do ...@@ -174,7 +204,11 @@ describe Clusters::Applications::Knative do
context 'when using synchronous reactive cache' do context 'when using synchronous reactive cache' do
before do before do
stub_reactive_cache(knative, services: kube_response(kube_knative_services_body)) stub_reactive_cache(knative,
{
services: kube_response(kube_knative_services_body),
pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
})
synchronous_reactive_cache(knative) synchronous_reactive_cache(knative)
end end
......
...@@ -20,6 +20,13 @@ module KubernetesHelpers ...@@ -20,6 +20,13 @@ module KubernetesHelpers
WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body)) WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
end end
def stub_kubeclient_service_pods(response = nil)
stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
def stub_kubeclient_pods(response = nil) def stub_kubeclient_pods(response = nil)
stub_kubeclient_discover(service.api_url) stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
...@@ -212,6 +219,13 @@ module KubernetesHelpers ...@@ -212,6 +219,13 @@ module KubernetesHelpers
} }
end end
def kube_knative_pods_body(name, namespace)
{
"kind" => "PodList",
"items" => [kube_knative_pod(name: name, namespace: namespace)]
}
end
def kube_knative_services_body(**options) def kube_knative_services_body(**options)
{ {
"kind" => "List", "kind" => "List",
...@@ -242,6 +256,28 @@ module KubernetesHelpers ...@@ -242,6 +256,28 @@ module KubernetesHelpers
} }
end end
# Similar to a kube_pod, but should contain a running service
def kube_knative_pod(name: "kube-pod", namespace: "default", status: "Running")
{
"metadata" => {
"name" => name,
"namespace" => namespace,
"generate_name" => "generated-name-with-suffix",
"creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => {
"serving.knative.dev/service" => name
}
},
"spec" => {
"containers" => [
{ "name" => "container-0" },
{ "name" => "container-1" }
]
},
"status" => { "phase" => status }
}
end
def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil) def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
{ {
"metadata" => { "metadata" => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment