Migrate an existing Forgejo
Bring users, organizations, teams, repositories, webhooks and deploy keys that already exist under the operator with the importer.
If your Forgejo already has users, organizations and repositories, the
operator won't touch them on its own. A CR whose name is already taken in
Forgejo stays Ready: False with reason ForgejoObjectAlreadyExists, so the
operator never takes over an object by accident.
To bring existing objects under management, use weebo-forgejo-importer.
It reads the live instance and creates the matching CRs with each Forgejo id
already recorded in their status. The operator then manages that exact
object instead of trying to create a new one.
Before you begin
- The operator is installed and a
ForgejoInstanceis connected. - A site-administrator token for the importer (read access is enough).
- For
--apply, a kube context allowed to create the CRs and patch theirstatus: cluster-admin, or the least-privilege role below. - If a
ForgejoNamespacePolicyexists, the import namespace must be allowed for the imported organizations.
What gets imported
| Forgejo | CR | Notes |
|---|---|---|
| Users | ForgejoUser | As managementPolicy: Observe by default (see below) |
| Organizations | ForgejoOrganization | |
| Teams and their members | ForgejoTeam | Except Owners, which Forgejo manages |
| Repositories | ForgejoRepository | Settings, units, branch protections and team access. Pull mirrors keep their migrate.mirrorInterval. |
| Repository and organization webhooks | ForgejoWebhook | Without secretRef (see below) |
| Deploy keys | ForgejoDeployKey | With their publicKey |
Not imported: OAuth2 applications and access tokens. Forgejo never returns their secrets, so the operator could never hand them out. Recreate them as new CRs (Credentials).
Every imported CR gets deletionPolicy: Orphan: these objects existed
before the operator, and deleting a manifest must not destroy them. Switch a
CR to Delete once you want that.
Run the importer
The importer ships in the operator image. Run it from a workstation with
your kubeconfig mounted. A small shell function keeps the commands below
short (use podman instead of docker if that's what you have):
export FORGEJO_TOKEN=<site-admin-token>
importer() {
docker run --rm -e FORGEJO_TOKEN \
-v "$HOME/.kube/config:/kubeconfig:ro" -e KUBECONFIG=/kubeconfig \
--entrypoint /usr/local/bin/weebo-forgejo-importer \
ghcr.io/batleforc/weebo-forgejo-operator:<version> "$@"
}| Flag | Default | Description |
|---|---|---|
--url / FORGEJO_URL | Instance URL, as in ForgejoInstance.spec.url | |
--token / FORGEJO_TOKEN | Site-administrator token | |
--namespace, -n | default | Namespace for repositories, webhooks and deploy keys |
--instance-ref | none | Written as spec.instanceRef.name on every CR |
--owner (repeatable) | everything | Only these organizations/users and their repositories. Other users are imported only when a team or repository references them. |
--manage-users | off | Import users as Manage instead of Observe |
--apply | off | Create the CRs and seed their status. Without it, the YAML is printed. |
Warnings, such as a team member that isn't a known user or a team left out of
a repository's teamRefs, are printed to stderr.
Adopt safely: review, apply, then commit
status is a subresource:
kubectl apply and GitOps tools never write it. That's why the adoption
happens in the cluster, once, before the manifests go to Git.
Review
Print what would be imported, without touching the cluster:
importer --url https://git.example.com --namespace forgejo --owner weebo > forgejo.yamlAdopt
Run the same command with --apply:
importer --url https://git.example.com --namespace forgejo --owner weebo --applyEach CR is created paused, with the forgejo.weebo.io/import-pending
annotation. Its status.forgejoId and forgejoName are written, and only
then is the annotation removed. The operator never sees an imported CR
without its id, so it can't create a duplicate.
Commit
Commit forgejo.yaml to your GitOps repository. Its status blocks are
ignored when applied; the CRs already exist with their ids.
Check
kubectl get fjorg,fjteam,fjuser
kubectl -n forgejo get fjrepo,fjhook,fjdkeyEverything should read READY True. The first reconcile of an adopted CR
sends nothing to Forgejo when the imported spec matches.
Importer RBAC
Without --apply the importer never talks to the cluster. With it, it
reads the existing CRs (to keep names stable), creates the new ones by
server-side apply, seeds their status and removes the pause annotation.
It never reads or writes Secrets. A dedicated identity needs no more than:
# Every imported kind is namespaced: bound only in the import namespace
# (--namespace).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: weebo-forgejo-importer-namespaced
rules:
- apiGroups: ["forgejo.weebo.io"]
resources: [forgejousers, forgejoorganizations, forgejoteams,
forgejorepositories, forgejowebhooks, forgejodeploykeys]
verbs: ["get", "list", "create", "patch"]
- apiGroups: ["forgejo.weebo.io"]
resources: [forgejousers/status, forgejoorganizations/status, forgejoteams/status,
forgejorepositories/status, forgejowebhooks/status, forgejodeploykeys/status]
verbs: ["patch"]
---
# The unique-claim check reads the CRs of every namespace and the
# instances' URLs.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: weebo-forgejo-importer-claims
rules:
- apiGroups: ["forgejo.weebo.io"]
resources: [forgejousers, forgejoorganizations, forgejoteams,
forgejoinstances, forgejolocalinstances]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: weebo-forgejo-importer-claims
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: weebo-forgejo-importer-claims
subjects:
- kind: ServiceAccount # or the User/Group running the importer
name: weebo-forgejo-importer
namespace: forgejo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: weebo-forgejo-importer
namespace: forgejo # the --namespace you import into
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: weebo-forgejo-importer-namespaced
subjects:
- kind: ServiceAccount
name: weebo-forgejo-importer
namespace: forgejoPatching status is what lets it adopt an object: keep this role off
anyone who shouldn't be able to point a CR at an arbitrary Forgejo object,
and remove the binding once the migration is done. The CRs still go through
the admission webhook, so the allow-list applies
to the import namespace. A user, organization or team another CR already
owns on the same server isn't adopted twice (RemoteObjectClaimed).
Finish by hand
- Users are imported as
Observe: they're never modified, only looked up, so teams and repositories can reference them.- With
--manage-usersthey becomeManage. A local account is then markedbot: true, because its password can't be known, and the operator never resets it, whatever later changes to the CR: only a password the operator handed out itself (status.passwordEmitted) is ever replaced. - A site admin imported with
--manage-userskeepsadmin: true, which needs anAllowrule naming it inallowedUsersfor theForgejoUserkind and the import namespace, even without any policy (see Security model). Without one, its CR is refused. - Turning such a user into a regular local user (
bot: falseplussecretTargets) resets its password into the targets.
- With
- Webhook secrets are write-only in Forgejo. An imported hook keeps its
current secret, and edits leave it alone. Adding a
secretRefrecreates the hook with that secret, which loses its delivery history. - Organization owners (
ownerRef) only matter at creation, so they're left unset.
From cluster-scoped users, organizations and teams
Releases before the namespaced identities had ForgejoUser,
ForgejoOrganization, ForgejoTeam and ForgejoUserKey cluster-scoped. A
CRD's scope can't change in place: its CRD — and with it every CR of the
kind — must be deleted. weebo-forgejo-operator migrate-to-namespaced moves
them without touching Forgejo:
Plan. Choose a namespace per CR — a mapping file of Kind/name: namespace lines, and a default for the others — and check the plan; it
writes the new manifests (for your GitOps repository) and plan.json to
--out:
ForgejoUser/alice: team-a
ForgejoOrganization/weebo: platformweebo-forgejo-operator migrate-to-namespaced --dry-run \
--mapping mapping.yaml --default-namespace platform --out ./migrationA reference whose target lands in another namespace gets that namespace,
and the target lists the referencing namespace in its sharedWith. Such
references need references.crossNamespace: true on the release, and
--allow-cross-namespace to acknowledge it.
Stop the operator (replicas: 0): no finalizer must run while the old
CRs are deleted. Nothing is deleted in Forgejo.
Migrate: the same command with --yes instead of --dry-run. It
removes the finalizers of the old CRs, deletes them and their CRDs, then
waits for the new CRDs: apply them now (crds.yaml of the release, or the
chart with crds.install). It recreates each CR in its namespace with its
binding to Forgejo (forgejoId, the resolved instance…), rewrites the
references of the namespaced CRs, and renames the owner recorded on the
Secrets the users emitted (ForgejoUser/alice →
ForgejoUser/team-a/alice). A run interrupted after the deletion resumes
from plan.json.
Start the operator, and commit the manifests of --out in place of the
old ones (Argo CD would otherwise re-apply cluster-scoped manifests).
Vault values keep their old owner id (the tool can't reach Vault): each
moved ForgejoUser/ForgejoUserKey carries it in its
forgejo.weebo.io/previous-owner annotation, and the operator takes those
values back, recording its new id on its next write. Remove the annotation
once every value is rewritten.
One thing the tool can't do for you: a user that wrote into another
namespace than the one it moves to is refused (CrossNamespaceReference)
until its secretTargets follow it (the dry run warns).
Next
Hand out credentials for the imported repositories: Credentials.