This guide walks through deploying the agentic controller, creating a Gateway with LLM credentials, defining an Agent, and creating an AgentRun to trigger execution.
- Kubernetes 1.33+ or OpenShift 4.20+ (the controller mounts skills via
the ImageVolume feature — a beta gate that is off by default on
Kubernetes 1.33–1.34, so enable
ImageVolumethere; it is on by default from 1.35 and GA in 1.36) - Agent Sandbox v1.0.x installed in the cluster
- Konveyor Hub installed in the cluster (step 2 covers this). Hub serves the agentic API the UI uses and resolves the repositories agents migrate; Hub-free runs are not supported yet (#122)
kubectlandhelmconfigured to talk to the cluster- LLM provider credentials (e.g. GCP Vertex AI, OpenAI, Anthropic, AWS Bedrock)
Everything agentic lives in the Hub namespace. Hub reads Agents, AgentRuns, Gateways, SkillCards and the other
konveyor.ioresources from its own namespace and nowhere else — there is no setting to point it elsewhere. Everykubectl applyandkubectl create secretin this guide therefore targets that namespace,konveyor-tackleby default, via$KONVEYOR_NS. Applying the Gateways or defaults into a different namespace (or leaving them in whatever your context happens to be) is the single most common way to end up with a UI that shows an error on every agentic page; see Troubleshooting.
The controller creates Agent Sandbox CRs to run agent workloads. Agent Sandbox must be installed before the controller can execute AgentRuns.
AGENT_SANDBOX_TAG=v1.0.0
# Clone and install via Helm
git clone --depth 1 --branch $AGENT_SANDBOX_TAG \
https://github.com/kubernetes-sigs/agent-sandbox.git /tmp/agent-sandbox
# The chart renders its own Namespace by default; disable it with
# namespace.create=false so it does not collide with --create-namespace.
helm install agent-sandbox /tmp/agent-sandbox/helm/ \
--namespace agent-sandbox-system \
--create-namespace \
--set namespace.create=false \
--set image.tag=$AGENT_SANDBOX_TAG
# Wait for the controller to be ready
kubectl wait deployment/agent-sandbox-controller \
--namespace agent-sandbox-system \
--for=condition=Available \
--timeout=120sNote: The clone +
helm installpath above and the upstream release manifest (kubectl apply -f .../<tag>/sandbox-with-extensions.yaml) are alternative install methods — use one, not both. Mixing them makes helm andkubectl applyfight over the same cluster-scoped CRDs, and backing out means deleting those CRDs (taking every Sandbox on the cluster with them). Note also that the release assets were renamed at v0.5.2 (manifest.yaml→sandbox.yaml), so pin a v0.5.2+ tag if you follow the release-manifest path.
Upgrading an existing cluster: Agent Sandbox v1.0.0 removes the legacy
v1alpha1API and its conversion webhooks, so you cannot jump straight to it from v0.4.x / early v0.5.x. Follow the upstream v1.0.0 API migration guide in order: (1) upgrade to a v0.5.x release; (2) run the v0.5 storage migration to move all resources tov1beta1and prune legacystoredVersions; (3) verify every agent-sandbox CRD reports only["v1beta1"]instatus.storedVersions(the apiserver rejects the upgrade otherwise); (4) upgrade to v1.0.0. Because Helm does not upgrade CRDs in a chart'scrds/directory, apply the v1beta1 CRDs (kubectl apply -f helm/crds/) beforehelm upgrade— otherwise the chart removes the conversion-webhook Service while the old CRDs still reference it. Fresh installs (the command above) are unaffected.
Future: OpenShell will replace the direct Agent Sandbox dependency. When integrated, the controller will provision sandboxes through the OpenShell gateway API instead of creating Sandbox CRs directly. See ADR 0004.
Hub is where the agentic resources live and what the UI talks to. Install
it before creating any Gateway or Agent so there is a namespace to put
them in. hack/install-konveyor.sh installs the tackle2-operator via its
Helm chart, waits for Hub, and grants Hub's ServiceAccount access to the
konveyor.io resources in its namespace (config/hub-rbac/). Operators
from konveyor/operator#614
(v0.11.0-alpha.4) on grant this themselves; the script pins an older commit,
and the grant is harmless next to a newer one:
export KONVEYOR_NS=konveyor-tackle # the script's default; change both together
make konveyor-install # add AUTH_REQUIRED=true to start with auth onAuthentication is off by default so the rest of this guide needs no credentials; Authentication below covers turning it on and what changes when you do.
Every command below uses -n "$KONVEYOR_NS". If you prefer, set it as
your context default instead and drop the flag:
kubectl config set-context --current --namespace="$KONVEYOR_NS"Already have a Hub? Skip the install but still grant the RBAC (needed for
operators before v0.11.0-alpha.4, harmless after). This is the one way to
apply config/hub-rbac/: the RoleBinding names the ServiceAccount's
namespace, which -n does not rewrite, so render and substitute:
kubectl kustomize config/hub-rbac/ \
| sed "s/konveyor-tackle/$KONVEYOR_NS/g" \
| kubectl apply -f -Operator path (no build, no kustomize). The tackle2-operator ships the agentic controller. One switch on the Tackle CR deploys the controller into the Hub namespace, installs the same
config/defaults/content there, and turns on the agentic pages in the UI:kubectl patch tackle tackle -n "$KONVEYOR_NS" --type merge \ -p '{"spec":{"agentic_enabled":true}}' kubectl get tackle tackle -n "$KONVEYOR_NS" \ -o jsonpath='{.status.conditions[?(@.type=="AgenticControllerReady")]}'Agent Sandbox (step 1) must already be installed or the operator posts
AgenticControllerReady=Falseand deploys nothing. If you take this path, skipmake deployandkubectl apply -k config/defaults/below — two controllers reconciling the same CRs is not a supported setup — and continue at Open the UI. The developer path below is for running your own build of the controller.
The default image quay.io/konveyor/agentic-controller:latest is public
and rebuilt on every merge to main, so you can deploy straight away
with no build step:
# Deploy CRDs, RBAC, and the controller manager
make deployTo build and push your own image instead (e.g. to test local changes),
set IMG to a registry you can push to:
export IMG=quay.io/<your-org>/agentic-controller:dev
make docker-build docker-push IMG=$IMG
make deploy IMG=$IMGThis creates the agentic-controller-system namespace and deploys
the controller. Verify it's running:
kubectl get pods -n agentic-controller-systemUpgrading a running controller: rerun
make deploy IMG=$IMGrather than restarting the pod or patching the Deployment's image. Two things are versioned with the image and onlymake deployre-applies them: the manager ClusterRole, and theSKILL_LOADER_IMAGEenv var (the image carrying/skill-loader, normally the controller's own — kustomize keeps it equal to the manager image). A barekubectl rollout restartonto a newer image crash-loops on the missing env var, and a stale ClusterRole shows up asFailed to watch ... forbiddenin the controller log. If you manage the Deployment outside kustomize, setSKILL_LOADER_IMAGEto the manager image yourself.
Deploy the default domain resources — the SkillCards and SkillCollection
that make up the skill catalog, plus the Agents and the
java-ee-to-quarkus AgentWorkflow the UI presents for users to run
(these are not included in make deploy to avoid name-prefix mangling
the cross-references between them). This is the same content the operator
installs on enable. They must land in the Hub namespace (see step 2) or
the UI cannot see them:
kubectl apply -k config/defaults/ -n "$KONVEYOR_NS"The shipped Agents declare no gateway, so they install and become Ready without a provider configured — you name a gateway when you run one (step 4 creates one). Inspect what was installed:
kubectl get skillcards,skillcollections,agents,agentworkflows -n "$KONVEYOR_NS"Samples vs. defaults.
config/defaults/holds the curated content above — installed automatically by the operator.config/samples/holds illustrative CRs you copy and edit (the Gateway samples in step 4, plus a standalone example Agent and AgentRun); nothing there is auto-installed.
To install only the CRDs without deploying the controller (e.g. for
local development with make run):
make installAlternatively, generate a single consolidated YAML containing CRDs and the controller deployment — useful when you don't want to build from source:
make build-installer IMG=$IMG
kubectl apply -f dist/install.yamlBefore creating anything else, confirm Hub can see what you just installed. This is the check that catches a wrong namespace or missing RBAC immediately instead of at the end of the guide. Through the Hub service:
kubectl -n "$KONVEYOR_NS" port-forward svc/tackle-hub 8080:8080 &
curl -s http://localhost:8080/agentic/agents | jq -r '.[].name'You should see the default Agents (migration-plan-agent,
migration-execute-agent, migration-verify-agent). A 500 with
is forbidden in the body means the
RBAC or the namespace is wrong — see
Troubleshooting. Add -u admin:admin once
authentication is on.
The operator exposes the UI as a Route named tackle on OpenShift
(openshift_cluster: true on the Tackle CR) or an Ingress named tackle
otherwise; on a Kind cluster without an ingress controller, port-forward
it:
kubectl -n "$KONVEYOR_NS" get route,ingress tackle 2>/dev/null
kubectl -n "$KONVEYOR_NS" port-forward svc/tackle-ui 8081:8080 # then http://localhost:8081The agentic pages (Agent runs, Agents, Workflows, Skills) appear only when
the UI runs with AGENTIC_ENABLED=true. The operator sets that together
with agentic_enabled, so on the operator path they are already there.
On the developer path the operator-managed UI keeps them hidden; run a
second UI Deployment of your own with AGENTIC_ENABLED=true and
TACKLE_HUB_URL=http://tackle-hub.$KONVEYOR_NS.svc:8080 (plus the
authentication variables in Authentication if auth is
on), or rely on the API check above.
A Gateway represents a single LLM provider/model combination with
credentials. Each Gateway serves exactly one model. The Gateway and the
Secret it references must both be in the Hub namespace — the controller
resolves credentialRef within the Gateway's own namespace, and Hub only
lists Gateways from its own.
Note: Gateway replaces the former
LLMProviderCRD. If you have existingLLMProviderresources, they must be recreated as Gateways — one Gateway per provider/model combination.
The Secret commands below use --from-literal for brevity, which
records the key value in your shell history. For anything beyond a
throwaway test cluster, prefer --from-file (reading the value from a
protected file) or an external secret manager.
Create a Secret with your GCP application default credentials:
gcloud auth application-default login
kubectl create secret generic vertex-credentials -n "$KONVEYOR_NS" \
--from-file=GOOGLE_APPLICATION_CREDENTIALS_JSON="$HOME/.config/gcloud/application_default_credentials.json" \
--from-literal=GCP_PROJECT_ID="$(gcloud config get-value project)" \
--from-literal=GCP_LOCATION=globalThe whole Secret is exposed to the agent via envFrom, so GCP_PROJECT_ID
and GCP_LOCATION ride along with the credentials file. goose's Vertex
provider requires GCP_PROJECT_ID (there is no default and the run
fails at first token without it). GCP_LOCATION is optional — goose
defaults to us-central1 — but global matches the sample Gateway
endpoint.
Apply the Gateway:
kubectl apply -f config/samples/gateway_vertex_ai.yaml -n "$KONVEYOR_NS"kubectl create secret generic openai-credentials -n "$KONVEYOR_NS" \
--from-literal=api-key="<your-openai-api-key>"
kubectl apply -f config/samples/gateway_openai.yaml -n "$KONVEYOR_NS"kubectl create secret generic anthropic-credentials -n "$KONVEYOR_NS" \
--from-literal=api-key="<your-anthropic-api-key>"
kubectl apply -f config/samples/gateway_anthropic.yaml -n "$KONVEYOR_NS"kubectl create secret generic bedrock-credentials -n "$KONVEYOR_NS" \
--from-literal=AWS_ACCESS_KEY_ID="<your-access-key-id>" \
--from-literal=AWS_SECRET_ACCESS_KEY="<your-secret-access-key>" \
--from-literal=AWS_REGION="us-east-1"
kubectl apply -f config/samples/gateway_aws_bedrock.yaml -n "$KONVEYOR_NS"AWS_REGION is what goose actually uses to reach Bedrock — the harness
derives the Bedrock endpoint from it and ignores the Gateway endpoint,
which only feeds the controller's connectivity check. Keep the Gateway
endpoint in the same region as AWS_REGION so that check stays
meaningful. The model's us. prefix is a cross-region inference
profile spanning the US regions (us-east-1/us-east-2/us-west-2), so
switching between US regions needs no model change — only moving to
another geo (eu., apac.) requires a new prefix.
kubectl create secret generic grok-credentials -n "$KONVEYOR_NS" \
--from-literal=api-key="<your-xai-api-key>"
kubectl apply -f config/samples/gateway_xai.yaml -n "$KONVEYOR_NS"Verify the Gateway is ready:
kubectl get gateways.konveyor.io -n "$KONVEYOR_NS"Note: Use the fully-qualified
gateways.konveyor.iorather than the baregateways. On any cluster with the Gateway API CRDs installed (OpenShift 4.19+ does this by default),gatewaysresolves togateways.gateway.networking.k8s.ioinstead, sokubectl get gatewayswould report no resources right after you applied your Gateway.
The Verified column shows whether the controller confirmed
connectivity to the endpoint.
An Agent is a template that declares what is available for execution: a container image, gateways, skills, a prompt, and typed parameters. Creating an Agent does not execute anything.
Note: The example
agent_example.yamlandagentrun_example.yamlreference the Vertex AI Gateway (gcp-vertex-ai) from Option A. If you created a different Gateway (Options B–D), update thespec.gateways[].refin the Agent and thespec.gatewayin the AgentRun to match your Gateway's name before applying them.
Verify the default SkillCards were deployed (from step 3):
kubectl get skillcards -n "$KONVEYOR_NS"Apply the example Agent:
kubectl apply -f config/samples/agent_example.yaml -n "$KONVEYOR_NS"Check that the Agent is ready (referenced Gateways and SkillCards must exist and be healthy):
kubectl get agents -n "$KONVEYOR_NS"An AgentRun triggers execution of an Agent. It references an Agent, selects a Gateway, carries task-specific instructions, and sets the environment the entry point needs. The controller validates the configuration, creates an Agent Sandbox, and tracks the run to completion.
The agent-java image resolves the repository to migrate and its git
credentials from the Hub installed in step 2, keyed by APP_ID, so the
application has to exist in Hub first. Register it through the UI
(Application inventory → Create new) or the API, using the Hub
port-forward from Open the UI:
curl -s -X POST http://localhost:8080/applications \
-H 'Content-Type: application/json' \
-d '{"name":"coolstore","repository":{"kind":"git","url":"https://github.com/konveyor-ecosystem/coolstore.git","branch":"main"}}'
# Find its id — this is the APP_ID the run needs
curl -s http://localhost:8080/applications | jq -r '.[] | "\(.id) \(.name)"'Private repositories also need a git credential (an Identity in Hub)
attached to the application. The sample AgentRun's spec.env points at
the in-cluster Hub service with APP_ID: "1"; edit HUB_BASE_URL,
APP_ID, and TARGET_BRANCH to match your Hub and application.
With authentication on, create runs through the UI or Hub's
POST /agentic/agentrunsrather thankubectl apply: Hub mints a per-run token the harness uses to reach Hub, and a run created straight in Kubernetes has none. See Authentication.
Apply the example AgentRun:
kubectl apply -f config/samples/agentrun_example.yaml -n "$KONVEYOR_NS"Watch the run:
kubectl get agentruns -n "$KONVEYOR_NS" -wOnce the phase moves to Running, the Sandbox pod is live. View
agent logs:
# Get the sandbox pod name from the AgentRun status
SANDBOX=$(kubectl get agentrun migration-run-001 -n "$KONVEYOR_NS" -o jsonpath='{.status.sandboxName}')
kubectl logs -f $SANDBOX -n "$KONVEYOR_NS"The AgentRun spec is immutable — to change values, delete the AgentRun and create a new one.
For multi-stage work (e.g. plan, execute, verify), use
AgentWorkflow and AgentWorkflowRun. See
hack/harness-test/workflow-resources.yaml for a complete example
that migrates a Java EE application to Quarkus using three stages.
Everything above runs with Hub authentication off. Turning it on is one
field on the Tackle CR; the operator restarts Hub and the UI with
AUTH_REQUIRED=true:
kubectl patch tackle tackle -n "$KONVEYOR_NS" --type merge \
-p '{"spec":{"feature_auth_required":true}}'
kubectl -n "$KONVEYOR_NS" rollout status deploy/tackle-hub deploy/tackle-ui(AUTH_REQUIRED=true make konveyor-install does the same on a fresh
install.) What that means in practice:
-
Hub is its own OpenID Connect provider. There is no Keycloak to install. The UI redirects to Hub's login page, and the issuer is derived from the request host, so any UI that proxies
/oidcto Hub works without registering redirect URIs. -
The seeded login is
admin/admin, frominternal/auth/seed/users.yamlin tackle2-hub. Change it before the cluster is reachable by anyone else; users and roles are managed under Hub's/authREST API and the UI's administration pages. -
Every Hub request now needs credentials. Anonymous calls return
401. Basic auth works for scripts, and the UI uses bearer tokens:curl -s -u admin:admin http://localhost:8080/agentic/agents | jq -r '.[].name'
-
Agentic endpoints are scoped. Each route group requires an
agentic.*scope (agentic.agents,agentic.agentruns, …), granted by role ininternal/auth/seed/roles.yaml. A user whose role lacks them gets403on the agentic pages while the rest of the UI works. -
Runs must be created through Hub. When Hub creates an AgentRun it also creates a per-run token Secret, attaches it via
envFrom, and setsHUB_BASE_URL, which is how the harness resolves the application and pushes results. A run created withkubectl applyhas no token and fails as soon as it talks to Hub. -
A UI Deployment you run yourself (developer path) needs
AUTH_REQUIRED=true,OIDC_CLIENT_ID=web-ui, andOIDC_ISSUER=http://tackle-hub.$KONVEYOR_NS.svc:8080/oidc. The UI server exits at startup withoutOIDC_ISSUER.
Samples (config/samples/) are illustrative CRs you copy and edit —
nothing here is auto-installed:
| File | Kind | Description |
|---|---|---|
gateway_vertex_ai.yaml |
Gateway | GCP Vertex AI with Claude |
gateway_openai.yaml |
Gateway | OpenAI GPT-4o |
gateway_anthropic.yaml |
Gateway | Anthropic direct API |
gateway_aws_bedrock.yaml |
Gateway | AWS Bedrock |
gateway_xai.yaml |
Gateway | xAI (Grok) |
agent_example.yaml |
Agent | Standalone Java migration agent |
agentrun_example.yaml |
AgentRun | Triggers the example agent |
Defaults (config/defaults/) are the curated content the operator
installs on enable — the skill catalog plus the Agents and AgentWorkflow
the UI runs. Apply them with kubectl apply -k config/defaults/ -n "$KONVEYOR_NS":
| File | Kind | Description |
|---|---|---|
skillcard_*.yaml |
SkillCard | Migration-stage skills (plan, execute, verify, javaee-to-quarkus, house-rules) |
skillcollection_java_migration.yaml |
SkillCollection | Grouped migration skills |
agent_migration_*.yaml |
Agent | Plan, execute, and verify stage agents (no gateway — named at run time) |
agentworkflow_javaee_to_quarkus.yaml |
AgentWorkflow | Three-stage Java EE → Quarkus workflow |
Run the controller locally against a cluster (CRDs must be installed):
make install # Install CRDs
make run # Run the controller from your hostThe project includes scripts for running the full stack in a Kind cluster:
make e2e-setup # Create Kind cluster + deploy Agent Sandbox + controller
make e2e-run # Run e2e tests
make e2e-cleanup # Tear down the Kind clusterkubectl delete --all acts on one namespace — keep the -n so you
don't remove resources you meant to keep:
# Delete runs, agents, and gateways in the Hub namespace
kubectl delete agentruns --all -n "$KONVEYOR_NS"
kubectl delete agents --all -n "$KONVEYOR_NS"
kubectl delete gateways.konveyor.io --all -n "$KONVEYOR_NS"
# Gateway verification Jobs (and their pods) are owned by the Gateway and
# go with it; sweep any left behind by a Gateway deleted another way
kubectl delete jobs -n "$KONVEYOR_NS" -l app.kubernetes.io/component=gateway-verificationWarning:
make undeployandmake uninstalldelete the CRDs, which are cluster-scoped. Deleting a CRD removes every custom resource of that type across all namespaces — not just the ones from this guide. Run these only if you intend to tear down the controller entirely.
# Undeploy the controller
make undeploy
# Or just uninstall CRDs
make uninstallThe tackle2-operator (OLM-packaged as konveyor/operator) ships the agentic
controller behind spec.agentic_enabled on the Tackle CR — see the
operator path in step 3. On every merge to main, the
sync-operator workflow renders this repo's CRDs, controller RBAC, and
config/defaults/ into a PR against the operator, so what the operator
installs is what this repo defines. Hub's ServiceAccount gets the
konveyor.io RBAC from the operator as of
konveyor/operator#614
(v0.11.0-alpha.4); config/hub-rbac/ here covers older operators. One thing
the operator does not do yet is resolve the Agent Sandbox dependency —
install it yourself (step 1). The kustomize / dist/install.yaml
path in step 3 remains the way to run a controller build the operator does
not ship.
UI agentic pages (Agent runs, Agents) show an error or never load; Hub returns 500
The UI calls Hub's /hub/agentic/* endpoints and Hub reads the
konveyor.io resources from its own namespace. A Hub log line like
agents.konveyor.io is forbidden: User "system:serviceaccount:konveyor-tackle:tackle-hub"
cannot list resource "agents" in API group "konveyor.io" in the namespace "konveyor-tackle"
means one or both of:
- Hub's ServiceAccount has no Role on
konveyor.ioresources in its namespace. Applyconfig/hub-rbac/there (step 2). If the Role exists but in a different namespace, it does nothing — the namespace in the error is the one that matters. - The Agents, Gateways, and credential Secrets were created in another
namespace (often whatever the kubectl context defaulted to). Hub cannot
see them from
konveyor-tackle. Recreate them in the Hub namespace with the-n "$KONVEYOR_NS"commands above; the cluster-scoped controller reconciles them wherever they are.
Confirm with kubectl auth can-i list agents.konveyor.io --as=system:serviceaccount:konveyor-tackle:tackle-hub -n konveyor-tackle
and kubectl get agents.konveyor.io -n konveyor-tackle.
Gateway shows Verified: false
The controller could not reach the endpoint. Check:
- The endpoint URL is correct
- The credential Secret exists and has the right keys
- Network policies allow egress from the controller namespace
For aws-bedrock and gcp-vertex-ai this is expected on controller
images older than #222:
the probe asks the endpoint for /v1/models, which only OpenAI-compatible
APIs serve, so the verification Job fails with a 404 and the Gateway parks
at Ready=False/ConnectionFailed, taking every Agent that references it
down with it. From #222 on, those providers are marked
Ready=True with reason=VerificationSkipped and Verified: false. On an
older image, mark the Gateway Ready by hand:
kubectl patch gateway.konveyor.io <name> -n "$KONVEYOR_NS" --subresource=status --type=merge \
-p '{"status":{"connectionVerified":true,"conditions":[{"type":"Ready","status":"True","reason":"ConnectionVerified","message":"Manually verified; /v1/models probe is invalid for this provider","lastTransitionTime":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}]}}'Hub returns 401 after enabling authentication
Every caller now needs credentials — see Authentication.
The usual culprits are scripts calling Hub without -u user:password or a
bearer token, and AgentRuns created with kubectl apply, whose harness
has no Hub token. Recreate those runs through the UI or Hub's API.
Agent shows Ready: False
The Agent references Gateways or SkillCards that don't exist or aren't ready. Check:
kubectl get gateways.konveyor.io -n "$KONVEYOR_NS"— all referenced gateways must existkubectl get skillcards -n "$KONVEYOR_NS"— all referenced skills must be resolved
AgentRun stuck in Pending
The controller is waiting for dependencies. Check:
- The referenced Agent is
Ready - The selected Gateway is in the Agent's gateway list
- Agent Sandbox is installed and healthy (step 1)
- Controller logs:
kubectl logs -n agentic-controller-system deploy/agentic-controller-controller-manager