Skip to content

Commit

Permalink
Merge pull request #1 from humoflife/bootstrap
Browse files Browse the repository at this point in the history
initial commit
  • Loading branch information
jbw976 authored Apr 5, 2024
2 parents 4c0fcee + 20063a7 commit f969d8e
Show file tree
Hide file tree
Showing 27 changed files with 1,564 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@

# Go workspace file
go.work

kubeconfig
function-shell
function-amd64.xpkg
function-arm64.xpkg
59 changes: 59 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# syntax=docker/dockerfile:1

# We use the latest Go 1.x version unless asked to use something else.
# The GitHub Actions CI job sets this argument for a consistent Go version.
ARG GO_VERSION=1

# Setup the base environment. The BUILDPLATFORM is set automatically by Docker.
# The --platform=${BUILDPLATFORM} flag tells Docker to build the function using
# the OS and architecture of the host running the build, not the OS and
# architecture that we're building the function for.
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build

RUN apt-get update && apt-get install -y jq unzip zsh

# TODO: Install awscli, gcloud
# RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip" && \
# unzip "/tmp/awscliv2.zip" && \
# ./aws/install

WORKDIR /fn

# Most functions don't want or need CGo support, so we disable it.
ENV CGO_ENABLED=0

# We run go mod download in a separate step so that we can cache its results.
# This lets us avoid re-downloading modules if we don't need to. The type=target
# mount tells Docker to mount the current directory read-only in the WORKDIR.
# The type=cache mount tells Docker to cache the Go modules cache across builds.
RUN --mount=target=. --mount=type=cache,target=/go/pkg/mod go mod download

# The TARGETOS and TARGETARCH args are set by docker. We set GOOS and GOARCH to
# these values to ask Go to compile a binary for these architectures. If
# TARGETOS and TARGETOS are different from BUILDPLATFORM, Go will cross compile
# for us (e.g. compile a linux/amd64 binary on a linux/arm64 build machine).
ARG TARGETOS
ARG TARGETARCH

# Build the function binary. The type=target mount tells Docker to mount the
# current directory read-only in the WORKDIR. The type=cache mount tells Docker
# to cache the Go modules cache across builds.
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /function .

# Produce the Function image. We use a very lightweight 'distroless' image that
# does not include any of the build tools used in previous stages.
#FROM gcr.io/distroless/base-debian11 AS image
FROM gcr.io/distroless/python3-debian12 AS image
WORKDIR /
COPY --from=build /bin /bin
COPY --from=build /etc /etc
COPY --from=build /lib /lib
COPY --from=build /tmp /tmp
COPY --from=build /usr /usr
COPY --from=build /function /function
EXPOSE 9443
USER nonroot:nonroot
ENTRYPOINT ["/function"]
52 changes: 52 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
REPO_URL="xpkg.upbound.io/upbound/function-shell"
VERSION_TAG="v0.0.3"
#PACKAGE_FILES="function-amd64.xpkg,function-arm64.xpkg"
PACKAGE_FILES="function-arm64.xpkg"

help: ## Print help for targets with comments
@printf "For more targets and info see comments in Makefile.\n\n"
@grep -E '^[a-zA-Z0-9._-]+:.*## .*$$' Makefile | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'

#all: docker-build-amd64 docker-build-arm64 xpkg-build-amd64 xpkg-build-arm64 xpkg-push
all: docker-build-amd64 docker-build-arm64 xpkg-build-arm64 xpkg-push

docker-build-amd64: ## Build AMD64 Docker Image
docker build . --quiet --platform=linux/amd64 --tag runtime-amd64

docker-build-arm64: ## Build ARM64 Docker Image
docker build . --quiet --platform=linux/arm64 --tag runtime-arm64

xpkg-build-amd64: ## Build AMD64 Composition Function XPKG
crossplane xpkg build \
--package-root=package \
--embed-runtime-image=runtime-amd64 \
--package-file=function-amd64.xpkg

xpkg-build-arm64: ## Build ARM64 Composition Function XPKG
crossplane xpkg build \
--package-root=package \
--embed-runtime-image=runtime-arm64 \
--package-file=function-arm64.xpkg

xpkg-push: ## Push XPKG Package Files, Requires Upbound login
up xpkg push ${REPO_URL}:${VERSION_TAG} -f ${PACKAGE_FILES}

lint: ## Lint the Code
golangci-lint run

fn-build: ## Build Function Code
go generate ./...
go build .

test: ## Run Code Tests
go test -v -cover .

render: ## Render Examples, Requires make debug first
crossplane beta render \
example/out-of-cluster/xr.yaml \
example/out-of-cluster/composition.yaml \
example/out-of-cluster/functions.yaml

debug: ## Run Shell Function For Rendering Examples
go run . --insecure --debug
9 changes: 9 additions & 0 deletions NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
To get started:

1. Replace `function-template-go` with your function in `go.mod`,
`package/crossplane.yaml`, and any Go imports. (You can also do this
automatically by running the `./init.sh <function-name>` script.)
2. Update `input/v1beta1/` to reflect your desired input (and run `go generate`)
3. Add your logic to `RunFunction` in `fn.go`
4. Add tests for your logic in `fn_test.go`
5. Update `README.md`, to be about your function!
222 changes: 222 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# function-shell

This Crossplane composition function accepts commands to run in a shell and it
returns the output to specified fields. It accepts the following paramereters:
- `shellEnvVarsSecretRef` - referencing environment variables in a
Kubernetes secret. shellEnvVarsSecretRef requires a `name`, a
`namespace` and a `key` for the secret. Inside of it, the shell
expects a JSON structure with key value environment variables. Example:

```
{
"ENV_FOO": "foo value",
"ENV_BAR": "bar value"
}
```
- 'shellEnvVars' - an array of environment variables with a <b>key</b> and <b>value</b>
each.
- 'shellCommand' - a shell command line that can contain pipes and
redirects and calling multiple programs.
- 'stdoutField' - the path to the field where the shell standard output
should be written.
- 'stderrField' - the path to the field where the shell standard error
output should be written.

## Practical Example: Obtain Dashboard Ids from Datadog

The composition calls the `function-shell` instructing it to obtain dashboard ids
from a Datadog account. For this, it specifies the location of a Kubernetes
secret where the `DATADOG_API_KEY` and `DATADOG_APP_KEY` environment variable values
are stored. The Datadog API endpoint is passed in a clear text environment
variable. The shell command uses a `curl` to the endpoint with a header that
contains the access credentials. The command output is piped into jq and
filtered for the ids.

The `function-shell` writes the dashboard ids to the specified output status field, and
any output that went to stderr into the specified stderr status field.

The composition is for illustration purposes only. When using the
`function-shell` in your own compositions, you may want to patch function input
from claim and other composition field values.

Note: `function-shell` has to receive permissions in form of a rolebinding to
read secrets and perform other actions that may be prohibited by default. Below
is a `clusterrolebinding` that will work, but you should exercise appropriate caution when
setting function permissions.

```
#!/bin/bash
SA=$(kubectl -n upbound-system get sa -o name | grep function-shell | sed -e 's|serviceaccount\/|upbound-system:|g')
kubectl create clusterrolebinding function-shell-admin-binding --clusterrole cluster-admin --serviceaccount="${SA}"
```

```yaml
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: shell.upbound.io
spec:
compositeTypeRef:
apiVersion: upbound.io/v1alpha1
kind: XShell
mode: Pipeline
pipeline:
- step: shell
functionRef:
name: function-shell
input:
apiVersion: shell.fn.crossplane.io/v1beta1
kind: Parameters
shellEnvVarsSecretRef:
name: datadog-secret
namespace: upbound-system
key: credentials
shellEnvVars:
- key: DATADOG_API_URL
value: "https://api.datadoghq.com/api/v1/dashboard"
shellCommand: |
curl -X GET "${DATADOG_API_URL}" \
-H "Accept: application/json" \
-H "DD-API-KEY: ${DATADOG_API_KEY}" \
-H "DD-APPLICATION-KEY: ${DATADOG_APP_KEY}"|\
jq '.dashboards[] .id';
stdoutField: status.atFunction.shell.stdout
stderrField: status.atFunction.shell.stderr
```
The composition is called through the following `claim`.

```
---
apiVersion: upbound.io/v1alpha1
kind: Shell
metadata:
name: shell-1
spec: {}
```
The API definition is as follows. Note that the API contains status fields that
are populated by `function-shell`.
```
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xshells.upbound.io
spec:
group: upbound.io
names:
kind: XShell
plural: xshells
claimNames:
kind: Shell
plural: shells
defaultCompositionRef:
name: shell.upbound.io
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
cmd:
type: string
status:
properties:
atFunction:
type: object
x-kubernetes-preserve-unknown-fields: true
```
The `crossplane beta trace` output after applying the in-cluster
shell-claim.yaml is as follows:
```
cbt shell.upbound.io/shell-1
NAME SYNCED READY STATUS
Shell/shell-1 (default) True True Available
└─ XShell/shell-1-ttfbh True True Available
```
The `XShell/shell-1-ttfbh` yaml output looks as per below. Notice the dashboard
ids in the `status.atFunction.shell.stdout` field, and the `curl` stderr output
in the `status.atFunction.shell.stderr` field.
```
apiVersion: upbound.io/v1alpha1
kind: XShell
metadata:
creationTimestamp: "2024-04-05T04:47:03Z"
finalizers:
- composite.apiextensions.crossplane.io
generateName: shell-1-
generation: 3
labels:
crossplane.io/claim-name: shell-1
crossplane.io/claim-namespace: default
crossplane.io/composite: shell-1-ttfbh
name: shell-1-ttfbh
resourceVersion: "2181275"
uid: 9ebda770-bab3-4822-bd36-739cea5cd35e
spec:
claimRef:
apiVersion: upbound.io/v1alpha1
kind: Shell
name: shell-1
namespace: default
compositionRef:
name: shell.upbound.io
compositionRevisionRef:
name: shell.upbound.io-ed28247
compositionUpdatePolicy: Automatic
resourceRefs: []
status:
atFunction:
shell:
stderr: "% Total % Received % Xferd Average Speed Time Time Time
\ Current\n Dload Upload Total Spent
\ Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:--
--:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:--
--:--:-- --:--:-- 0\r100 4255 100 4255 0 0 9081 0 --:--:--
--:--:-- --:--:-- 9072"
stdout: |-
"vn4-agn-ftd"
"9pt-bhb-uwj"
"6su-nff-222"
"sm3-cxs-q98"
"ssx-sci-uvi"
"3fd-h4e-7w6"
"qth-94z-ip5"
conditions:
- lastTransitionTime: "2024-04-05T04:47:29Z"
reason: ReconcileSuccess
status: "True"
type: Synced
- lastTransitionTime: "2024-04-05T04:47:29Z"
reason: Available
status: "True"
type: Ready
```
# Run code generation - see input/generate.go
$ go generate ./...
# Run tests - see fn_test.go
$ go test ./...
# Build the function's runtime image - see Dockerfile
$ docker build . --tag=runtime
# Build a function package - see package/crossplane.yaml
$ crossplane xpkg build -f package --embed-runtime-image=runtime
```

[functions]: https://docs.crossplane.io/latest/concepts/composition-functions
[go]: https://go.dev
[function guide]: https://docs.crossplane.io/knowledge-base/guides/write-a-composition-function-in-go
[package docs]: https://pkg.go.dev/github.com/crossplane/function-sdk-go
[docker]: https://www.docker.com
[cli]: https://docs.crossplane.io/latest/cli
Loading

0 comments on commit f969d8e

Please sign in to comment.