Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ linters:
- nilnil
- funcorder

# Deprecated linters that have been replaced with newer versions.
- gomodguard # Replaced by gomodguard_v2 (deprecated since v2.12.0)

# Below are linters that lint for things we don't value. Each entry below
# this line must have a comment explaining the rationale.

Expand Down
11 changes: 7 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ RUN --mount=target=. \

# Produce the Function image.
FROM python:3.14-bookworm AS image
RUN apt-get update && apt-get install -y coreutils curl jq unzip zsh less
RUN apt-get update && apt-get install -y coreutils curl jq unzip zsh less libnss-wrapper
RUN groupadd -g 65532 nonroot
RUN useradd -u 65532 -g 65532 -d /home/nonroot --system --shell /usr/sbin/nologin nonroot
RUN mkdir /scripts /.aws && chown 65532:65532 /scripts /.aws
RUN useradd -u 65532 -g 65532 -d /home/nonroot --create-home nonroot
RUN mkdir /scripts /.aws && chown 65532:65532 /scripts /.aws
COPY scripts/nss-wrapper-entrypoint.sh /scripts/nss-wrapper-entrypoint.sh
RUN chmod +x /scripts/nss-wrapper-entrypoint.sh

# Download platform-specific AWS CLI binaries
ARG TARGETPLATFORM
Expand All @@ -55,7 +57,8 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"; \
fi && \
unzip "/tmp/awscliv2.zip" && \
./aws/install
./aws/install && \
rm -rf /aws /tmp/awscliv2.zip

WORKDIR /

Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ returns the output to specified fields.
- [Parameters](#parameters)
- [Error Handling and Output Capture](#error-handling-and-output-capture)
- [Caching Function Outputs](#caching-function-outputs)
- [Running as Arbitrary User IDs](#running-as-arbitrary-user-ids)
- [Examples](#examples)
- [Development and Test](#development-and-test)

Expand Down Expand Up @@ -153,10 +154,69 @@ set a new cache duration for the output.

See the echo [composition.yaml](example/echo/composition.yaml) for an example.

## Running as Arbitrary User IDs

Some Kubernetes environments (OpenShift, restricted Pod Security Standards) force containers to run as arbitrary user IDs that don't exist in `/etc/passwd`. This can cause tools like AWS CLI, git, and ssh to fail because they can't look up the current user.

The function-shell image includes `nss-wrapper` support to handle this scenario. When enabled via a `DeploymentRuntimeConfig`, the entrypoint script creates a fake passwd/group entry for the running UID, allowing tools that require user lookup to function correctly.

### Enabling nss-wrapper

Configure your `DeploymentRuntimeConfig` to use the nss-wrapper entrypoint:

```yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-shell-arbitrary-uid
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
containers:
- name: package-runtime
# Override the entrypoint to use nss-wrapper
command: ["/scripts/nss-wrapper-entrypoint.sh"]
args: ["/function"]
securityContext:
allowPrivilegeEscalation: false
privileged: false
runAsNonRoot: true
runAsUser: 2000
runAsGroup: 2000
```

Reference this config in your Function:

```yaml
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-shell
spec:
package: xpkg.upbound.io/crossplane-contrib/function-shell:v0.7.0
runtimeConfigRef:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
name: function-shell-arbitrary-uid
```

The nss-wrapper entrypoint:

- Creates a home directory at `/tmp/home` (writable by any user)
- Generates passwd/group entries for the arbitrary UID/GID
- Sets `HOME` environment variable appropriately
- Only activates when running as a UID other than 0 (root) or 65532 (default nonroot user)

See the [arbitrary-uid example](example/arbitrary-uid/) for a complete configuration.

## Examples

This repository includes the following examples in the `example/` directory:

- **[arbitrary-uid](example/arbitrary-uid/)** - Running as arbitrary user IDs (OpenShift/restricted environments)
- **[echo](example/echo/)** - Basic shell command execution with output capture
- **[datadog-dashboard-ids](example/datadog-dashboard-ids/)** - API integration with secret management
- **[fieldRef](example/fieldRef/)** - Using field references for dynamic values
Expand Down
118 changes: 118 additions & 0 deletions example/arbitrary-uid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Arbitrary UID Example

This example demonstrates how to run function-shell as an arbitrary user ID,
which is common in OpenShift and other restricted Kubernetes environments.

## Problem

Some Kubernetes environments force containers to run as arbitrary user IDs
that don't exist in `/etc/passwd`. This causes tools like AWS CLI, git, and
ssh to fail because they can't look up the current user.

## Solution

The function-shell image includes `nss-wrapper` support. When enabled via
a `DeploymentRuntimeConfig`, the entrypoint script creates fake passwd/group
entries for the running UID/GID, allowing tools that require user lookup to
function correctly.

## Files

- `functions.yaml` - Function and DeploymentRuntimeConfig with nss-wrapper enabled
- `composition.yaml` - Example composition that prints user info (HOME, UID, GID, whoami)
- `xr.yaml` - Example composite resource

## Testing Locally

1. Start the function in development mode:

```shell
go run . --insecure --debug
```

2. In another terminal, render the example:

```shell
crossplane beta render \
example/arbitrary-uid/xr.yaml \
example/arbitrary-uid/composition.yaml \
example/arbitrary-uid/functions.yaml
```

## Expected Output

The composition runs a shell command that outputs:

- `HOME` environment variable (should be `/tmp/home` for arbitrary UIDs)
- Current user ID and group ID
- Output of `whoami` (should be `runner` for arbitrary UIDs)

Example output in `status.atFunction.shell.stdout`:

```yaml
status:
atFunction:
shell:
stderr: ""
stdout: |-
HOME=/tmp/home
uid=2000(runner) gid=2000(runner) groups=2000(runner)
whoami=runner
```

## Deploying to a Cluster

1. Install the function with the DeploymentRuntimeConfig:

```shell
kubectl apply -f example/arbitrary-uid/functions.yaml
```

2. Apply the CompositeResourceDefinition and Composition:

```shell
kubectl apply -f example/arbitrary-uid/definition.yaml
kubectl apply -f example/arbitrary-uid/composition.yaml
```

3. Create a composite resource to trigger the composition:

```shell
kubectl apply -f example/arbitrary-uid/xr.yaml
```

4. Check the composite resource status:

```shell
kubectl get xr example-arbitrary-uid -o yaml
```

The output should show the function successfully ran with the arbitrary UID:

```yaml
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-arbitrary-uid
spec:
crossplane:
compositionRef:
name: arbitrary-uid-example
status:
atFunction:
shell:
stderr: ""
stdout: |-
HOME=/tmp/home
uid=2000(runner) gid=2000(runner) groups=2000(runner)
whoami=runner
conditions:
- lastTransitionTime: "2026-07-27T17:45:59Z"
reason: ReconcileSuccess
status: "True"
type: Synced
- lastTransitionTime: "2026-07-27T17:45:59Z"
reason: Available
status: "True"
type: Ready
```
22 changes: 22 additions & 0 deletions example/arbitrary-uid/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: arbitrary-uid-example
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: shell
functionRef:
name: function-shell
input:
apiVersion: shell.fn.crossplane.io/v1alpha1
kind: Parameters
shellCommand: |
echo "HOME=${HOME}"
echo "$(id)"
echo "whoami=$(whoami)"
stdoutField: status.atFunction.shell.stdout
stderrField: status.atFunction.shell.stderr
27 changes: 27 additions & 0 deletions example/arbitrary-uid/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: xrs.example.crossplane.io
spec:
group: example.crossplane.io
names:
kind: XR
plural: xrs
scope: Cluster
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties: {}
status:
type: object
properties:
atFunction:
type: object
x-kubernetes-preserve-unknown-fields: true
46 changes: 46 additions & 0 deletions example/arbitrary-uid/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Example: Running function-shell as an arbitrary UID
#
# Some Kubernetes environments (OpenShift, restricted Pod Security Standards)
# force containers to run as arbitrary user IDs that don't exist in /etc/passwd.
# This can cause tools like AWS CLI, git, and ssh to fail.
#
# This example shows how to use the nss-wrapper entrypoint to handle arbitrary UIDs.
# The entrypoint script creates a fake passwd entry for the running UID, allowing
# tools that require user lookup to function correctly.
#
---
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-shell
spec:
package: xpkg.upbound.io/crossplane-contrib/function-shell:v0.7.0
runtimeConfigRef:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
name: function-shell-arbitrary-uid
---
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-shell-arbitrary-uid
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
containers:
- name: package-runtime
# Override the entrypoint to use nss-wrapper
# This enables running as arbitrary UIDs
command: ["/scripts/nss-wrapper-entrypoint.sh"]
args: ["/function"]
securityContext:
allowPrivilegeEscalation: false
privileged: false
runAsNonRoot: true
# Example: OpenShift may assign a UID like 1000580000
# The nss-wrapper entrypoint will handle this automatically
runAsUser: 2000
runAsGroup: 2000
6 changes: 6 additions & 0 deletions example/arbitrary-uid/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
apiVersion: example.crossplane.io/v1
kind: XR
metadata:
name: example-arbitrary-uid
spec: {}
2 changes: 1 addition & 1 deletion input/v1alpha1/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (sev *ShellEnvVar) GetType() ShellEnvVarType {
return sev.Type
}

// ShellEnvVarsRef refers to an environment variable or secret leaded into
// ShellEnvVarsRef refers to an environment variable or secret loaded into
// the function pod.
type ShellEnvVarsRef struct {
// The Key whose value is the secret
Expand Down
Loading
Loading