Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
strategy:
fail-fast: false
matrix:
component: [access, mcp, ephemeral, tenancy]
component: [access, mcp, ephemeral, tenancy, marketplace]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1
Expand All @@ -97,7 +97,7 @@ jobs:
- name: Run e2e tests
run: make test-e2e-${{ matrix.component }}
env:
# Only the ephemeral and tenancy harnesses read this. Set the KCP_IMAGE
# Only the ephemeral, tenancy and marketplace harnesses read this. Set the KCP_IMAGE
# repository variable to pin a tag when kcp main is broken and the
# noise is not useful.
KCP_IMAGE: ${{ vars.KCP_IMAGE || 'ghcr.io/kcp-dev/kcp:main' }}
2 changes: 2 additions & 0 deletions .github/workflows/images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
description: kcp ephemeral resources virtual workspace (ephemeral-virtual-workspace, endpointslice-controller, example-webhook)
- component: tenancy-vw
description: kcp tenancy component (tenancy-vw server with init, operator and virtualworkspace subcommands, plus the tenancyctl client)
- component: marketplace-vw
description: kcp marketplace component (marketplace-vw server with init, operator and virtualworkspace subcommands)
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ cover.out
.local/
pki/
.dex/
.tilt/

# Kubeconfigs the README walkthrough writes (`tenancyctl kubeconfig -o ...`).
*.kubeconfig

# Serial files `openssl x509 -CAcreateserial` leaves beside wherever it ran.
*.srl

# Binaries built into component roots by `go build ./cmd/...`. They shadow
# bin/ on tab-completion and go stale silently; use `make build`.
/ephemeral/ephemeral-virtual-workspace
Expand All @@ -26,3 +30,10 @@ pki/

# Self-signed serving certs a VW generates when none are supplied.
apiserver.local.config/
/marketplace/bin/

# The web UI's build output and dependencies. Running vitest from the repo
# root also leaves a node_modules/ cache there.
node_modules/
ui/dist/
*.tsbuildinfo
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,14 @@ FROM gcr.io/distroless/static:nonroot AS tenancy-vw
COPY --from=build-tenancy /out/ /
USER 65532:65532
ENTRYPOINT ["/tenancy-vw"]

# ── marketplace ──────────────────────────────────────────────────────
FROM builder AS build-marketplace
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /out/marketplace-vw ./marketplace/

FROM gcr.io/distroless/static:nonroot AS marketplace-vw
COPY --from=build-marketplace /out/ /
USER 65532:65532
ENTRYPOINT ["/marketplace-vw"]
42 changes: 37 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ help: ## Show available targets
# ── Build ────────────────────────────────────────────────────────────

.PHONY: build
build: build-access build-mcp build-ephemeral build-tenancy ## Build every binary into bin/
build: build-access build-mcp build-ephemeral build-tenancy build-marketplace build-cli ## Build every binary into bin/

.PHONY: build-access
build-access: ## Build the access virtual workspace binaries
Expand Down Expand Up @@ -68,16 +68,20 @@ lint: ## Run golangci-lint (configuration in .golangci.yml)
.PHONY: vet
vet: ## Run go vet, including the e2e build tag so the tests cannot rot
$(GO) vet ./...
$(GO) vet -tags e2e ./test/... ./access/test/... ./mcp/test/... ./ephemeral/test/... ./tenancy/test/...
$(GO) vet -tags e2e ./test/... ./access/test/... ./mcp/test/... ./ephemeral/test/... ./tenancy/test/... ./marketplace/test/...

.PHONY: tidy
tidy: ## Sync go.mod / go.sum
$(GO) mod tidy

.PHONY: verify
verify: verify-gofmt vet verify-fork-pin verify-e2e-manifests ## Run every verification check
verify: verify-gofmt vet verify-fork-pin verify-e2e-manifests verify-producer-quickstart ## Run every verification check
$(GO) mod tidy -diff

.PHONY: verify-producer-quickstart
verify-producer-quickstart: ## Build and test the producer-quickstart template, which is a Go module of its own
cd producer-quickstart && $(GO) mod tidy -diff && $(GO) vet ./... && $(GO) build ./... && $(GO) test ./...

.PHONY: verify-gofmt
verify-gofmt: ## Check gofmt -s cleanliness
@out=$$(gofmt -l -s $$(find . -name '*.go' -not -path './$(BIN_DIR)/*')); \
Expand All @@ -95,7 +99,7 @@ verify-fork-pin: ## Check the kcp Kubernetes fork pin matches virtual-workspace-
# Each component owns its e2e harness; these are the entry points CI uses.

.PHONY: test-e2e
test-e2e: test-e2e-access test-e2e-mcp test-e2e-ephemeral test-e2e-tenancy ## Run every component's e2e tests in sequence
test-e2e: test-e2e-access test-e2e-mcp test-e2e-ephemeral test-e2e-tenancy test-e2e-marketplace ## Run every component's e2e tests in sequence

.PHONY: test-e2e-access
test-e2e-access: ## Run the access VW e2e tests (throwaway kind cluster via kcp-operator)
Expand All @@ -113,14 +117,34 @@ test-e2e-ephemeral: ## Run the ephemeral VW e2e tests (local processes against a
test-e2e-tenancy: ## Run the tenancy e2e tests (local processes against a real kcp)
cd tenancy && ./hack/ci/run-e2e-tests.sh

.PHONY: generate
generate: ## Regenerate the APIResourceSchemas from the sdk Go types (controller-gen + kcp apigen)
./hack/update-apiresourceschemas.sh

.PHONY: build-cli
build-cli: ## Build contribctl, the repo-wide CLI bundling every component's client
$(GO) build -o $(BIN_DIR)/contribctl ./cmd/cli

.PHONY: build-marketplace
build-marketplace: ## Build the marketplace server binary
$(GO) build -o $(BIN_DIR)/marketplace-vw ./marketplace

.PHONY: test-spike-marketplace
test-spike-marketplace: ## Prove the marketplace design's binder composition against a real kcp
./marketplace/hack/ci/run-spike.sh

.PHONY: test-e2e-marketplace
test-e2e-marketplace: ## Run the marketplace e2e tests (local processes against a real kcp)
cd marketplace && ./hack/ci/run-e2e-tests.sh

# ── Images ───────────────────────────────────────────────────────────
# One Dockerfile, one shared builder stage, three image targets.

IMAGE_PREFIX ?= ghcr.io/kcp-dev/contrib-virtual-workspaces
IMAGE_TAG ?= latest

.PHONY: images
images: image-access image-mcp image-ephemeral image-tenancy ## Build every container image
images: image-access image-mcp image-ephemeral image-tenancy image-marketplace ## Build every container image

.PHONY: image-access
image-access: ## Build the access-vw container image
Expand All @@ -138,6 +162,14 @@ image-ephemeral: ## Build the ephemeral-vw container image
image-tenancy: ## Build the tenancy-vw container image
docker build --target tenancy-vw -t $(IMAGE_PREFIX)/tenancy-vw:$(IMAGE_TAG) .

.PHONY: image-marketplace
image-marketplace: ## Build the marketplace-vw container image
docker build --target marketplace-vw -t $(IMAGE_PREFIX)/marketplace-vw:$(IMAGE_TAG) .

.PHONY: tilt-up
tilt-up: ## Bring up kcp (from $(KCP_DIR), default ../kcp) plus this repo's components under Tilt
./hack/tilt/up.sh

.PHONY: clean
clean: ## Remove build artifacts
rm -rf $(BIN_DIR)
93 changes: 91 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# contrib-virtual-workspaces

Contrib virtual workspaces for [kcp](https://github.com/kcp-dev/kcp).
Virtual workspaces for [kcp](https://github.com/kcp-dev/kcp), and the
platform built out of two of them.

**kcp Platform** is [tenancy/](tenancy/) and [marketplace/](marketplace/)
together — tenants, projects and memberships, plus a catalog of offerings
to enable in them — served under the `*.platform.kcp.io` API groups, with
[ui/](ui/) as its console. The other three components are standalone
virtual workspaces that stand on their own.

| Component | What it serves | Image |
|---|---|---|
| [access/](access/) | **SelfClusterAccessReview** — "which workspaces can this user access?" in one call | `ghcr.io/kcp-dev/contrib-virtual-workspaces/access-vw` |
| [mcp/](mcp/) | [Model Context Protocol](https://modelcontextprotocol.io) as a virtual workspace, scoped to what the caller can see (uses the access VW) | `ghcr.io/kcp-dev/contrib-virtual-workspaces/mcp-vw` |
| [ephemeral/](ephemeral/) | Non-persisted resources: `POST` in, a provider webhook answers, nothing reaches etcd | `ghcr.io/kcp-dev/contrib-virtual-workspaces/ephemeral-vw` |
| [ephemeral/](ephemeral/) | Non-persisted resources: `POST` in, a producer webhook answers, nothing reaches etcd | `ghcr.io/kcp-dev/contrib-virtual-workspaces/ephemeral-vw` |
| [tenancy/](tenancy/) | Tenants, projects and memberships as kcp workspaces and RBAC, plus **SelfTenancyReview** — "which tenants am I in?" — and the `tenancyctl` client | `ghcr.io/kcp-dev/contrib-virtual-workspaces/tenancy-vw` |
| [marketplace/](marketplace/) | Producers and offerings: a read-only catalog, where enabling is a plain kcp `APIBinding` the consumer authors themselves | `ghcr.io/kcp-dev/contrib-virtual-workspaces/marketplace-vw` |

Each component's README documents its design, deployment and limits.

Expand Down Expand Up @@ -35,6 +43,87 @@ make images # all three container images
make image-mcp # or per component
```

## One CLI

Every component that ships a client exposes it as importable cobra
commands, and `contribctl` (built by `make build-cli` into `bin/`)
arranges them around what a person does rather than around which
component implements it:

```sh
contribctl login --issuer https://dex.example.com --url https://kcp.example.com:8443
contribctl use # pick a tenant and project
contribctl tenants # what you can reach
contribctl offers # what you could enable
contribctl enable acme.things # enable it here
contribctl membership add alice@example.com --role admin
```

The user-facing commands need no kubeconfig at all — the virtual
workspaces authenticate the caller and answer for the identity they hold.
`contribctl admin` is the other side, writing to kcp with a kubeconfig.
`login --url` remembers the server in `~/.kube/contribctl.json`, so
nothing after it has to be told where kcp is.

The standalone binaries (`tenancyctl`, `scar-to-kubeconfig`) remain the
same code under their own names.

## One UI

[ui/](ui/) is a web console over exactly the same surfaces: sign in with
the same identity provider, see your tenants and projects, browse the
catalog and enable from it, and look at what a workspace serves. It is
static files talking to the front-proxy with the user's own token — no
backend of ours, nothing privileged, and so it can never show more than
kcp would answer for that person.

`make tilt-up` starts it with the rest of the stack, on
http://localhost:5173; `cd ui && npm install && npm run dev` runs it
against a stack that is already up.

## Building a producer

[producer-quickstart/](producer-quickstart/) is a producer ready to copy: a Go type, the
schema generated from it, a controller that reconciles it in every workspace
that enabled it, the instructions the console shows, a Dockerfile, a
deployment manifest and a CI workflow. Copy the directory, rename the
group, kind and producer, replace the fake provisioning in the reconciler,
and `make publish` puts it on the catalog. It is its own Go module so the
copy builds alone; `make verify` here builds and tests it so it cannot rot.

## Development with Tilt

`make tilt-up` (or `hack/tilt/up.sh`) brings up the whole stack in one
place: kcp itself comes from the kcp checkout's own Tilt setup
([contrib/tilt](https://github.com/kcp-dev/kcp/tree/main/contrib/tilt) —
operator, shards, front-proxy behind https://kcp.localhost:8443,
observability, dashboard), included unchanged by the repo
[Tiltfile](Tiltfile), and this repo's components run on top as
live-reloading local processes.

- `KCP_DIR` points at the kcp clone (default `../kcp`).
- Admin kubeconfigs land in `.tilt/` (`tilt-frontproxy.kubeconfig`, per
shard), extracted by kcp's own tooling.
- Each component's virtual workspace is deployed **in the cluster** by the
kcp-operator (a `VirtualWorkspace` object: Deployment, Service, serving
certificate, and the component's own `init` as an init container) and
routed by the front-proxy, so everything answers on one address —
`https://kcp.localhost:8443`, kcp under `/clusters/` and each virtual
workspace under `/services/<name>`. The operators run as local
processes, which is where the fast iteration is.
- `TENANCY_ENABLED=false` / `MARKETPLACE_ENABLED=false` skip a component.
- Dex runs in the same kind cluster as the shared OIDC issuer
(`https://dex.kcp.localhost:8443/dex`), trusted by kcp and both virtual
workspaces, so a token names the same user everywhere. The environment
is seeded with a demo tenant and project, so `whoami` has something to
say right after login. `DEX_ENABLED=false` / `SEED_ENABLED=false` turn
either off.

[hack/tilt/README.md](hack/tilt/README.md) walks the stack end to end:
bringing it up, logging in with Dex, and an ordinary user going from a
password to a working kubeconfig for exactly the workspaces they were
granted.

## Testing

An identity provider is shared by every component: [hack/dex/](hack/dex/)
Expand Down
Loading
Loading