Merge branch 'ingress-hostnames' into 'master'
Add support for ingress hostnames Closes #58244, #56158, and #46691 See merge request gitlab-org/gitlab-ce!25181
This commit is contained in:
commit
5596eed162
30 changed files with 236 additions and 156 deletions
|
@ -89,53 +89,26 @@ export default {
|
|||
ingressInstalled() {
|
||||
return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
|
||||
},
|
||||
ingressExternalIp() {
|
||||
return this.applications.ingress.externalIp;
|
||||
ingressExternalEndpoint() {
|
||||
return this.applications.ingress.externalIp || this.applications.ingress.externalHostname;
|
||||
},
|
||||
certManagerInstalled() {
|
||||
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
|
||||
},
|
||||
ingressDescription() {
|
||||
const extraCostParagraph = sprintf(
|
||||
return sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|%{boldNotice} This will add some extra resources
|
||||
like a load balancer, which may incur additional costs depending on
|
||||
the hosting provider your Kubernetes cluster is installed on. If you are using
|
||||
Google Kubernetes Engine, you can %{pricingLink}.`,
|
||||
`ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}.`,
|
||||
),
|
||||
),
|
||||
{
|
||||
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
|
||||
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
|
||||
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
|
||||
pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const externalIpParagraph = sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
|
||||
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
|
||||
),
|
||||
),
|
||||
{
|
||||
ingressHelpLink: `<a href="${this.ingressHelpPath}">
|
||||
${_.escape(s__('ClusterIntegration|More information'))}
|
||||
</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return `
|
||||
<p>
|
||||
${extraCostParagraph}
|
||||
</p>
|
||||
<p class="settings-message append-bottom-0">
|
||||
${externalIpParagraph}
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
certManagerDescription() {
|
||||
return sprintf(
|
||||
|
@ -196,11 +169,26 @@ export default {
|
|||
knativeUpgradeFailed() {
|
||||
return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED;
|
||||
},
|
||||
knativeExternalIp() {
|
||||
return this.knative.externalIp;
|
||||
knativeExternalEndpoint() {
|
||||
return this.knative.externalIp || this.knative.externalHostname;
|
||||
},
|
||||
knativeDescription() {
|
||||
return sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`,
|
||||
),
|
||||
),
|
||||
{
|
||||
pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
canUpdateKnativeEndpoint() {
|
||||
return this.knativeExternalIp && !this.knativeUpgradeFailed && !this.knativeUpgrading;
|
||||
return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
|
||||
},
|
||||
knativeHostname: {
|
||||
get() {
|
||||
|
@ -289,31 +277,31 @@ export default {
|
|||
|
||||
<template v-if="ingressInstalled">
|
||||
<div class="form-group">
|
||||
<label for="ingress-ip-address">
|
||||
{{ s__('ClusterIntegration|Ingress IP Address') }}
|
||||
<label for="ingress-endpoint">
|
||||
{{ s__('ClusterIntegration|Ingress Endpoint') }}
|
||||
</label>
|
||||
<div v-if="ingressExternalIp" class="input-group">
|
||||
<div v-if="ingressExternalEndpoint" class="input-group">
|
||||
<input
|
||||
id="ingress-ip-address"
|
||||
:value="ingressExternalIp"
|
||||
id="ingress-endpoint"
|
||||
:value="ingressExternalEndpoint"
|
||||
type="text"
|
||||
class="form-control js-ip-address"
|
||||
class="form-control js-endpoint"
|
||||
readonly
|
||||
/>
|
||||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="ingressExternalIp"
|
||||
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
|
||||
:text="ingressExternalEndpoint"
|
||||
:title="s__('ClusterIntegration|Copy Ingress Endpoint to clipboard')"
|
||||
class="input-group-text js-clipboard-btn"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<input v-else type="text" class="form-control js-ip-address" readonly value="?" />
|
||||
<input v-else type="text" class="form-control js-endpoint" readonly value="?" />
|
||||
<p class="form-text text-muted">
|
||||
{{
|
||||
s__(`ClusterIntegration|Point a wildcard DNS to this
|
||||
generated IP address in order to access
|
||||
your application after it has been deployed.`)
|
||||
generated endpoint in order to access
|
||||
your application after it has been deployed.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
|
@ -321,19 +309,21 @@ export default {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<p v-if="!ingressExternalIp" class="settings-message js-no-ip-message">
|
||||
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
|
||||
{{
|
||||
s__(`ClusterIntegration|The IP address is in
|
||||
the process of being assigned. Please check your Kubernetes
|
||||
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
|
||||
s__(`ClusterIntegration|The endpoint is in
|
||||
the process of being assigned. Please check your Kubernetes
|
||||
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
|
||||
}}
|
||||
|
||||
<a :href="ingressHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
<div v-html="ingressDescription"></div>
|
||||
<template v-if="!ingressInstalled">
|
||||
<div class="bs-callout bs-callout-info" v-html="ingressDescription"></div>
|
||||
</template>
|
||||
</div>
|
||||
</application-row>
|
||||
<application-row
|
||||
|
@ -443,7 +433,7 @@ export default {
|
|||
}}
|
||||
</p>
|
||||
|
||||
<template v-if="ingressExternalIp">
|
||||
<template v-if="ingressExternalEndpoint">
|
||||
<div class="form-group">
|
||||
<label for="jupyter-hostname">
|
||||
{{ s__('ClusterIntegration|Jupyter Hostname') }}
|
||||
|
@ -468,7 +458,7 @@ export default {
|
|||
<p v-if="ingressInstalled" class="form-text text-muted">
|
||||
{{
|
||||
s__(`ClusterIntegration|Replace this with your own hostname if you want.
|
||||
If you do so, point hostname to Ingress IP Address from above.`)
|
||||
If you do so, point hostname to Ingress IP Address from above.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
|
@ -493,10 +483,10 @@ export default {
|
|||
>
|
||||
<div slot="description">
|
||||
<span v-if="!rbac">
|
||||
<p v-if="!rbac" class="bs-callout bs-callout-info append-bottom-0">
|
||||
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
|
||||
{{
|
||||
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
|
||||
to install Knative.`)
|
||||
to install Knative.`)
|
||||
}}
|
||||
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
|
@ -534,31 +524,31 @@ export default {
|
|||
</template>
|
||||
<template v-if="knativeInstalled">
|
||||
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
|
||||
<label for="knative-ip-address">
|
||||
<label for="knative-endpoint">
|
||||
<strong>
|
||||
{{ s__('ClusterIntegration|Knative Endpoint:') }}
|
||||
</strong>
|
||||
</label>
|
||||
<div v-if="knativeExternalIp" class="input-group">
|
||||
<div v-if="knativeExternalEndpoint" class="input-group">
|
||||
<input
|
||||
id="knative-ip-address"
|
||||
:value="knativeExternalIp"
|
||||
id="knative-endpoint"
|
||||
:value="knativeExternalEndpoint"
|
||||
type="text"
|
||||
class="form-control js-knative-ip-address"
|
||||
class="form-control js-knative-endpoint"
|
||||
readonly
|
||||
/>
|
||||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="knativeExternalIp"
|
||||
:text="knativeExternalEndpoint"
|
||||
:title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
|
||||
class="input-group-text js-knative-ip-clipboard-btn"
|
||||
class="input-group-text js-knative-endpoint-clipboard-btn"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
class="form-control js-knative-ip-address"
|
||||
class="form-control js-knative-endpoint"
|
||||
readonly
|
||||
value="?"
|
||||
/>
|
||||
|
@ -576,13 +566,13 @@ export default {
|
|||
</p>
|
||||
|
||||
<p
|
||||
v-if="!knativeExternalIp"
|
||||
class="settings-message js-no-knative-ip-message mt-2 mr-3 mb-0 ml-3 "
|
||||
v-if="!knativeExternalEndpoint"
|
||||
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
|
||||
>
|
||||
{{
|
||||
s__(`ClusterIntegration|The IP address is in
|
||||
the process of being assigned. Please check your Kubernetes
|
||||
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
|
||||
s__(`ClusterIntegration|The endpoint is in
|
||||
the process of being assigned. Please check your Kubernetes
|
||||
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
|
||||
}}
|
||||
</p>
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class ClusterStore {
|
|||
requestStatus: null,
|
||||
requestReason: null,
|
||||
externalIp: null,
|
||||
externalHostname: null,
|
||||
},
|
||||
cert_manager: {
|
||||
title: s__('ClusterIntegration|Cert-Manager'),
|
||||
|
@ -68,6 +69,7 @@ export default class ClusterStore {
|
|||
hostname: null,
|
||||
isEditingHostName: false,
|
||||
externalIp: null,
|
||||
externalHostname: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -120,6 +122,7 @@ export default class ClusterStore {
|
|||
|
||||
if (appId === INGRESS) {
|
||||
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
|
||||
this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
|
||||
} else if (appId === CERT_MANAGER) {
|
||||
this.state.applications.cert_manager.email =
|
||||
this.state.applications.cert_manager.email || serverAppEntry.email;
|
||||
|
@ -136,6 +139,8 @@ export default class ClusterStore {
|
|||
}
|
||||
this.state.applications.knative.externalIp =
|
||||
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
|
||||
this.state.applications.knative.externalHostname =
|
||||
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
|
||||
} else if (appId === RUNNER) {
|
||||
this.state.applications.runner.version = version;
|
||||
this.state.applications.runner.upgradeAvailable = upgradeAvailable;
|
||||
|
|
|
@ -48,6 +48,7 @@ module Clusters
|
|||
def schedule_status_update
|
||||
return unless installed?
|
||||
return if external_ip
|
||||
return if external_hostname
|
||||
|
||||
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
|
||||
end
|
||||
|
|
|
@ -18,8 +18,10 @@ module Clusters
|
|||
|
||||
def set_initial_status
|
||||
return unless not_installable?
|
||||
return unless cluster&.application_ingress_available?
|
||||
|
||||
if cluster&.application_ingress_available? && cluster.application_ingress.external_ip
|
||||
ingress = cluster.application_ingress
|
||||
if ingress.external_ip || ingress.external_hostname
|
||||
self.status = 'installable'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,7 @@ module Clusters
|
|||
def schedule_status_update
|
||||
return unless installed?
|
||||
return if external_ip
|
||||
return if external_hostname
|
||||
|
||||
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
|
||||
end
|
||||
|
|
|
@ -67,6 +67,7 @@ module Clusters
|
|||
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
|
||||
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
|
||||
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
|
||||
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
|
||||
|
||||
alias_attribute :base_domain, :domain
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ class ClusterApplicationEntity < Grape::Entity
|
|||
expose :status_reason
|
||||
expose :version
|
||||
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
|
||||
expose :external_hostname, if: -> (e, _) { e.respond_to?(:external_hostname) }
|
||||
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
|
||||
expose :email, if: -> (e, _) { e.respond_to?(:email) }
|
||||
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
|
||||
|
|
|
@ -11,9 +11,13 @@ module Clusters
|
|||
|
||||
def execute
|
||||
return if app.external_ip
|
||||
return if app.external_hostname
|
||||
return unless try_obtain_lease
|
||||
|
||||
app.update!(external_ip: ingress_ip) if ingress_ip
|
||||
app.external_ip = ingress_ip if ingress_ip
|
||||
app.external_hostname = ingress_hostname if ingress_hostname
|
||||
|
||||
app.save! if app.changed?
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -25,12 +29,16 @@ module Clusters
|
|||
end
|
||||
|
||||
def ingress_ip
|
||||
service.status.loadBalancer.ingress&.first&.ip
|
||||
ingress_service&.ip
|
||||
end
|
||||
|
||||
def service
|
||||
def ingress_hostname
|
||||
ingress_service&.hostname
|
||||
end
|
||||
|
||||
def ingress_service
|
||||
strong_memoize(:ingress_service) do
|
||||
app.ingress_service
|
||||
app.ingress_service.status.loadBalancer.ingress&.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
= s_('ClusterIntegration|Alternatively')
|
||||
%code #{@cluster.application_ingress_external_ip}.nip.io
|
||||
= s_('ClusterIntegration| can be used instead of a custom domain.')
|
||||
- custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip')
|
||||
- custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-external-endpoint')
|
||||
- custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url }
|
||||
= s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe }
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
cluster_status: @cluster.status_name,
|
||||
cluster_status_reason: @cluster.status_reason,
|
||||
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
|
||||
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
|
||||
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
|
||||
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-endpoint'),
|
||||
ingress_dns_help_path: help_page_path('user/project/clusters/index.md', anchor: 'manually-determining-the-external-endpoint'),
|
||||
manage_prometheus_path: manage_prometheus_path } }
|
||||
|
||||
.js-cluster-application-notice
|
||||
|
|
5
changelogs/unreleased/ingress-hostnames.yml
Normal file
5
changelogs/unreleased/ingress-hostnames.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added support for ingress hostnames
|
||||
merge_request: 25181
|
||||
author: walkafwalka
|
||||
type: added
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddExternalHostnameToIngressAndKnative < ActiveRecord::Migration[5.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :clusters_applications_ingress, :external_hostname, :string
|
||||
add_column :clusters_applications_knative, :external_hostname, :string
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20190301081611) do
|
||||
ActiveRecord::Schema.define(version: 20190301182457) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -708,6 +708,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do
|
|||
t.string "cluster_ip"
|
||||
t.text "status_reason"
|
||||
t.string "external_ip"
|
||||
t.string "external_hostname"
|
||||
t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true, using: :btree
|
||||
end
|
||||
|
||||
|
@ -733,6 +734,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do
|
|||
t.string "hostname"
|
||||
t.text "status_reason"
|
||||
t.string "external_ip"
|
||||
t.string "external_hostname"
|
||||
t.index ["cluster_id"], name: "index_clusters_applications_knative_on_cluster_id", unique: true, using: :btree
|
||||
end
|
||||
|
||||
|
|
|
@ -387,27 +387,27 @@ Upgrades will reset values back to the values built into the `runner`
|
|||
chart plus the values set by
|
||||
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml)
|
||||
|
||||
## Getting the external IP address
|
||||
## Getting the external endpoint
|
||||
|
||||
NOTE: **Note:**
|
||||
With the following procedure, a load balancer must be installed in your cluster
|
||||
to obtain the external IP address. You can use either
|
||||
to obtain the endpoint. You can use either
|
||||
[Ingress](#installing-applications), or Knative's own load balancer
|
||||
([Istio](https://istio.io)) if using [Knative](#installing-applications).
|
||||
|
||||
In order to publish your web application, you first need to find the external IP
|
||||
address associated to your load balancer.
|
||||
In order to publish your web application, you first need to find the endpoint which will be either an IP
|
||||
address or a hostname associated with your load balancer.
|
||||
|
||||
### Let GitLab fetch the IP address
|
||||
### Let GitLab fetch the external endpoint
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6.
|
||||
|
||||
If you [installed Ingress or Knative](#installing-applications),
|
||||
you should see the Ingress IP address on this same page within a few minutes.
|
||||
If you don't see this, GitLab might not be able to determine the IP address of
|
||||
you should see the Ingress Endpoint on this same page within a few minutes.
|
||||
If you don't see this, GitLab might not be able to determine the external endpoint of
|
||||
your ingress application in which case you should manually determine it.
|
||||
|
||||
### Manually determining the IP address
|
||||
### Manually determining the external endpoint
|
||||
|
||||
If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
|
||||
**Advanced settings**, or go directly to the
|
||||
|
@ -417,7 +417,7 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**.
|
|||
|
||||
If the cluster is not on GKE, follow the specific instructions for your
|
||||
Kubernetes provider to configure `kubectl` with the right credentials.
|
||||
The output of the following examples will show the external IP address of your
|
||||
The output of the following examples will show the external endpoint of your
|
||||
cluster. This information can then be used to set up DNS entries and forwarding
|
||||
rules that allow external access to your deployed applications.
|
||||
|
||||
|
@ -425,7 +425,13 @@ If you installed the Ingress [via the **Applications**](#installing-applications
|
|||
run the following command:
|
||||
|
||||
```bash
|
||||
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
|
||||
```
|
||||
|
||||
Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
|
||||
|
||||
```bash
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
|
||||
```
|
||||
|
||||
For Istio/Knative, the command will be different:
|
||||
|
@ -434,12 +440,6 @@ For Istio/Knative, the command will be different:
|
|||
kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
|
||||
```
|
||||
|
||||
Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
|
||||
|
||||
```bash
|
||||
kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
|
||||
```
|
||||
|
||||
Otherwise, you can list the IP addresses of all load balancers:
|
||||
|
||||
```bash
|
||||
|
@ -456,13 +456,12 @@ reserved IP.
|
|||
|
||||
Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
|
||||
|
||||
### Pointing your DNS at the cluster IP
|
||||
### Pointing your DNS at the external endpoint
|
||||
|
||||
Once you've set up the static IP, you should associate it to a [wildcard DNS
|
||||
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able
|
||||
to reach your apps. This heavily depends on your domain provider, but in case
|
||||
you aren't sure, just create an A record with a wildcard host like
|
||||
`*.example.com.`.
|
||||
Once you've set up the external endpoint, you should associate it with a [wildcard DNS
|
||||
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
|
||||
in order to be able to reach your apps. If your external endpoint is an IP address,
|
||||
use an A record. If your external endpoint is a hostname, use a CNAME record.
|
||||
|
||||
## Multiple Kubernetes clusters **[PREMIUM]**
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ To run Knative on Gitlab, you will need:
|
|||
applications or functions onto your cluster. You can install the GitLab Runner
|
||||
onto the existing Kubernetes cluster. See [Installing Applications](../index.md#installing-applications) for more information.
|
||||
1. **Domain Name:** Knative will provide its own load balancer using Istio. It will provide an
|
||||
external IP address for all the applications served by Knative. You will be prompted to enter a
|
||||
external IP address or hostname for all the applications served by Knative. You will be prompted to enter a
|
||||
wildcard domain where your applications will be served. Configure your DNS server to use the
|
||||
external IP address for that domain.
|
||||
external IP address or hostname for that domain.
|
||||
1. **`.gitlab-ci.yml`:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko)
|
||||
to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm) to simplify the
|
||||
deployment of knative services and functions.
|
||||
|
@ -62,18 +62,8 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.
|
|||
|
||||
![install-knative](img/install-knative.png)
|
||||
|
||||
1. After the Knative installation has finished, you can wait for the IP address to be displayed in the
|
||||
**Knative IP Address** field (takes up to 5 minutes) or retrieve the Istio Ingress IP address by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```bash
|
||||
35.161.143.124 my-machine-name:~ my-user$
|
||||
```
|
||||
1. After the Knative installation has finished, you can wait for the IP address or hostname to be displayed in the
|
||||
**Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../#manually-determining-the-external-endpoint).
|
||||
|
||||
NOTE: **Note:**
|
||||
Running `kubectl` commands on your cluster requires setting up access to the cluster first.
|
||||
|
@ -82,8 +72,8 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.
|
|||
|
||||
1. The ingress is now available at this address and will route incoming requests to the proper service based on the DNS
|
||||
name in the request. To support this, a wildcard DNS A record should be created for the desired domain name. For example,
|
||||
if your Knative base domain is `example.com` then you need to create an A record with domain `*.example.com`
|
||||
pointing the ip address of the ingress.
|
||||
if your Knative base domain is `knative.info` then you need to create an A record or CNAME record with domain `*.knative.info`
|
||||
pointing the ip address or hostname of the ingress.
|
||||
|
||||
![dns entry](img/dns-entry.png)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
|
|||
|
||||
### About managed NGINX Ingress deployments
|
||||
|
||||
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address).
|
||||
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
|
||||
|
||||
NGINX is configured for Prometheus monitoring, by setting:
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
|
|||
|
||||
### About managed NGINX Ingress deployments
|
||||
|
||||
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address).
|
||||
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
|
||||
|
||||
NGINX is configured for Prometheus monitoring, by setting:
|
||||
|
||||
|
|
|
@ -1614,9 +1614,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|%{title} upgraded successfully."
|
||||
msgstr ""
|
||||
|
||||
|
@ -1638,9 +1635,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Alternatively"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1695,7 +1689,7 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Copy CA Certificate"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
|
||||
msgid "ClusterIntegration|Copy Ingress Endpoint to clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
|
||||
|
@ -1782,7 +1776,7 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Ingress"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Ingress IP Address"
|
||||
msgid "ClusterIntegration|Ingress Endpoint"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
|
||||
|
@ -1797,6 +1791,12 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Installing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1878,9 +1878,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|More information"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|No machine types matched your search"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1893,9 +1890,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|No zones matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Note:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Number of nodes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1905,7 +1899,7 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
|
||||
msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Project cluster"
|
||||
|
@ -1998,7 +1992,7 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
|
||||
msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
|
||||
|
@ -2058,9 +2052,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|access to Google Kubernetes Engine"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|check the pricing here"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|documentation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2070,6 +2061,9 @@ msgstr ""
|
|||
msgid "ClusterIntegration|meets the requirements"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|pricing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|properly configured"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ module QA
|
|||
end
|
||||
|
||||
view 'app/assets/javascripts/clusters/components/applications.vue' do
|
||||
element :ingress_ip_address, 'id="ingress-ip-address"' # rubocop:disable QA/ElementWithPattern
|
||||
element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/clusters/clusters/_form.html.haml' do
|
||||
|
@ -35,7 +35,7 @@ module QA
|
|||
def ingress_ip
|
||||
# We need to wait longer since it can take some time before the
|
||||
# ip address is assigned for the ingress controller
|
||||
page.find('#ingress-ip-address', wait: 1200).value
|
||||
page.find('#ingress-endpoint', wait: 1200).value
|
||||
end
|
||||
|
||||
def set_domain(domain)
|
||||
|
|
|
@ -82,7 +82,7 @@ describe 'Clusters Applications', :js do
|
|||
|
||||
it 'should show info block and not be installable' do
|
||||
page.within('.js-cluster-application-row-knative') do
|
||||
expect(page).to have_css('.bs-callout-info')
|
||||
expect(page).to have_css('.rbac-notice')
|
||||
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
|
||||
end
|
||||
end
|
||||
|
@ -93,7 +93,7 @@ describe 'Clusters Applications', :js do
|
|||
|
||||
it 'should not show callout block and be installable' do
|
||||
page.within('.js-cluster-application-row-knative') do
|
||||
expect(page).not_to have_css('.bs-callout-info')
|
||||
expect(page).not_to have_css('.rbac-notice')
|
||||
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
|
||||
end
|
||||
end
|
||||
|
@ -226,14 +226,14 @@ describe 'Clusters Applications', :js do
|
|||
|
||||
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
|
||||
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
|
||||
expect(page).to have_selector('.js-no-ip-message')
|
||||
expect(page.find('.js-ip-address').value).to eq('?')
|
||||
expect(page).to have_selector('.js-no-endpoint-message')
|
||||
expect(page.find('.js-endpoint').value).to eq('?')
|
||||
|
||||
# We receive the external IP address and display
|
||||
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
|
||||
|
||||
expect(page).not_to have_selector('.js-no-ip-message')
|
||||
expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
|
||||
expect(page).not_to have_selector('.js-no-endpoint-message')
|
||||
expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
|
||||
end
|
||||
|
||||
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"version": { "type": "string" },
|
||||
"status_reason": { "type": ["string", "null"] },
|
||||
"external_ip": { "type": ["string", "null"] },
|
||||
"external_hostname": { "type": ["string", "null"] },
|
||||
"hostname": { "type": ["string", "null"] },
|
||||
"email": { "type": ["string", "null"] },
|
||||
"update_available": { "type": ["boolean", "null"] }
|
||||
|
|
|
@ -106,7 +106,7 @@ describe('Applications', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
|
||||
expect(vm.$el.querySelector('.js-endpoint').value).toEqual('0.0.0.0');
|
||||
|
||||
expect(
|
||||
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
|
||||
|
@ -114,6 +114,32 @@ describe('Applications', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with hostname', () => {
|
||||
it('renders hostname with a clipboard button', () => {
|
||||
vm = mountComponent(Applications, {
|
||||
applications: {
|
||||
ingress: {
|
||||
title: 'Ingress',
|
||||
status: 'installed',
|
||||
externalHostname: 'localhost.localdomain',
|
||||
},
|
||||
helm: { title: 'Helm Tiller' },
|
||||
cert_manager: { title: 'Cert-Manager' },
|
||||
runner: { title: 'GitLab Runner' },
|
||||
prometheus: { title: 'Prometheus' },
|
||||
jupyter: { title: 'JupyterHub', hostname: '' },
|
||||
knative: { title: 'Knative', hostname: '' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-endpoint').value).toEqual('localhost.localdomain');
|
||||
|
||||
expect(
|
||||
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
|
||||
).toEqual('localhost.localdomain');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without ip address', () => {
|
||||
it('renders an input text with a question mark and an alert text', () => {
|
||||
vm = mountComponent(Applications, {
|
||||
|
@ -126,9 +152,9 @@ describe('Applications', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
|
||||
expect(vm.$el.querySelector('.js-endpoint').value).toEqual('?');
|
||||
|
||||
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
|
||||
expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -140,7 +166,7 @@ describe('Applications', () => {
|
|||
});
|
||||
|
||||
expect(vm.$el.textContent).not.toContain('Ingress IP Address');
|
||||
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
|
||||
expect(vm.$el.querySelector('.js-endpoint')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -268,11 +294,11 @@ describe('Applications', () => {
|
|||
it('renders ip address with a clipboard button', () => {
|
||||
vm = mountComponent(Applications, props);
|
||||
|
||||
expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('1.1.1.1');
|
||||
expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1');
|
||||
|
||||
expect(
|
||||
vm.$el
|
||||
.querySelector('.js-knative-ip-clipboard-btn')
|
||||
.querySelector('.js-knative-endpoint-clipboard-btn')
|
||||
.getAttribute('data-clipboard-text'),
|
||||
).toEqual('1.1.1.1');
|
||||
});
|
||||
|
@ -316,9 +342,9 @@ describe('Applications', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('?');
|
||||
expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('?');
|
||||
|
||||
expect(vm.$el.querySelector('.js-no-knative-ip-message')).not.toBe(null);
|
||||
expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ const CLUSTERS_MOCK_DATA = {
|
|||
status: APPLICATION_STATUS.ERROR,
|
||||
status_reason: 'Cannot connect',
|
||||
external_ip: null,
|
||||
external_hostname: null,
|
||||
},
|
||||
{
|
||||
name: 'runner',
|
||||
|
@ -62,6 +63,7 @@ const CLUSTERS_MOCK_DATA = {
|
|||
status: APPLICATION_STATUS.INSTALLED,
|
||||
status_reason: 'Cannot connect',
|
||||
external_ip: '1.1.1.1',
|
||||
external_hostname: null,
|
||||
},
|
||||
{
|
||||
name: 'runner',
|
||||
|
|
|
@ -78,6 +78,7 @@ describe('Clusters Store', () => {
|
|||
requestStatus: null,
|
||||
requestReason: null,
|
||||
externalIp: null,
|
||||
externalHostname: null,
|
||||
},
|
||||
runner: {
|
||||
title: 'GitLab Runner',
|
||||
|
@ -113,6 +114,7 @@ describe('Clusters Store', () => {
|
|||
hostname: null,
|
||||
isEditingHostName: false,
|
||||
externalIp: null,
|
||||
externalHostname: null,
|
||||
},
|
||||
cert_manager: {
|
||||
title: 'Cert-Manager',
|
||||
|
|
|
@ -56,6 +56,14 @@ describe Clusters::Applications::Ingress do
|
|||
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is already an external_hostname' do
|
||||
let(:application) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
|
||||
|
||||
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
|
||||
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#install_command' do
|
||||
|
|
|
@ -26,6 +26,13 @@ describe Clusters::Applications::Jupyter do
|
|||
|
||||
it { expect(jupyter).to be_installable }
|
||||
end
|
||||
|
||||
context 'when ingress is installed and external_hostname is assigned' do
|
||||
let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
|
||||
let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
|
||||
|
||||
it { expect(jupyter).to be_installable }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#install_command' do
|
||||
|
|
|
@ -64,6 +64,14 @@ describe Clusters::Applications::Knative do
|
|||
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is already an external_hostname' do
|
||||
let(:application) { create(:clusters_applications_knative, :installed, external_hostname: 'localhost.localdomain') }
|
||||
|
||||
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
|
||||
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a command' do
|
||||
|
|
|
@ -31,6 +31,7 @@ describe Clusters::Cluster do
|
|||
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
|
||||
it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
|
||||
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
|
||||
it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
|
||||
|
||||
it { is_expected.to respond_to :project }
|
||||
|
||||
|
|
|
@ -6,9 +6,17 @@ describe Clusters::Applications::CheckIngressIpAddressService do
|
|||
let(:application) { create(:clusters_applications_ingress, :installed) }
|
||||
let(:service) { described_class.new(application) }
|
||||
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
|
||||
let(:ingress) { [{ ip: '111.222.111.222' }] }
|
||||
let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
|
||||
|
||||
let(:ingress) do
|
||||
[
|
||||
{
|
||||
ip: '111.222.111.222',
|
||||
hostname: 'localhost.localdomain'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
let(:kube_service) do
|
||||
::Kubeclient::Resource.new(
|
||||
{
|
||||
|
|
|
@ -12,6 +12,14 @@ shared_examples 'check ingress ip executions' do |app_name|
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the ingress external hostname is available' do
|
||||
it 'updates the external_hostname for the app' do
|
||||
subject
|
||||
|
||||
expect(application.external_hostname).to eq('localhost.localdomain')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ingress ip address is not available' do
|
||||
let(:ingress) { nil }
|
||||
|
||||
|
|
Loading…
Reference in a new issue