Skip to content

Commit

Permalink
Merge branch 'main' into RHIDP-4264-2
Browse files Browse the repository at this point in the history
  • Loading branch information
nilgaar committed Nov 14, 2024
2 parents d91591b + 6f77c7d commit 93b4ab3
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 114 deletions.
7 changes: 6 additions & 1 deletion .rhdh/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -364,13 +364,18 @@ ENV NPM_CONFIG_ignore-scripts='true'
ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t
ENV SEGMENT_TEST_MODE=false

# RHIDP-2217: corporate proxy support (configured using 'global-agent')
# RHIDP-2217: corporate proxy support (configured using 'global-agent' for 'node-fetch' calls and 'undici' for 'fetch' calls)
# This is to avoid having to define several environment variables for the same purpose,
# i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios).
# By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value,
# 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.
ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE=''

# RHDHBUGS-106,RHIDP-4646: requests to the loopback interface should bypass the corporate proxy if set.
# Note that NO_PROXY will take effect only if the 'HTTP(S)_PROXY' environment variables are set.
# Users can still override this when running the image.
ENV NO_PROXY='localhost,127.0.0.1'

# The `--no-node-snapshot` node option enables the usage of the backstage scaffolder on nodejs 20
# https://github.com/backstage/backstage/issues/20661

Expand Down
7 changes: 6 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,18 @@ ENV NPM_CONFIG_ignore-scripts='true'
ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t
ENV SEGMENT_TEST_MODE=false

# RHIDP-2217: corporate proxy support (configured using 'global-agent')
# RHIDP-2217: corporate proxy support (configured using 'global-agent' for 'node-fetch' calls and 'undici' for 'fetch' calls)
# This is to avoid having to define several environment variables for the same purpose,
# i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios).
# By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value,
# 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.
ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE=''

# RHDHBUGS-106,RHIDP-4646: requests to the loopback interface should bypass the corporate proxy if set.
# Note that NO_PROXY will take effect only if the 'HTTP(S)_PROXY' environment variables are set.
# Users can still override this when running the image.
ENV NO_PROXY='localhost,127.0.0.1'

# The `--no-node-snapshot` node option enables the usage of the backstage scaffolder on nodejs 20
# https://github.com/backstage/backstage/issues/20661

Expand Down
26 changes: 23 additions & 3 deletions docs/corporate-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,25 @@ Out of the box, the Showcase application can be run behind a corporate proxy, by
- `HTTP_PROXY`: Proxy to use for HTTP requests.
- `HTTPS_PROXY`: Proxy to use for HTTPS requests.

Additionally, you can set the `NO_PROXY` environment variable to exclude certain domains from proxying. The value is a comma-separated list of hostnames that do not require a proxy to get reached, even if one is specified.
Additionally, you can set the `NO_PROXY` environment variable to exclude certain domains from proxying. The value is a comma or space-separated list of hostnames that do not require a proxy to get reached, even if one is specified.

## Understanding the `NO_PROXY` exclusion rules

`NO_PROXY` is a comma or space-separated list of hostnames or IP addresses, optionally with port numbers. If the input URL matches any of the entries listed in `NO_PROXY`, then that URL will be fetched by a direct request (i.e., bypassing the proxy settings).

Note that the default value for `NO_PROXY` in the container image is `localhost,127.0.0.1`. If you want to override it, please make sure to also include at least `localhost` or `localhost:7007` in the list; otherwise, the Backend might not work correctly.

Matching follows the rules below:

- `NO_PROXY=*` will bypass the proxy for all requests.
- Space and commas may be used to separate the entries in the `NO_PROXY` list. For example, `NO_PROXY=localhost,example.com`, `NO_PROXY="localhost example.com"`, or `NO_PROXY="localhost, example.com"` would have the same effect.
- If `NO_PROXY` does not contain any entries, then all requests will be sent through the proxy if the `HTTP(S)_PROXY` settings are configured. Otherwise, requests will be fetched directly.
- No DNS lookup is performed to decide if a request should bypass the proxy or not. For example, if DNS is known to resolve `example.com` to `1.2.3.4`, setting `NO_PROXY=1.2.3.4` will not have any effect on requests sent to `example.com`. Only requests explicitly sent to the IP address `1.2.3.4` will bypass the proxy.
- If a port is added after the host name or IP Address, then the input request must match both the host/IP and port in order to bypass the proxy. For example, `NO_PROXY=example.com:1234` would exclude requests to `http(s)://example.com:1234` (so calling them directly), but not requests to other ports like `http(s)://example.com` (which will be sent through the proxy).
- If no port is specified after the host name or IP address, all requests to that host/IP address will bypass the proxy regardless of the port. For example, `NO_PROXY=localhost` would exclude all requests sent to `localhost` (so calling them directly), like `http(s)://localhost:7077` and `http(s)://localhost:8888`.
- IP Address blocks in CIDR notation will not work. So setting `NO_PROXY=10.11.0.0/16` will not have any effect, even if a request is explicitly sent to an IP address in that block.
- Only IPv4 addresses are supported. IPv6 addresses like `::1` will not work.
- Generally, the proxy is only bypassed if the host name is an exact match for an entry in the `NO_PROXY` list. The only exceptions are entries that start with a dot (`.`) or with a wildcard (`*`). In such a case, the proxy is bypassed if the host name ends with the entry. Please note that you should list both the domain and the wildcard domain if you want to exclude a domain and all its subdomains. For example, you would set `NO_PROXY=example.com,.example.com` to bypass the proxy for requests sent to `http(s)://example.com` and `http(s)://subdomain.example.com`.

## Helm deployment

Expand All @@ -23,7 +41,8 @@ upstream:
value: '<my_https_proxy_url>'
- name: NO_PROXY
# List of comma-separated URLs that should be excluded from proxying.
# Example: 'foo.com,baz.com'
# Make sure you include 'localhost'.
# Example: 'localhost,foo.com,baz.com'
value: '<my_no_proxy_settings>'
```
Expand Down Expand Up @@ -62,7 +81,8 @@ spec:
value: '<my_https_proxy_url>'
- name: NO_PROXY
# List of comma-separated URLs that should be excluded from proxying.
# Example: 'foo.com,baz.com'
# Make sure you include 'localhost'.
# Example: 'localhost,foo.com,baz.com'
value: '<my_no_proxy_settings>'
```
Expand Down
120 changes: 114 additions & 6 deletions docs/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

As mentioned in [Running the Showcase application behind a corporate proxy](corporate-proxy.md), the `HTTP(S)_PROXY` and `NO_PROXY` environment variables are supported.

If you are behind a corporate proxy and are running the Showcase locally, as depicted in [Running locally with a basic configuration](index.md#running-locally-with-a-basic-configuration) or [Running locally with the Optional Plugins](index.md#running-locally-with-the-optional-plugins), you will need to additionally set the `GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE` to an empty value prior to running `yarn start`.
If you are behind a corporate proxy and are running the Showcase locally with `yarn`, as depicted in [Running locally with a basic configuration](index.md#running-locally-with-a-basic-configuration) or [Running locally with the Optional Plugins](index.md#running-locally-with-the-optional-plugins), you will need to additionally set the `GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE` to an empty value prior to running `yarn start`.

Example:

Expand All @@ -20,22 +20,117 @@ You can use the command below to quickly start a local corporate proxy server (b
podman container run --rm --name squid-container \
-e TZ=UTC \
-p 3128:3128 \
-it docker.io/ubuntu/squid:latest
-it registry.redhat.io/rhel9/squid:latest
```

# Plugin vendors

The upstream Backstage project recommends the use of the `node-fetch` libraries in backend plugins for HTTP data fetching - see [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013/).

We currently only support corporate proxy settings for Axios, `fetch` and `node-fetch` libraries. Backend plugins using any of these libraries have nothing special to do to support corporate proxies.
We currently only support corporate proxy settings for the Axios, `fetch` and `node-fetch` libraries. Backend plugins using any of these libraries have nothing special to do to support corporate proxies.

Axios and `node-fetch` are supported with the proxy settings through the use of the [`global-agent`](https://github.com/gajus/global-agent#supported-libraries) package.
The native `fetch` library is supported with the proxy settings through the use of the Node's [`undici`](https://github.com/nodejs/undici) package.

# Logging

The following environment variables can be helpful when inspecting the behavior of the application with the proxy settings, for example to understand which requests are getting fetched through the proxy or directly bypassing the proxy.

- `ROARR_LOG`: setting it to `true` enables the `global-agent` logs. More details in https://github.com/gajus/global-agent#enable-logging
- `NODE_DEBUG`: setting it to `fetch` or `undici` enables debug statements for the native `fetch` calls. More details in https://github.com/nodejs/undici/blob/main/docs/docs/api/Debug.md

<details>

<summary>Example of logs</summary>

We can get an output like below with the following environment variables set:

- `NO_PROXY="localhost,.example.com"`
- `HTTP(S)_PROXY="http://proxy:3128"`
- `ROARR_LOG="true"`
- `NODE_DEBUG="fetch"`

```text
[...]
{"context":{"package":"global-agent","namespace":"createGlobalProxyAgent","logLevel":30,"configuration":{"environmentVariableNamespace":"","forceGlobalAgent":true,"socketConnectionTimeout":60
000},
"state":{
"HTTP_PROXY":"http://proxy:3128",
"HTTPS_PROXY":"http://proxy:3128",
"NO_PROXY":"localhost,.example.com"}
},
"message":"global agent has been initialized",
"sequence":3,"time":1731063427425,"version":"1.0.0"}
[...]
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"https://api.github.com/user",
"proxy":"http://proxy:3128","requestId":2},
"message":"proxying request",
"sequence":28,"time":1731063460170,"version":"1.0.0"}
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"http://10.10.10.105:8888/path/path1.yaml",
"proxy":"http://proxy:3128","requestId":1},
"message":"proxying request",
"sequence":23,"time":1731065107588,"version":"1.0.0"}
[...]
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"http://localhost:7007/api/catalog/.backstage/auth/v1/jwks.json"},
"message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY",
"sequence":4,"time":1731063026743,"version":"1.0.0"}
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,"destination":"https://example.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml"},
"message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY",
"sequence":5,"time":1731063027049,"version":"1.0.0"}
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"https://example.com:1234/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml"},
"message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY",
"sequence":6,"time":1731063027052,"version":"1.0.0"}
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"https://subdomain.example.com/path/path1.yaml"},
"message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY",
"sequence":7,"time":1731063027053,"version":"1.0.0"}
{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,
"destination":"https://subdomain2.example.com/path/path1.yaml"},
"message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY",
"sequence":8,"time":1731063027055,"version":"1.0.0"}
[...]
FETCH 2: connecting to httpbin.org using https:undefined
FETCH 2: connecting to proxy:3128:3128 using http:undefined
FETCH 2: connected to proxy:3128:3128 using http:h1
FETCH 2: sending request to CONNECT http://proxy:3128/httpbin.org:443
FETCH 2: trailers received from GET https://httpbin.org//anything
FETCH 2: connected to proxy:3128:3128 using http:h1
[...]
FETCH 16: connected to localhost:7007:7007 using http:h1
FETCH 16: sending request to GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json
FETCH 16: received response to GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json - HTTP 200
FETCH 16: trailers received from GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json
[...]
```

</details>


# Testing

The most challenging part of writing an end-to-end test from the context of a corporate proxy is to set up an environment where an application is forbidden access to the public Internet except through that proxy.

One possible approach is to simulate such an environment in a Kubernetes namespace with the help of [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to control ingress and egress traffic for pods within that namespace.
## Locally with `podman-compose` or `docker-compose`

You can leverage the [`rhdh-local`](https://github.com/redhat-developer/rhdh-local) project to test how a given Showcase/Red Hat Developer Hub container image would behave when it is running behind a corporate proxy server. You would need either [`podman-compose`](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) or [Docker Compose](https://docs.docker.com/compose/).

See [Testing RHDH in a simulated corporate proxy setup](https://github.com/redhat-developer/rhdh-local#testing-rhdh-in-a-simulated-corporate-proxy-setup) for more details.

## On a cluster

To do so:
This approach simulates a corporate proxy environment in a Kubernetes/OpenShift namespace with the help of [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to control ingress and egress traffic for pods within that namespace.

### Kubernetes

1. Make sure the network plugin in your Kubernetes cluster supports network policies. [k3d](https://k3d.io) for example supports Network Policies out of the box.

Expand Down Expand Up @@ -174,7 +269,7 @@ spec:
# --- TRUNCATED ---
```

# Testing on OpenShift
### OpenShift

2. Create a separate proxy project, and deploy a [Squid](https://www.squid-cache.org/)-based proxy application there. The full URL to access the proxy server from within the cluster would be `http://squid-service.proxy.svc.cluster.local:3128`.

Expand Down Expand Up @@ -322,3 +417,16 @@ spec:
- name: secrets-rhdh
# --- TRUNCATED ---
```



# External Resources

The way the proxy settings are supposed to be parsed and handled is unfortunately not codified in any standard, and might vary depending on the library, especially concerning the `NO_PROXY` handling.
See this nice article from GitLab highlighting some of the subtle differences that might cause issues: [We need to talk: Can we standardize NO_PROXY?](https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/).

For reference, the following resources can help understand how the packages we use here handle such settings:
- Undici's proxy agent tests: https://github.com/nodejs/undici/blob/v6.19.8/test/env-http-proxy-agent.js
- global-agent tests:
- https://github.com/gajus/global-agent/blob/master/test/global-agent/factories/createGlobalProxyAgent.ts
- and specifically on the `NO_PROXY` handling: https://github.com/gajus/global-agent/blob/master/test/global-agent/utilities/isUrlMatchingNoProxy.ts
77 changes: 3 additions & 74 deletions packages/backend/src/corporate-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { bootstrap } from 'global-agent';
import { Agent, Dispatcher, ProxyAgent, setGlobalDispatcher } from 'undici';
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';

/**
* Adds support for corporate proxy to both 'node-fetch' (using 'global-agent') and native 'fetch' (using 'undici') packages.
*
* Ref: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md
* Ref: https://gist.github.com/zicklag/1bb50db6c5138de347c224fda14286da (to support 'no_proxy')
*/
export function configureCorporateProxyAgent() {
// Bootstrap global-agent, which addresses node-fetch proxy-ing.
Expand All @@ -28,76 +27,6 @@ export function configureCorporateProxyAgent() {
// More details in https://github.com/gajus/global-agent#what-is-the-reason-global-agentbootstrap-does-not-use-http_proxy
bootstrap();

// Configure the undici package, which affects the native 'fetch'. It leverages the same env vars used by global-agent,
// or the more conventional HTTP(S)_PROXY ones.
const proxyEnv =
process.env.GLOBAL_AGENT_HTTP_PROXY ??
process.env.GLOBAL_AGENT_HTTPS_PROXY ??
process.env.HTTP_PROXY ??
process.env.http_proxy ??
process.env.HTTPS_PROXY ??
process.env.https_proxy;

if (proxyEnv) {
const proxyUrl = new URL(proxyEnv);

// Create an access token if the proxy requires authentication
let token: string | undefined = undefined;
if (proxyUrl.username && proxyUrl.password) {
const b64 = Buffer.from(
`${proxyUrl.username}:${proxyUrl.password}`,
).toString('base64');
token = `Basic ${b64}`;
}

// Create a default agent that will be used for no_proxy origins
const defaultAgent = new Agent();

// Create an interceptor that will use the appropriate agent based on the origin and the no_proxy
// environment variable.
// Collect the list of domains that we should not use a proxy for.
// The only wildcard available is a single * character, which matches all hosts, and effectively disables the proxy.
const noProxyEnv =
process.env.GLOBAL_AGENT_NO_PROXY ??
process.env.NO_PROXY ??
process.env.no_proxy;
const noProxyList = noProxyEnv?.split(',') || [];

const isNoProxy = (origin?: string): boolean => {
for (const exclusion of noProxyList) {
if (exclusion === '*') {
// Effectively disables proxying
return true;
}
// Matched as either a domain which contains the hostname, or the hostname itself.
if (origin === exclusion || origin?.endsWith(`.${exclusion}`)) {
return true;
}
}
return false;
};

const noProxyInterceptor = (
dispatch: Dispatcher['dispatch'],
): Dispatcher['dispatch'] => {
return (opts, handler) => {
return isNoProxy(opts.origin?.toString())
? defaultAgent.dispatch(opts, handler)
: dispatch(opts, handler);
};
};

// Create a proxy agent that will send all requests through the configured proxy, unless the
// noProxyInterceptor bypasses it.
const proxyAgent = new ProxyAgent({
uri: proxyUrl.protocol + proxyUrl.host,
token,
interceptors: {
Client: [noProxyInterceptor],
},
});

// Make sure our configured proxy agent is used for all `fetch()` requests globally.
setGlobalDispatcher(proxyAgent);
}
// Configure the undici package, which sets things up for the native 'fetch'.
setGlobalDispatcher(new EnvHttpProxyAgent());
}
7 changes: 4 additions & 3 deletions plugins/dynamic-home-page/docs/headline.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dynamicPlugins:
## Available props
| Prop | Default | Description |
| ------- | ------- | ----------- |
| `title` | none | Title |
| Prop | Default | Description |
| ------- | ------- | ------------------------------------------ |
| `title` | none | Title |
| `align` | `left` | Alignment like `left`, `center` or `right` |
Loading

0 comments on commit 93b4ab3

Please sign in to comment.