diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a81648f..a67cf62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 ./... diff --git a/.golangci.yml b/.golangci.yml index e209cbc..9efda5b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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. diff --git a/Dockerfile b/Dockerfile index 62129fa..5a2dad5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 / diff --git a/README.md b/README.md index 0fc5c85..34cee42 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/example/arbitrary-uid/README.md b/example/arbitrary-uid/README.md new file mode 100644 index 0000000..0efbaad --- /dev/null +++ b/example/arbitrary-uid/README.md @@ -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 + ``` diff --git a/example/arbitrary-uid/composition.yaml b/example/arbitrary-uid/composition.yaml new file mode 100644 index 0000000..bba9c88 --- /dev/null +++ b/example/arbitrary-uid/composition.yaml @@ -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 diff --git a/example/arbitrary-uid/definition.yaml b/example/arbitrary-uid/definition.yaml new file mode 100644 index 0000000..42f2bc1 --- /dev/null +++ b/example/arbitrary-uid/definition.yaml @@ -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 diff --git a/example/arbitrary-uid/functions.yaml b/example/arbitrary-uid/functions.yaml new file mode 100644 index 0000000..1fe78a0 --- /dev/null +++ b/example/arbitrary-uid/functions.yaml @@ -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 diff --git a/example/arbitrary-uid/xr.yaml b/example/arbitrary-uid/xr.yaml new file mode 100644 index 0000000..bee367b --- /dev/null +++ b/example/arbitrary-uid/xr.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: example.crossplane.io/v1 +kind: ArbitraryUID +metadata: + name: example-arbitrary-uid +spec: + crossplane: + compositionRef: + name: arbitrary-uid-example diff --git a/input/v1alpha1/parameters.go b/input/v1alpha1/parameters.go index 81445d1..5a06bf4 100644 --- a/input/v1alpha1/parameters.go +++ b/input/v1alpha1/parameters.go @@ -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 @@ -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 diff --git a/package/input/template.fn.crossplane.io_parameters.yaml b/package/input/template.fn.crossplane.io_parameters.yaml index d19e627..cc5eba5 100644 --- a/package/input/template.fn.crossplane.io_parameters.yaml +++ b/package/input/template.fn.crossplane.io_parameters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.21.0 name: parameters.template.fn.crossplane.io spec: group: template.fn.crossplane.io @@ -19,7 +19,7 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Input can be used to provide input to this Function. + description: Parameters can be used to provide input to this Function. properties: apiVersion: description: |- @@ -54,9 +54,11 @@ spec: shellEnvVars: description: shellEnvVars items: + description: ShellEnvVar is a Shell Environment Variable of the form + key=value. properties: fieldRef: - description: FieldRef is a reference to a field in the Composition + description: FieldRef is a reference to a field in the Composition. properties: defaultValue: description: DefaultValue when Policy is Optional and field @@ -79,13 +81,18 @@ spec: - path type: object key: + description: Key is the Environment Variable key like API_KEY type: string type: - description: ShellEnvVarType is a type of ShellEnvVar + description: 'Type is the type of ShellEnVar: Value, ValueRef, FieldRef.' type: string value: + description: Value is a fixed value, like http://api.example.com type: string valueRef: + description: |- + ValueRef retrieves a Environment Variable value from a composite field. + Can result in error if field is not set: use FieldRef which can handle missing fields. type: string type: object type: array diff --git a/scripts/nss-wrapper-entrypoint.sh b/scripts/nss-wrapper-entrypoint.sh new file mode 100644 index 0000000..9317d93 --- /dev/null +++ b/scripts/nss-wrapper-entrypoint.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# nss-wrapper-entrypoint.sh +# +# This entrypoint script enables running the function as an arbitrary UID +# (common in OpenShift and restricted Kubernetes environments). +# +# When a container runs as an arbitrary UID that doesn't exist in /etc/passwd, +# many tools (aws cli, git, ssh, etc.) fail because they can't look up the +# current user. nss_wrapper intercepts these lookups and provides a fake +# passwd entry. +# +# Usage: Configure your DeploymentRuntimeConfig to use this entrypoint: +# +# containers: +# - name: package-runtime +# command: ["/scripts/nss-wrapper-entrypoint.sh"] +# args: ["/function"] +# + +set -e + +[ "$#" -eq 0 ] && { echo "usage: $0 [args...]" >&2; exit 64; } + +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +# Pick a writable HOME: the assigned UID may not own the home directory that +# /etc/passwd (or the image's ENV HOME) points at. +if [ ! -w "${HOME:-/}" ]; then + for candidate in /tmp/home /tmp; do + if mkdir -p "$candidate" 2>/dev/null && [ -w "$candidate" ]; then + export HOME="$candidate" + break + fi + done + [ -w "$HOME" ] || echo "warning: no writable HOME found ($HOME)" >&2 +fi + +# Enable nss_wrapper only when the current UID has no passwd entry. +if ! getent passwd "$USER_ID" >/dev/null 2>&1 && [ -w "$HOME" ]; then + export NSS_WRAPPER_PASSWD=/tmp/passwd + export NSS_WRAPPER_GROUP=/tmp/group + + cat /etc/passwd > "$NSS_WRAPPER_PASSWD" + echo "runner:x:${USER_ID}:${GROUP_ID}:Function Runner:${HOME}:/usr/sbin/nologin" >> "$NSS_WRAPPER_PASSWD" + + cat /etc/group > "$NSS_WRAPPER_GROUP" + if ! grep -q "^[^:]*:[^:]*:${GROUP_ID}:" "$NSS_WRAPPER_GROUP"; then + echo "runner:x:${GROUP_ID}:" >> "$NSS_WRAPPER_GROUP" + fi + + export LD_PRELOAD=libnss_wrapper.so +fi + +exec "$@"