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
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pki/
# 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 @@ -29,6 +32,8 @@ pki/
apiserver.local.config/
/marketplace/bin/

# The web UI's build output and dependencies.
ui/node_modules/
# 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,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 Down
24 changes: 16 additions & 8 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,8 @@ if DEX_ENABLED:
if str(local('grep -qs dex.kcp.localhost /etc/hosts && echo yes || echo no', quiet=True)).strip() == 'no':
print('WARNING: dex.kcp.localhost missing from /etc/hosts — add it to the 127.0.0.1 line for login to work')

print('dex → %s (alice@team-a.example.com / bob@team-b.example.com / mallory@strangers.example.com, password "password")' % DEX_ISSUER)
print('login: bin/contribctl tenancy login --issuer %s --certificate-authority %s/dex/ca.crt' % (DEX_ISSUER, TILT_DIR))
print('dex → %s (alice@team-a.example.com / bob@team-b.example.com / mallory@strangers.example.com / admin@platform.example.com, password "password")' % DEX_ISSUER)
print('login: bin/contribctl login --issuer %s --certificate-authority %s/dex/ca.crt' % (DEX_ISSUER, TILT_DIR))
print('walkthrough: hack/tilt/README.md')
print('JWT usernames arrive as the plain email, e.g. alice@team-a.example.com')

Expand Down Expand Up @@ -524,8 +524,10 @@ def component(name, binary, operator_args):
serve_cmd='%s operator --kubeconfig %s %s --v=4' % (bin_path, KUBECONFIG, operator_args),
deps=[bin_path],
# The exports it reconciles are installed by the virtual
# workspace's init container.
resource_deps=['%s-build' % name, '%s-vw' % name, 'contrib-kubeconfig'],
# workspace's init container. ingress-forward is only ready once
# kcp answers on the host, and the operator exits if it cannot
# reach kcp at startup.
resource_deps=['%s-build' % name, '%s-vw' % name, 'contrib-kubeconfig', 'ingress-forward'],
labels=[name],
)

Expand Down Expand Up @@ -593,9 +595,15 @@ if enabled('MCP'):
print('mcp VW → %s/services/mcp' % FRONT_PROXY_URL)
print(' publish it to the catalog: tilt trigger mcp-offering')

# The dex user who runs the platform: init binds it to the platform-admin
# role in the tenancy store and the marketplace catalog, which is what
# makes the console's Admin page appear for it.
PLATFORM_ADMIN = 'admin@platform.example.com'
PLATFORM_ADMIN_INIT_ARGS = ' - --platform-admin-user=%s\n' % PLATFORM_ADMIN

if enabled('TENANCY'):
virtual_workspace('tenancy', 'tenancy-vw', 'tenancy',
init_args='',
init_args=PLATFORM_ADMIN_INIT_ARGS,
# No --workspace-path: the server kubeconfig is already scoped to
# root:tenancy:controllers by targetWorkspace, and retargeting it
# would append a second /clusters/ segment.
Expand All @@ -611,14 +619,14 @@ if enabled('TENANCY'):
'--tenants-workspace root:tenancy:tenants',
)
# Demo data, so a fresh environment answers whoami with something:
# the component's own `tenancy seed` command converges on tenant acme
# the component's own `admin seed` command converges on tenant acme
# + project web, alice admin / bob view — each granted under both
# identities (dex email and dev-cert CN). Idempotent; re-run anytime
# with `tilt trigger tenancy-seed`. SEED_ENABLED=false skips it.
if os.getenv('SEED_ENABLED', 'true').lower() not in ('false', '0', 'no', 'off'):
local_resource('tenancy-seed',
cmd='make --no-print-directory build-cli && ' +
'bin/contribctl tenancy seed --kubeconfig ' + KUBECONFIG +
'bin/contribctl admin seed --kubeconfig ' + KUBECONFIG +
' --tenant acme --display-name "Acme Corp" --project web' +
' --admin alice@team-a.example.com --admin alice' +
' --view bob@team-b.example.com --view bob',
Expand All @@ -629,7 +637,7 @@ if enabled('TENANCY'):

if enabled('MARKETPLACE'):
virtual_workspace('marketplace', 'marketplace-vw', 'marketplace',
init_args='',
init_args=PLATFORM_ADMIN_INIT_ARGS,
vw_args=''.join([' - %s\n' % a for a in OIDC_VW_ARGS_LIST]),
)
component('marketplace', 'marketplace-vw',
Expand Down
3 changes: 1 addition & 2 deletions access/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ func Run(ctx context.Context, o *Options) error {
if err != nil && !errors.Is(err, context.Canceled) {
return fmt.Errorf("rbac provider failed: %w", err)
}
<-serveErr
return nil
return <-serveErr
case err := <-serveErr:
return err
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,16 @@ kubeconfig and are for whoever runs the platform.`,
}

// marketplaceDefaults is the other piece of glue: the server `login --url`
// stored, and how to verify it, handed to the marketplace commands as
// their flag defaults. The marketplace never reads the context file; it
// only knows it was told an address.
// stored, how to verify it, and the token `login` cached, handed to the
// marketplace commands as their flag defaults. The marketplace never reads
// the context file or the token cache; it only knows what it was told.
func marketplaceDefaults() marketplacecli.ServerDefaults {
stored := clicontext.Load()
return marketplacecli.ServerDefaults{
URL: stored.ServiceURL("marketplace"),
CertificateAuthority: stored.CertificateAuthority,
Insecure: stored.Insecure,
Token: tenancycli.CachedToken(),
}
}

Expand Down
15 changes: 11 additions & 4 deletions hack/tilt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,21 @@ fills it.
## 2. Log in with Dex

Dex is the one identity provider kcp and both virtual workspaces trust, so
a token names the same user everywhere. Three users, all with the password
a token names the same user everywhere. Four users, all with the password
`password`:

| Login | Identity everywhere |
|---|---|
| `alice@team-a.example.com` | `alice@team-a.example.com` |
| `bob@team-b.example.com` | `bob@team-b.example.com` |
| `mallory@strangers.example.com` | `mallory@strangers.example.com` |
| `admin@platform.example.com` | `admin@platform.example.com` |

`admin` is the platform administrator: the tenancy and marketplace init
steps run with `--platform-admin-user=admin@platform.example.com`, which
binds it to a role that may manage Tenants in `root:tenancy:store` and
Producers in `root:marketplace:catalog`, and nothing else. It is the only
user the console shows the Admin page to.

The identity is the **plain email, with no groups**: the kcp-operator only
speaks the legacy `--oidc-*` claim mapping, so the CEL claim mapping used
Expand Down Expand Up @@ -461,9 +468,9 @@ metadata:
# The offering is the export: these annotations publish it, and its own
# permissionClaims (none here) are the consent form.
annotations:
marketplace.platform.kcp.io/publish: "true"
marketplace.platform.kcp.io/display-name: "Things as a service"
marketplace.platform.kcp.io/description: "One Thing per workspace, batteries included."
marketplace.kcp.hub/publish: "true"
marketplace.kcp.hub/display-name: "Things as a service"
marketplace.kcp.hub/description: "One Thing per workspace, batteries included."
spec:
resources:
- name: things
Expand Down
9 changes: 7 additions & 2 deletions hack/tilt/dex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# https://dex.kcp.localhost:8443/dex both from the host (the ingress
# port-forward) and from the kcp pods (hostAliases -> the envoy gateway).
#
# The identities mirror hack/dex/dex.sh: alice/bob/mallory, one shared
# password ("password"), a public client "kcp" for PKCE and the password
# The identities mirror hack/dex/dex.sh: alice/bob/mallory, plus admin,
# the platform administrator the Tiltfile grants the platform-admin role
# to. One shared password ("password"), a public client "kcp" for PKCE and the password
# grant. With kcp's operator-side claim mapping (usernameClaim: email, no
# prefix) the resulting kcp username is the plain email,
# e.g. alice@team-a.example.com.
Expand Down Expand Up @@ -116,6 +117,10 @@ data:
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: mallory
userID: "3"
- email: admin@platform.example.com
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: admin
userID: "4"
---
apiVersion: apps/v1
kind: Deployment
Expand Down
19 changes: 13 additions & 6 deletions marketplace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ bin/marketplace-vw init --kubeconfig "$KUBECONFIG"
This creates the three workspaces from [the model](#the-model) and nothing
below them. Idempotent: run it twice and the second run is a no-op.

`--platform-admin-user` and `--platform-admin-group` (both repeatable) name
who may register Producers: they are bound to
`marketplace.platform.kcp.io:platform-admin` in the catalog, a role that
may enter it and manage Producers and nothing else. The binding holds
exactly the identities of the latest run, so dropping one from the flags
revokes it.

### 5. Run the operator

```sh
Expand Down Expand Up @@ -238,9 +245,9 @@ metadata:
name: things.acme.example
# These three annotations are the offering. There is no other object.
annotations:
marketplace.platform.kcp.io/publish: "true"
marketplace.platform.kcp.io/display-name: "Things as a service"
marketplace.platform.kcp.io/description: "One Thing per workspace, batteries included."
marketplace.kcp.hub/publish: "true"
marketplace.kcp.hub/display-name: "Things as a service"
marketplace.kcp.hub/description: "One Thing per workspace, batteries included."
spec:
resources:
- name: things
Expand Down Expand Up @@ -273,7 +280,7 @@ with it, and the export and everyone already bound to it are untouched:

```sh
kubectl --server "$PRODUCER" annotate apiexport things.acme.example \
marketplace.platform.kcp.io/publish-
marketplace.kcp.hub/publish-
```

### 8. Create a consumer workspace and enable
Expand Down Expand Up @@ -467,8 +474,8 @@ the APIExport:
```yaml
metadata:
annotations:
marketplace.platform.kcp.io/instructions: things-instructions # a ConfigMap, same workspace
marketplace.platform.kcp.io/docs-url: https://docs.example/things
marketplace.kcp.hub/instructions: things-instructions # a ConfigMap, same workspace
marketplace.kcp.hub/docs-url: https://docs.example/things
```

The ConfigMap's keys are sections, shown as tabs; a leading `NN-` orders
Expand Down
13 changes: 12 additions & 1 deletion marketplace/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func newInitCommand() *cobra.Command {
workspaceType string
serverUsers []string
serverGroups []string
adminUsers []string
adminGroups []string
timeout time.Duration
)

Expand Down Expand Up @@ -109,7 +111,11 @@ created. Idempotent: safe to run on every pod start and every upgrade.`,
if err := bootstrap.BindCatalog(ctx, catalogCfg, result); err != nil {
return fmt.Errorf("bind the Provider API into %s: %w", catalogPath, err)
}
logger.Info("catalog ready", "workspace", catalogPath)
if err := bootstrap.GrantPlatformAdmins(ctx, catalogCfg, adminUsers, adminGroups); err != nil {
return fmt.Errorf("grant platform admins in %s: %w", catalogPath, err)
}
logger.Info("catalog ready", "workspace", catalogPath,
"platformAdminUsers", adminUsers, "platformAdminGroups", adminGroups)
}
if producersWorkspace != "" {
producersPath, err = accessbootstrap.JoinWorkspacePath(workspacePrefix, producersWorkspace)
Expand Down Expand Up @@ -183,6 +189,11 @@ created. Idempotent: safe to run on every pod start and every upgrade.`,
"or the virtual workspace does not run as this bootstrap's own credential.")
cmd.Flags().StringSliceVar(&serverGroups, "server-group", nil,
"Additional Group to grant the server role to, repeatable.")
cmd.Flags().StringSliceVar(&adminUsers, "platform-admin-user", nil,
"User who may manage Producers in the catalog workspace, repeatable. Bound to a role "+
"that allows exactly that; anyone left off the list on a later run loses it.")
cmd.Flags().StringSliceVar(&adminGroups, "platform-admin-group", nil,
"Group, as the identity provider asserts it, whose members may manage Producers, repeatable.")
cmd.Flags().DurationVar(&timeout, "timeout", 3*time.Minute,
"Overall time budget for installing and verifying.")

Expand Down
6 changes: 3 additions & 3 deletions marketplace/config/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ such kind. The annotations on the `APIExport` are the offering:
```yaml
metadata:
annotations:
marketplace.platform.kcp.io/publish: "true"
marketplace.platform.kcp.io/display-name: Things as a service
marketplace.platform.kcp.io/description: One Thing per workspace, batteries included.
marketplace.kcp.hub/publish: "true"
marketplace.kcp.hub/display-name: Things as a service
marketplace.kcp.hub/description: One Thing per workspace, batteries included.
```

Only the exact string `"true"` publishes; anything else leaves the export
Expand Down
8 changes: 4 additions & 4 deletions marketplace/config/examples/things.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ metadata:
# Only the exact string "true" publishes. Anything else leaves the
# export off the catalog, which is the safe direction: being on it is
# what grants `bind` to strangers.
marketplace.platform.kcp.io/publish: "true"
marketplace.platform.kcp.io/display-name: Things as a service
marketplace.platform.kcp.io/instructions: things-instructions
marketplace.platform.kcp.io/description: One Thing per workspace, batteries included.
marketplace.kcp.hub/publish: "true"
marketplace.kcp.hub/display-name: Things as a service
marketplace.kcp.hub/instructions: things-instructions
marketplace.kcp.hub/description: One Thing per workspace, batteries included.
spec:
resources:
- name: things
Expand Down
6 changes: 3 additions & 3 deletions marketplace/config/examples/widgets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ kind: APIExport
metadata:
name: widgets.acme.example
annotations:
marketplace.platform.kcp.io/publish: "true"
marketplace.platform.kcp.io/display-name: Widgets
marketplace.platform.kcp.io/description: A widget kind, and nothing else.
marketplace.kcp.hub/publish: "true"
marketplace.kcp.hub/display-name: Widgets
marketplace.kcp.hub/description: A widget kind, and nothing else.
spec:
resources:
- name: widgets
Expand Down
Loading
Loading