Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}

- name: Check Generated Code
run: |
go generate ./...
git diff --exit-code || (echo "Generated code is out of date. Run 'go generate ./...' and commit the changes." && exit 1)

- name: Run Unit Tests
run: go test -v -cover ./...

Expand Down
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 && chmod 777 /.aws && chown 65532:65532 /scripts
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
103 changes: 103 additions & 0 deletions example/arbitrary-uid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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

Note: Local testing with `crossplane beta render` runs the function on your host machine
without the container's user configuration. The nss-wrapper functionality and arbitrary UID
handling can only be fully tested in a Kubernetes cluster (see "Deploying to a Cluster" below).

If you want to test the function logic locally:

1. Start the function in development mode:

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

2. In another terminal, render the example (this will use your host user, not UID 2000):

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

## 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 arbitraryuid example-arbitrary-uid -o yaml
```

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

```yaml
apiVersion: example.crossplane.io/v1
kind: ArbitraryUID
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: ArbitraryUID
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: arbitraryuids.example.crossplane.io
spec:
group: example.crossplane.io
names:
kind: ArbitraryUID
plural: arbitraryuids
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
52 changes: 52 additions & 0 deletions example/arbitrary-uid/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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:
securityContext:
runAsNonRoot: true
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
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
9 changes: 9 additions & 0 deletions example/arbitrary-uid/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
apiVersion: example.crossplane.io/v1
kind: ArbitraryUID
metadata:
name: example-arbitrary-uid
spec:
crossplane:
compositionRef:
name: arbitrary-uid-example
7 changes: 4 additions & 3 deletions input/v1alpha1/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (
// +kubebuilder:storageversion
// +kubebuilder:resource:categories=crossplane
type Parameters struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

// shellEnvVarsRef
// +optional
Expand Down Expand Up @@ -94,7 +95,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