<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kupe Cloud</title>
    <description>The latest articles on DEV Community by Kupe Cloud (kupecloud).</description>
    <link>https://dev.to/kupecloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14549%2F8f09b9fa-2603-4150-9447-9bdda93863bc.png</url>
      <title>DEV Community: Kupe Cloud</title>
      <link>https://dev.to/kupecloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kupecloud"/>
    <language>en</language>
    <item>
      <title>Create a Kubernetes cluster from the command line | Kupe Cloud</title>
      <dc:creator>Billy Walker</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:02:19 +0000</pubDate>
      <link>https://dev.to/kupecloud/create-a-kubernetes-cluster-from-the-command-line-kupe-cloud-2d72</link>
      <guid>https://dev.to/kupecloud/create-a-kubernetes-cluster-from-the-command-line-kupe-cloud-2d72</guid>
      <description>&lt;p&gt;Getting a Kubernetes cluster shouldn't involve a wizard, a downloaded file and a kubeconfig you'll have lost by Friday. With the Kupe CLI it's four commands, and the last one is &lt;code&gt;kubectl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe auth login
kupe cluster create dev &lt;span class="nt"&gt;--cpu-limit&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory-limit&lt;/span&gt; 8Gi &lt;span class="nt"&gt;--storage-limit&lt;/span&gt; 50Gi
kupe cluster kubeconfig dev &lt;span class="nt"&gt;--merge&lt;/span&gt; &lt;span class="nt"&gt;--exec&lt;/span&gt;
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post walks through what each of those does, why we built the CLI the way we did, and how the same commands hold up when a CI job runs them with nobody watching. If you already use &lt;code&gt;kubectl&lt;/code&gt;, most of it should feel familiar. That was the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and sign in
&lt;/h2&gt;

&lt;p&gt;You'll need a Kupe Cloud account first, and &lt;a href="https://kupe.cloud/signup" rel="noopener noreferrer"&gt;sign-up is open&lt;/a&gt; during the alpha.&lt;/p&gt;

&lt;p&gt;The quickest install is Homebrew (&lt;code&gt;brew install kupecloud/tap/kupe&lt;/code&gt;) or the install script (&lt;code&gt;curl -fsSL https://get.kupe.cloud | sh&lt;/code&gt;). The &lt;a href="https://docs.kupe.cloud/platform/kupe-cli/" rel="noopener noreferrer"&gt;CLI docs&lt;/a&gt; cover Windows and every other route.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kupe auth login&lt;/code&gt; uses the OAuth device flow, the same pattern a smart TV uses when it shows you a code and tells you to finish on your phone. The CLI prints a short code and a URL, you approve in any browser, and the CLI collects the tokens. Your password never passes through it, and because there's no local web server involved it behaves the same over SSH or in a dev container. Tokens live in your operating system's keyring where one exists, and refresh themselves on later commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a cluster
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe cluster create dev &lt;span class="nt"&gt;--cpu-limit&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory-limit&lt;/span&gt; 8Gi &lt;span class="nt"&gt;--storage-limit&lt;/span&gt; 50Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three limits have no defaults. They cap what workloads in the cluster can use and count against your plan's pool; &lt;code&gt;kupe plan list&lt;/code&gt; shows what your plan allows.&lt;/p&gt;

&lt;p&gt;Underneath, cluster creation is asynchronous. The API accepts the request straight away and the cluster moves through phases (&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;provisioning&lt;/code&gt;, &lt;code&gt;running&lt;/code&gt;) while the platform builds it. The CLI watches that phase and only returns once the cluster is &lt;code&gt;running&lt;/code&gt;, so the next line of a script can assume the cluster exists. If you'd rather have your prompt back immediately, pass &lt;code&gt;--wait=false&lt;/code&gt; and come back later with &lt;code&gt;kupe cluster wait dev --for running&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect kubectl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe cluster kubeconfig dev &lt;span class="nt"&gt;--merge&lt;/span&gt; &lt;span class="nt"&gt;--exec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--merge&lt;/code&gt;, the command prints a kubeconfig to stdout for you to redirect into a file. With it, the cluster is added to the kubeconfig you already use, under a context named &lt;code&gt;kupe-&amp;lt;tenant&amp;gt;-&amp;lt;cluster&amp;gt;&lt;/code&gt;, and becomes your current context, so there's no second file to keep track of.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--exec&lt;/code&gt; flag changes what goes in that kubeconfig. By default it embeds your token, which makes the file a secret and means it stops working when the token does. With &lt;code&gt;--exec&lt;/code&gt; there is no token in the file at all. Instead, the file tells &lt;code&gt;kubectl&lt;/code&gt; to run &lt;code&gt;kupe auth get-token&lt;/code&gt; whenever it needs a credential, using Kubernetes' standard &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/" rel="noopener noreferrer"&gt;exec credential plugin&lt;/a&gt; mechanism (hence the flag name). The AWS, Google and Azure CLIs connect &lt;code&gt;kubectl&lt;/code&gt; to their managed clusters the same way.&lt;/p&gt;

&lt;p&gt;The trade-off is that &lt;code&gt;kubectl&lt;/code&gt; now needs &lt;code&gt;kupe&lt;/code&gt; on its &lt;code&gt;PATH&lt;/code&gt;. That suits a laptop. For a throwaway container where you can't count on the binary being there, leave &lt;code&gt;--exec&lt;/code&gt; off and treat the kubeconfig as the credential it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it looks like kubectl (mostly)
&lt;/h2&gt;

&lt;p&gt;Our design notes start from two rules. The first is that a developer at a terminal and a CI job run the same commands, and the binary adapts to which one it is. Spinners, colour and confirmation prompts only appear on a terminal. What the CLI writes to stdout is the same either way, with progress and errors kept on stderr, so &lt;code&gt;kupe cluster list -o json | jq&lt;/code&gt; never finds a spinner in its input.&lt;/p&gt;

&lt;p&gt;The second rule is that we shouldn't surprise &lt;code&gt;kubectl&lt;/code&gt; users, since you're probably switching between the two all day. So &lt;code&gt;-o&lt;/code&gt; takes &lt;code&gt;table&lt;/code&gt;, &lt;code&gt;wide&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;yaml&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;go-template=...&lt;/code&gt;, and exit codes and error shapes follow kubectl wherever it has a convention.&lt;/p&gt;

&lt;p&gt;We deliberately broke from &lt;code&gt;kubectl&lt;/code&gt; in a few places, and each time there was an alternative we weighed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Noun first.&lt;/strong&gt; &lt;code&gt;kubectl&lt;/code&gt; puts the verb first (&lt;code&gt;kubectl get pods&lt;/code&gt;) because it's a generic client for dozens of resource kinds that all share the same verbs. Kupe has a handful of nouns and you'll type &lt;code&gt;kupe cluster &amp;lt;tab&amp;gt;&lt;/code&gt; far more often than &lt;code&gt;kupe &amp;lt;verb&amp;gt;&lt;/code&gt;, so &lt;code&gt;kupe cluster create&lt;/code&gt; gives completion a smaller list to offer. &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;fly&lt;/code&gt; and &lt;code&gt;hcloud&lt;/code&gt; read the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-o json&lt;/code&gt;, not &lt;code&gt;--json&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;gh&lt;/code&gt; and &lt;code&gt;fly&lt;/code&gt; use a &lt;code&gt;--json&lt;/code&gt; flag. We kept kubectl's &lt;code&gt;-o&lt;/code&gt; because &lt;code&gt;yaml&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;wide&lt;/code&gt; and &lt;code&gt;go-template&lt;/code&gt; all belong under one umbrella.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our own printer.&lt;/strong&gt; We looked at reusing the printing library from Kubernetes itself, but it expects Kubernetes objects. Our resources are plain structs, so we wrote a small printer instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contexts keep you honest
&lt;/h2&gt;

&lt;p&gt;Sign in to a second tenant and you get a second context, so staging and production can't blur together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe auth login &lt;span class="nt"&gt;--tenant&lt;/span&gt; acme-staging &lt;span class="nt"&gt;--context&lt;/span&gt; staging
kupe config use-context staging
kupe config current-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kupe config view&lt;/code&gt; shows the active configuration with the tokens redacted, which makes it safe to paste into a support request. Every command also takes &lt;code&gt;--tenant&lt;/code&gt; and &lt;code&gt;--context&lt;/code&gt; (or &lt;code&gt;KUPE_TENANT&lt;/code&gt; and &lt;code&gt;KUPE_CONTEXT&lt;/code&gt;), which is what you'll reach for in scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it in CI
&lt;/h2&gt;

&lt;p&gt;A pipeline can't open a browser, so it signs in with an API key instead. Mint one scoped to the tenant, give it an expiry, and store the output as a CI secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe apikey create &lt;span class="nt"&gt;--name&lt;/span&gt; ci &lt;span class="nt"&gt;--role&lt;/span&gt; admin &lt;span class="nt"&gt;--expires-at&lt;/span&gt; 90d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw key is printed once and can't be retrieved again. Pass it to the CLI as &lt;code&gt;KUPE_API_TOKEN&lt;/code&gt;; the &lt;code&gt;--token&lt;/code&gt; flag also works but leaves the value in shell history and process listings, which is why the help text itself warns against it.&lt;/p&gt;

&lt;p&gt;Here is a job that creates a cluster, deploys to it, and always cleans up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUPE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CI_KUPE_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUPE_TENANT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"acme"&lt;/span&gt;

&lt;span class="nv"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_SHA&lt;/span&gt;:0:7&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'kupe cluster delete "$cluster" --yes'&lt;/span&gt; EXIT

kupe cluster create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cluster&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu-limit&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory-limit&lt;/span&gt; 8Gi &lt;span class="nt"&gt;--storage-limit&lt;/span&gt; 50Gi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait-timeout&lt;/span&gt; 10m

kupe cluster kubeconfig &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cluster&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--merge&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
kubectl &lt;span class="nt"&gt;--context&lt;/span&gt; &lt;span class="s2"&gt;"kupe-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUPE_TENANT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;cluster&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; apply &lt;span class="nt"&gt;-f&lt;/span&gt; example.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details do the work here. &lt;code&gt;create&lt;/code&gt; waits by default, so there's no &lt;code&gt;sleep&lt;/code&gt; to guess at. Outside a terminal, &lt;code&gt;delete&lt;/code&gt; refuses to run without &lt;code&gt;--yes&lt;/code&gt;, so a stray redirect can't delete infrastructure. And &lt;code&gt;delete&lt;/code&gt; waits as well, so a script that recreates the same name straight away isn't racing the old cluster on its way out. We learned that one from our own tests, when a destroy-and-recreate collided with the previous cluster's teardown. The platform copes with that now, but waiting is still the safer default.&lt;/p&gt;

&lt;p&gt;The exit codes are part of the contract too, and they're stable across releases. &lt;code&gt;3&lt;/code&gt; means authentication failed, &lt;code&gt;4&lt;/code&gt; means not found, &lt;code&gt;5&lt;/code&gt; means conflict, and &lt;code&gt;8&lt;/code&gt; means a &lt;code&gt;--wait-timeout&lt;/code&gt; ran out. That lets a script tell a bad token from a slow cluster without parsing error text. The &lt;a href="https://github.com/kupecloud/kupe-cli/blob/main/docs/design.md" rel="noopener noreferrer"&gt;full table&lt;/a&gt; is in the repository.&lt;/p&gt;

&lt;p&gt;We rely on that contract ourselves. The CLI's live test suite builds the real binary and runs it against the deployed API: &lt;code&gt;whoami&lt;/code&gt;, tenant and member commands, a bad token failing without leaking into the output, and an opt-in create-and-delete of a real cluster. If a command's output or exit code changes, that's where we find out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-o name&lt;/code&gt; helps with housekeeping too. A cluster per commit counts against your plan's cluster limit, so this clears out any CI clusters a cancelled job left behind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kupe cluster list &lt;span class="nt"&gt;-o&lt;/span&gt; name | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^ci-'&lt;/span&gt; | xargs &lt;span class="nt"&gt;-n1&lt;/span&gt; kupe cluster delete &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;The CLI is imperative, which makes it good for trying things out and for the glue between CI steps, and a poor record of what exists. If you want clusters reviewable in a pull request, use the &lt;a href="https://docs.kupe.cloud/terraform/" rel="noopener noreferrer"&gt;Terraform provider&lt;/a&gt;. If you're writing a service, call the &lt;a href="https://docs.kupe.cloud/api/" rel="noopener noreferrer"&gt;API&lt;/a&gt;. If your team needs a shared view, use the &lt;a href="https://console.kupe.cloud" rel="noopener noreferrer"&gt;console&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a first run, create a throwaway cluster, merge its kubeconfig, run a few &lt;code&gt;kubectl&lt;/code&gt; commands, then delete it and watch the delete wait for you. If you hit any issues while experimenting with the CLI, &lt;a href="https://kupe.cloud/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt; and we'll do our best to help.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>buildingkupe</category>
      <category>engineeringnotes</category>
    </item>
    <item>
      <title>Kupe Cloud alpha is open</title>
      <dc:creator>Billy Walker</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:33:28 +0000</pubDate>
      <link>https://dev.to/kupecloud/kupe-cloud-alpha-is-open-1eb4</link>
      <guid>https://dev.to/kupecloud/kupe-cloud-alpha-is-open-1eb4</guid>
      <description>&lt;p&gt;Signup for Kupe Cloud is open. There is no invite code, no card and no charge during alpha.&lt;/p&gt;

&lt;p&gt;Alpha is where we find out whether Kupe does what we built it to do: give a team a cluster with GitOps, secrets, metrics, logs and alerting already wired, so they can get on with shipping. We kept it closed for the first two months. The core paths went to a small group of people who already run Kubernetes for a living, because they notice when something is off and can say why. We spent the same two months trying to break the platform ourselves, on production, after every change.&lt;/p&gt;

&lt;p&gt;This post is about what that found, and what changed before we opened signup.&lt;/p&gt;

&lt;h2&gt;
  
  
  We became the first tenant
&lt;/h2&gt;

&lt;p&gt;The end-to-end suite has sixteen phases and a full run takes just under forty minutes. It goes through the public signup service rather than an admin shortcut, creates the cluster through the public API and deploys through Argo CD inside that cluster. Only the residue sweep afterwards runs with admin access, because somebody has to check the cupboard after the party. Everything on the customer path runs as a customer would.&lt;/p&gt;

&lt;p&gt;We ran three load profiles alongside it: steady traffic at a small demo app, a workload that outgrows its cluster, and concurrent tenant onboarding through the real signup route. The traffic run handled 298,708 requests over fifteen minutes, with a p95 of 140 ms and no failed requests through the external gateway and TLS. That result came from a tiny demo app on the smallest cluster size. It says the path holds under that test; it does not predict your application's latency.&lt;/p&gt;

&lt;p&gt;The cluster-growth tests answered a different question: does the platform add capacity while the workload is still asking for it? In one run, the HPA moved from two to thirteen replicas and the platform added a node under load. A separate scale-out run onboarded two tenants concurrently and left no residue. These are small runs on purpose. They gave us enough surface area to find the awkward parts before asking strangers to do it for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things that broke were useful
&lt;/h2&gt;

&lt;p&gt;Closed alpha earned its keep here. A routine operating-system update brought one node back without networking because a component of the cluster network could not start on the updated kernel. Our reboot tooling could not run on that node either, so it retained the cluster-wide reboot lock.&lt;/p&gt;

&lt;p&gt;The first fix looked sensible and changed nothing. It turned off in-place updates on the nodes our provisioning tooling manages, and the workers that needed it are created by the autoscaler, which that tooling never touches. We stopped patching nodes in place. Nodes are now replaced from validated images instead. A follow-up audit found an image-rebuild job that had been reporting success while building nothing. Green is a poor signal when the successful path is to skip every input.&lt;/p&gt;

&lt;p&gt;Capacity is the other ordinary failure. Our infrastructure provider does not always have a server available when the autoscaler asks for one. The autoscaler now tries the next size up, alerting includes the pool and the reason, and there is a runbook for the operator who gets the alert. With an empty pool, the first cluster waits for a node to arrive.&lt;/p&gt;

&lt;p&gt;A few failures only appear when two tenants do the same thing at once. Two clusters with the same name collided on a host-side role name. A concurrent teardown could also lose the record that told the platform to purge a tenant's data. The scale-out driver found both, and deletion now runs through the same endpoints customers use. You can read the public &lt;a href="https://docs.kupe.cloud/platform/deletion" rel="noopener noreferrer"&gt;deletion documentation&lt;/a&gt; before trying it yourself.&lt;/p&gt;

&lt;p&gt;Alerting needed the same scrutiny. Six chronic alerts were re-posting the same unactionable backlog every four hours. They were responsible for most of the noise, rather than the resource alerts we initially suspected. Fixing that reduced alert volume by 92%. Backlog work and incidents now travel through separate alert paths; an alert in the incident channel should be worth somebody's sleep.&lt;/p&gt;

&lt;p&gt;We also made isolation a routine assertion instead of a promise in an architecture diagram. Every end-to-end run creates a second real tenant and checks API access, metrics, logs, dashboards, GitOps destinations, host RBAC and the network boundary. The test that proves one tenant cannot read another's metrics runs before each change ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the platform
&lt;/h2&gt;

&lt;p&gt;The image pipeline builds on a cadence, validates the result, canaries a node, then rolls control planes before core nodes. Promotion runs through a staging estate with full end-to-end and load tests before production. That architecture exists to keep an update from becoming a cluster-wide leap of faith. A cluster without high availability can still see a brief API interruption while a node is replaced; choose HA when you create the cluster if that interruption is not acceptable. The &lt;a href="https://docs.kupe.cloud/platform/high-availability" rel="noopener noreferrer"&gt;high-availability documentation&lt;/a&gt; explains that choice.&lt;/p&gt;

&lt;p&gt;Signup is open with one tenant per account for now, plus a cap on total tenants that we raise as we go. The point is containment: an ordinary bad day should stay small. Tenant and account deletion are self-service from the console or CLI, and the suite checks for residue after every deletion.&lt;/p&gt;

&lt;p&gt;Releases are built, signed and pinned by digest before they are versioned. The cluster will not run an unsigned image, and baseline policies are enforced. The property that matters is that an untrusted image never becomes a running workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still rough
&lt;/h2&gt;

&lt;p&gt;Alpha is still alpha. Dedicated nodes and backup tooling do not exist yet, there is no SLA, and maintenance can interrupt a single-replica cluster. The &lt;a href="https://dev.to/alpha"&gt;alpha service notice&lt;/a&gt; has the current limits and the parts of the service that remain experimental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the first fifteen minutes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/signup"&gt;Create an account&lt;/a&gt;, create a cluster and deploy something small. It takes no invite code and nothing to pay. Then tell us where the path felt heavier than you expected, especially in the first fifteen minutes: a label you expected to find, a default you expected to be set, or a step that made you stop and ask why it existed. That is exactly the feedback alpha is for. Send it to &lt;a href="mailto:support@kupe.cloud"&gt;support@kupe.cloud&lt;/a&gt;, and check &lt;a href="https://status.kupe.cloud" rel="noopener noreferrer"&gt;status.kupe.cloud&lt;/a&gt; before treating an odd result as your fault.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>productupdates</category>
      <category>buildingkupe</category>
    </item>
    <item>
      <title>vCluster custom resource sync, one plugin for every kind | Kupe Cloud</title>
      <dc:creator>Billy Walker</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:12:52 +0000</pubDate>
      <link>https://dev.to/kupecloud/vcluster-custom-resource-sync-one-plugin-for-every-kind-kupe-cloud-2jg5</link>
      <guid>https://dev.to/kupecloud/vcluster-custom-resource-sync-one-plugin-for-every-kind-kupe-cloud-2jg5</guid>
      <description>&lt;p&gt;vCluster runs a full Kubernetes control plane as a pod inside a namespace of a host cluster. We use it to run our tenants' clusters: each one gets cluster-admin inside a real API boundary, without the cost of a dedicated cluster underneath. Anything self-contained inside the virtual cluster works out of the box: Deployments, Services, the operators a tenant installs. The trouble starts when a tenant writes an Argo CD &lt;code&gt;Application&lt;/code&gt; that only the central Argo CD on the host can reconcile, an &lt;code&gt;HTTPRoute&lt;/code&gt; that needs to attach to a shared Gateway, or an alert rule that belongs on the host ruler. Every multi-tenant platform grows these crossings: objects written on one side of a boundary that a controller on the other side has to act on.&lt;/p&gt;

&lt;p&gt;The options for syncing resources across that boundary are thinner than you'd expect. vCluster's open-source syncer handles the core resources well: Pods, Services, Secrets and friends. But the resources we most needed to sync were custom resources, and some of them needed references translated on the way across. vCluster does offer custom-resource syncing and sync patches, but they ship in the Pro image, behind a licence validated against vCluster Platform. That's a reasonable product boundary; it just wasn't a dependency we wanted in the boot and data path of every tenant cluster.&lt;/p&gt;

&lt;p&gt;The open-source routes weren't in better shape. Loft's own &lt;a href="https://github.com/loft-sh/vcluster-generic-crd-sync-plugin" rel="noopener noreferrer"&gt;generic CRD sync plugin&lt;/a&gt; pointed in the right direction but has been archived, which leaves the vCluster plugin SDK and a controller of your own.&lt;/p&gt;

&lt;p&gt;So that's what we built: an &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin" rel="noopener noreferrer"&gt;open vCluster generic sync plugin&lt;/a&gt;, Apache-2.0, a single plugin process that syncs any resource kind you declare in its configuration. It carries the pieces a real platform needs: a direction and mode per kind, reference translation, label and namespace selectors, and status sync. Every crossing is explicit, reviewable and independent of a licence service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources cross the boundary for different reasons
&lt;/h2&gt;

&lt;p&gt;It's worth naming the actual objects rather than filing them all under “multi-tenant integration”. Each crossing has an owner, a direction and a reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some objects leave the tenant cluster for a host controller: an &lt;code&gt;Application&lt;/code&gt; for the central Argo CD, a &lt;code&gt;PrometheusRule&lt;/code&gt; for the host Mimir ruler.&lt;/li&gt;
&lt;li&gt;Some go the same way but need their status back: an &lt;code&gt;HTTPRoute&lt;/code&gt; attaches to the host Gateway, and the tenant needs to see whether it was accepted.&lt;/li&gt;
&lt;li&gt;Some only cross on a condition: a dashboard &lt;code&gt;ConfigMap&lt;/code&gt; leaves only when it carries the dashboard label.&lt;/li&gt;
&lt;li&gt;Some travel the other way: platform-managed &lt;code&gt;Secret&lt;/code&gt; objects come back into the tenant cluster.&lt;/li&gt;
&lt;li&gt;Some are read-only imports: &lt;code&gt;GatewayClass&lt;/code&gt; objects are mirrored so tenants can discover the shared gateway classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those aren't interchangeable operations. A route needs its service references translated; a gateway class must remain host-owned; a repository credential should be admitted only by a narrow label selector. A single broad “sync CRDs” switch would hide all of that policy in code and RBAC.&lt;/p&gt;

&lt;p&gt;Our plugin makes the policy a list of &lt;code&gt;syncResources&lt;/code&gt; entries. Each entry names an &lt;code&gt;apiVersion&lt;/code&gt;, &lt;code&gt;kind&lt;/code&gt;, a direction (&lt;code&gt;toHost&lt;/code&gt; or &lt;code&gt;fromHost&lt;/code&gt;) and an optional selector, plus a mode: &lt;code&gt;sync&lt;/code&gt; reconciles the source object to its target, while &lt;code&gt;mirror&lt;/code&gt; is a read-only import. Patches translate references and nothing else: &lt;code&gt;rewriteName&lt;/code&gt;, &lt;code&gt;rewriteNamespace&lt;/code&gt;, &lt;code&gt;rewriteRef&lt;/code&gt;, &lt;code&gt;rewriteHostRef&lt;/code&gt;, &lt;code&gt;rewriteLabelSelector&lt;/code&gt; and &lt;code&gt;none&lt;/code&gt; are the complete vocabulary.&lt;/p&gt;

&lt;p&gt;That restriction is deliberate. A sync configuration should say which reference is being translated and why. It shouldn't become a second, unreviewed templating language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin protects the host copy
&lt;/h2&gt;

&lt;p&gt;A resource crossing a cluster boundary isn't a blind copy. The plugin has a few rules that keep source-of-truth decisions visible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;statusSync&lt;/code&gt; only operates for a resource with a real status subresource, and status always flows from host to virtual cluster. The plugin strips tenant-written status before it creates the host object. A tenant cannot write an &lt;code&gt;Accepted=True&lt;/code&gt; condition into a route or an application and have the platform treat it as host state.&lt;/p&gt;

&lt;p&gt;Selectors support labels plus namespace include and exclude rules. Exclusions win. Deletes are provenance-gated: the plugin removes only an object it created and stamped, never an unrelated object that happens to share a name. Trace logging redacts &lt;code&gt;Secret&lt;/code&gt; data, and the plugin exports Prometheus metrics on its own endpoint.&lt;/p&gt;

&lt;p&gt;The configuration parser is strict as well. An unknown field is an error, so a misspelt key in the sort of YAML that lives for months in a tenant chart fails at load time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Argo field we had to leave alone
&lt;/h2&gt;

&lt;p&gt;One lesson came the hard way, from a real failure in our own Argo CD integration. Argo stores a pending sync in the top-level &lt;code&gt;operation&lt;/code&gt; field of an &lt;code&gt;Application&lt;/code&gt;. Our syncer treated the virtual copy as complete desired state and could remove that field after Argo wrote it but before Argo's worker read it. Auto-sync then never ran.&lt;/p&gt;

&lt;p&gt;The fix became &lt;code&gt;hostOwnedFields&lt;/code&gt;. These are top-level fields owned by a host controller: the syncer neither copies them from the virtual object nor deletes them when the virtual object lacks them, and host-side changes do not trigger reconciliation. &lt;code&gt;Application.operation&lt;/code&gt; is now host-owned by default.&lt;/p&gt;

&lt;p&gt;That rule sounds narrow until you run a controller on both sides of a boundary. The object may be shared; every field does not have to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small custom-resource sync
&lt;/h2&gt;

&lt;p&gt;The smallest useful configuration syncs one custom resource from a tenant vCluster to its host cluster. For this walkthrough we'll borrow the test-only &lt;code&gt;Widget&lt;/code&gt; CRD from the plugin's own end-to-end suite.&lt;/p&gt;

&lt;p&gt;First install the CRD on both sides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/kupecloud/vcluster-generic-sync-plugin/main/test/testdata/widget-crd.yaml
vcluster connect my-vcluster &lt;span class="nt"&gt;--&lt;/span&gt; kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/kupecloud/vcluster-generic-sync-plugin/main/test/testdata/widget-crd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the plugin and the RBAC required to discover the CRD and reconcile &lt;code&gt;Widget&lt;/code&gt; objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generic-sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/kupecloud/vcluster-generic-sync-plugin:latest&lt;/span&gt; &lt;span class="c1"&gt;# pin a release in production&lt;/span&gt;
    &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
    &lt;span class="na"&gt;rbac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;extraRules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;example.com'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;widgets'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delete'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;patch'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;get'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;watch'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;clusterRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;extraRules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apiextensions.k8s.io'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customresourcedefinitions'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;get'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;watch'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
      &lt;span class="na"&gt;syncResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com/v1&lt;/span&gt;
          &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Widget&lt;/span&gt;
          &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;toHost&lt;/span&gt;

&lt;span class="na"&gt;rbac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;clusterRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two &lt;code&gt;version&lt;/code&gt; fields are unrelated: &lt;code&gt;v2&lt;/code&gt; is vCluster's plugin format, while &lt;code&gt;config.version: v1&lt;/code&gt; is the plugin's own configuration schema. The &lt;code&gt;rbac&lt;/code&gt; blocks are plain vCluster chart values; the chart folds &lt;code&gt;extraRules&lt;/code&gt; into the Role and ClusterRole it creates for the syncer pod.&lt;/p&gt;

&lt;p&gt;Create the &lt;a href="https://raw.githubusercontent.com/kupecloud/vcluster-generic-sync-plugin/main/test/testdata/widget.yaml" rel="noopener noreferrer"&gt;matching test object&lt;/a&gt; through the vCluster connection. The plugin's &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin/blob/main/docs/examples/base-config.md" rel="noopener noreferrer"&gt;base configuration guide&lt;/a&gt; explains the RBAC shape in more detail. In a real installation, scope those rules to each kind and namespace you intend to sync; &lt;code&gt;*&lt;/code&gt; is easier to write and much harder to explain later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Gateway, tenant routes
&lt;/h2&gt;

&lt;p&gt;Gateway API shows why a generic syncer needs patches. The tenant's &lt;code&gt;HTTPRoute&lt;/code&gt; has two kinds of references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;parentRefs&lt;/code&gt; point at a Gateway that already exists on the host. Keep those values unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backendRefs&lt;/code&gt; point at Services from the tenant cluster. Rewrite those references to the host-side names created by vCluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin's &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin/blob/main/docs/examples/gateway-api.md" rel="noopener noreferrer"&gt;Gateway API example&lt;/a&gt; installs the Gateway API CRDs on both clusters and supplies the complete RBAC. The sync configuration does the real work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generic-sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/kupecloud/vcluster-generic-sync-plugin:latest&lt;/span&gt; &lt;span class="c1"&gt;# pin a release in production&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
      &lt;span class="na"&gt;syncResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
          &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPRoute&lt;/span&gt;
          &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;toHost&lt;/span&gt;
          &lt;span class="na"&gt;statusSync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spec.parentRefs[*]&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spec.rules[*].backendRefs[*]&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rewriteRef&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;type: none&lt;/code&gt; is a sharp tool. It lets a route target a shared host Gateway directly, so the host must enforce which vCluster namespaces may attach, which hostnames they may claim, and where the listener sends traffic. The plugin can preserve a reference; it cannot decide whether that reference is safe for your gateway policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven crossings, scoped permissions
&lt;/h2&gt;

&lt;p&gt;Our production configuration contains seven resource kinds built from the same pieces: a read-only &lt;code&gt;GatewayClass&lt;/code&gt; mirror; host-side &lt;code&gt;HTTPRoute&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, &lt;code&gt;PrometheusRule&lt;/code&gt; and dashboard sync; repository-secret sync guarded by Argo labels; and platform-managed &lt;code&gt;Secret&lt;/code&gt; import.&lt;/p&gt;

&lt;p&gt;The RBAC follows the resource boundary. &lt;code&gt;Application&lt;/code&gt; and repository-secret permissions live in namespace &lt;code&gt;Role&lt;/code&gt;s rather than a broad &lt;code&gt;ClusterRole&lt;/code&gt;, so the plugin in one tenant vCluster cannot list another tenant's credentials. &lt;code&gt;hostNamespace&lt;/code&gt; is useful for controllers such as central Argo CD, but it's not a reason to make every host namespace visible.&lt;/p&gt;

&lt;p&gt;There's a limit to what “generic” means here. The plugin can sync arbitrary declared kinds, yet it stamps &lt;code&gt;kupe.cloud/*&lt;/code&gt; labels and annotations as its provenance contract. That's how it knows which objects it owns, which copies it may delete, and which changes are worth reconciling. The generic part is the configured resource type; the ownership contract stays specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one crossing
&lt;/h2&gt;

&lt;p&gt;If you're adding the plugin to your own vCluster setup, start with a single kind: the one that already has an obvious source of truth. Write one &lt;code&gt;syncResources&lt;/code&gt; entry for it, grant the narrowest RBAC that lets it work, and watch what lands on the host: the object, its translated references, its status. Once that first crossing behaves, the rest are the same pattern with different nouns.&lt;/p&gt;

&lt;p&gt;If something goes wrong, look at ownership before configuration. In our experience the first failure is a field or a reference that both sides think they own, and &lt;code&gt;hostOwnedFields&lt;/code&gt; or a patch is usually the fix.&lt;/p&gt;

&lt;p&gt;The plugin lives at &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin" rel="noopener noreferrer"&gt;github.com/kupecloud/vcluster-generic-sync-plugin&lt;/a&gt;, with the &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin/tree/main/docs/configuration" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt;, worked examples and the end-to-end suite the &lt;code&gt;Widget&lt;/code&gt; walkthrough came from. If you hit a bug or something that doesn't behave as documented, &lt;a href="https://github.com/kupecloud/vcluster-generic-sync-plugin/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; and we'll happily help.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>buildingkupe</category>
      <category>engineeringnotes</category>
    </item>
  </channel>
</rss>
