Hands-On Labs · The Capstone · Part 1 of 5

Capstone Part 1 — GitOps Foundation

This is the first of five parts that build one continuous project across the rest of this capstone: cert-tracker, a small service that tracks your own progress through the sixteen-exam Golden Kubestronaut ladder — which certs you've passed, when each one expires, and whether you currently qualify for Kubestronaut or Golden Kubestronaut. Part 1 lays the foundation every later part stands on: a running Argo CD install, a config repo held deliberately separate from cert-tracker's own source, and an Application object whose sync policy actually closes the GitOps loop — prune and selfHeal both explicitly on, not left at their off-by-default settings. By the end of this page you will have watched cert-tracker deploy itself from a git push with no kubectl apply from you, watched a manual edit get reverted within seconds, and watched a resource you deleted from Git disappear from the cluster on its own.

☺ Explain it like I'm 10

Imagine your bedroom has a poster taped to the door listing exactly how the room should look: bed made, three books on the shelf, lamp on the desk. A small, tireless robot checks the room against that poster every few minutes, forever. Move a book off the shelf yourself, and the robot quietly puts it back — it isn't asking permission, it's just following the poster. Cross a book off the poster instead, and the robot removes that book from the shelf too, because the poster is the only truth it trusts. This page installs that robot, hangs up the first poster, and proves — with your own hands, not just a claim — that both kinds of correction actually happen.

🤖Your host for this part: Recon the Robot — the same reconciler behind GitOps Philosophy, CGOA, and Argo CD, this time reconciling a project you're personally invested in landing safely.
⚠ Where you're starting, and what you'll have when you're done

Starting: a Kubernetes cluster you can already reach with kubectl — nothing running on it yet, no Argo CD, no cert-tracker. Leaving this page: Argo CD installed and reachable through the argocd CLI; a cert-tracker app repo holding a small Express service and its Dockerfile; a separate cert-tracker-config repo holding Kustomize manifests under apps/cert-tracker/base and .../overlays/dev; an AppProject named kubestronaut scoping exactly that repo and destination namespace; and an Application object with syncPolicy.automated.prune and .selfHeal both explicitly true. You'll also have firsthand proof that a hand-edited Deployment gets reverted within seconds and a resource deleted from Git disappears from the cluster on its own. Part 2 picks up exactly here and turns the plain Deployment this part creates into a real canary rollout.

What this part assumes, and what it produces

☺ Like you're 10: Just a cluster you can already reach, plus the everyday CLI toolbox — nothing about cert-tracker exists yet.

You need a Kubernetes cluster reachable right now with kubectl — a local kind or minikube cluster is exactly right for this capstone and is what every command below assumes, though any conformant cluster you already administer works identically. You also need Docker (or another OCI-compliant builder) to build cert-tracker's image, git and a GitHub account (or another Git host — swap the URLs), and the argocd CLI installed locally. This course assumes the kubectl fluency of the five core Kubernetes certifications this whole ladder sits on top of — if kubectl apply, namespaces and Deployments aren't yet comfortable, the Kubernetes course is the right stop before this one. Nothing else needs to exist yet: no service mesh, no policy engine, no observability stack, no portal. Those arrive starting in Part 3.

The world model this whole capstone shares

Five parts, one project, so it's worth naming the shape once, here, so nothing surprises you later:

ThingName / valueIntroduced
The applicationcert-tracker — tracks your progress through the 16-exam Golden Kubestronaut ladderPart 1 — this page
App source repocert-tracker on GitHub — code and Dockerfile onlyPart 1
Container imageghcr.io/kubestronaut/cert-tracker:v0.1.0Part 1
GitOps config repocert-tracker-config — Kustomize base + overlays, kept separate from app sourcePart 1
AppProjectkubestronaut — scopes exactly that repo and the cert-tracker namespacePart 1
Argo CD Applicationcert-trackerprune: true, selfHeal: truePart 1
Release strategyplain Deployment → canary Argo RolloutPart 1 (Deployment) → Part 2 (Rollout)
Mesh & policynot yet installedPart 3
Observabilitynot yet wiredPart 4
Developer portalnot yet cataloguedPart 5

Keep that table in your head across all five parts: whenever a later page says "the cert-tracker Application" or "the config repo," this is where those names were born.

Standing up cert-tracker: the app this loop deploys

☺ Like you're 10: A tiny web service that remembers which exams you've passed — small on purpose, so every later part can add to it without a rewrite.

cert-tracker is deliberately small: an Express service, an in-memory store seeded with all sixteen exams from the sixteen-exam ladder, and three routes. Create package.json:

{
  "name": "cert-tracker",
  "version": "0.1.0",
  "private": true,
  "scripts": { "start": "node src/index.js" },
  "dependencies": { "express": "^4.19.2" }
}
// src/index.js
const express = require("express");
const app = express();
app.use(express.json());

// The sixteen exams Golden Kubestronaut is built from
const EXAMS = [
  { id: "kcna", name: "Kubernetes and Cloud Native Associate", family: "kubernetes-core" },
  { id: "kcsa", name: "Kubernetes and Cloud Native Security Associate", family: "kubernetes-core" },
  { id: "cka", name: "Certified Kubernetes Administrator", family: "kubernetes-core" },
  { id: "ckad", name: "Certified Kubernetes Application Developer", family: "kubernetes-core" },
  { id: "cks", name: "Certified Kubernetes Security Specialist", family: "kubernetes-core" },
  { id: "cgoa", name: "Certified GitOps Associate", family: "project-associate" },
  { id: "capa", name: "Certified Argo Project Associate", family: "project-associate" },
  { id: "cba", name: "Certified Backstage Associate", family: "project-associate" },
  { id: "cca", name: "Certified Cilium Associate", family: "project-associate" },
  { id: "ica", name: "Istio Certified Associate", family: "project-associate" },
  { id: "kca", name: "Kyverno Certified Associate", family: "project-associate" },
  { id: "otca", name: "OpenTelemetry Certified Associate", family: "project-associate" },
  { id: "pca", name: "Prometheus Certified Associate", family: "project-associate" },
  { id: "cnpa", name: "Certified Cloud Native Platform Engineering Associate", family: "platform" },
  { id: "cnpe", name: "Certified Cloud Native Platform Engineer", family: "platform" },
  { id: "lfcs", name: "Linux Foundation Certified System Administrator", family: "linux" },
];
const KUBESTRONAUT_IDS = ["kcna", "kcsa", "cka", "ckad", "cks"];

const passed = new Map(); // id -> passedOn (ISO date) — in-memory only, on purpose, for now

app.get("/healthz", (_req, res) => res.status(200).json({ status: "ok" }));

app.get("/exams", (_req, res) => {
  res.status(200).json(EXAMS.map((e) => ({ ...e, passedOn: passed.get(e.id) || null })));
});

app.post("/exams/:id/pass", (req, res) => {
  const exam = EXAMS.find((e) => e.id === req.params.id);
  if (!exam) return res.status(404).json({ error: "no such exam" });
  const passedOn = (req.body && req.body.passedOn) || new Date().toISOString().slice(0, 10);
  passed.set(exam.id, passedOn);
  res.status(200).json({ ...exam, passedOn });
});

app.get("/status", (_req, res) => {
  res.status(200).json({
    passedCount: passed.size,
    totalCount: EXAMS.length,
    kubestronaut: KUBESTRONAUT_IDS.every((id) => passed.has(id)),
    goldenKubestronaut: EXAMS.every((e) => passed.has(e.id)),
  });
});

module.exports = { app };

if (require.main === module) {
  const port = process.env.PORT || 8080;
  app.listen(port, () => console.log(`cert-tracker listening on :${port}`));
}
# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
EXPOSE 8080
CMD ["node", "src/index.js"]

Scaffold the repo, sanity-check it locally, then build and push the image the Deployment below will reference — swap kubestronaut for your own GitHub username or org; this page keeps the placeholder used throughout the course for consistency:

mkdir cert-tracker && cd cert-tracker
git init -b main
# add package.json, src/index.js, Dockerfile exactly as above
git add .
git commit -m "chore: cert-tracker v0.1.0 — track the sixteen-exam ladder"
gh repo create cert-tracker --public --source=. --remote=origin --push

npm install && npm start &
curl localhost:8080/healthz            # {"status":"ok"}
curl localhost:8080/status             # passedCount 0, kubestronaut false
kill %1

docker build -t ghcr.io/kubestronaut/cert-tracker:v0.1.0 .
docker push ghcr.io/kubestronaut/cert-tracker:v0.1.0

A GitOps repo is not the app repo: structuring cert-tracker-config

☺ Like you're 10: Two separate folders on two separate shelves — one holds the app's own code, the other holds only the poster describing how it should run.

Per CGOA's tooling and patterns domains, this is a deliberate state store boundary, not a filing preference. cert-tracker runs arbitrary application code and takes pull requests from anyone; cert-tracker-config holds nothing but declarative manifests. Scoping Argo CD's AppProject.sourceRepos to the config repo alone is a real, auditable trust boundary — Argo CD is never given a reason to read a single line of cert-tracker's own source. Later capstone parts add sibling directories under this same config repo rather than new repositories, so the state store stays small enough to hand-audit even once mesh, policy and observability manifests join it.

cert-tracker-config/
├── apps/
│   └── cert-tracker/
│       ├── base/
│       │   ├── kustomization.yaml
│       │   ├── deployment.yaml
│       │   └── service.yaml
│       └── overlays/
│           └── dev/
│               ├── kustomization.yaml
│               └── patch-replicas.yaml
└── bootstrap/
    ├── project.yaml
    └── application.yaml
# apps/cert-tracker/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cert-tracker
  labels: { app: cert-tracker }
spec:
  replicas: 2
  selector:
    matchLabels: { app: cert-tracker }
  template:
    metadata:
      labels: { app: cert-tracker }
    spec:
      containers:
        - name: cert-tracker
          image: ghcr.io/kubestronaut/cert-tracker:v0.1.0
          ports: [{ containerPort: 8080 }]
          readinessProbe: { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 3 }
          livenessProbe: { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 10 }
# apps/cert-tracker/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cert-tracker
spec:
  selector: { app: cert-tracker }
  ports: [{ port: 80, targetPort: 8080 }]
---
# apps/cert-tracker/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [deployment.yaml, service.yaml]
# apps/cert-tracker/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cert-tracker
resources: [../../base]
patches:
  - path: patch-replicas.yaml
    target: { kind: Deployment, name: cert-tracker }
---
# apps/cert-tracker/overlays/dev/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: cert-tracker }
spec:
  replicas: 1   # one replica is plenty on a local kind cluster

Notice there is no namespace.yaml in base/ — the overlay's namespace: transformer stamps every resource into cert-tracker, and the Application below creates that namespace itself through a sync option, not a tracked manifest. That choice matters once prune: true is on: pruning a tracked Namespace object would take the whole namespace, and everything in it, with it.

mkdir cert-tracker-config && cd cert-tracker-config
git init -b main
# create the tree above
git add .
git commit -m "chore: cert-tracker base + dev overlay"
gh repo create cert-tracker-config --public --source=. --remote=origin --push

Installing Argo CD

☺ Like you're 10: Move the inspector robot into the building before you hand it a poster to check against.

Install the stable release into its own namespace, and wait for both key workloads to report ready — note the second one is a StatefulSet, not a Deployment, per Argo CD's architecture, which is exactly why kubectl get deploy -n argocd alone would quietly miss it:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

kubectl -n argocd rollout status deploy/argocd-server --timeout=180s
kubectl -n argocd rollout status statefulset/argocd-application-controller --timeout=180s

Retrieve the generated admin password, log in with the CLI, and confirm it's talking to your cluster:

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo
kubectl -n argocd port-forward svc/argocd-server 8080:443 &

argocd login localhost:8080 --username admin --insecure
argocd version --short

Wiring the reconciliation loop: AppProject and Application

☺ Like you're 10: One rule says which poster this robot is even allowed to read; the other says exactly which poster, and which room, to match it to.

The default AppProject that ships out of the box trusts any source repo and any destination — fine for a demo, a real gap in production. Scope a dedicated project first:

# bootstrap/project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: kubestronaut
  namespace: argocd
spec:
  description: The capstone project — cert-tracker, and everything later parts add to it
  sourceRepos:
    - https://github.com/kubestronaut/cert-tracker-config.git
  destinations:
    - server: https://kubernetes.default.svc
      namespace: cert-tracker
  clusterResourceWhitelist: []   # no namespaces, no CRDs — CreateNamespace=true below covers the one we need
# bootstrap/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-tracker
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: kubestronaut
  source:
    repoURL: https://github.com/kubestronaut/cert-tracker-config.git
    targetRevision: main
    path: apps/cert-tracker/overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: cert-tracker
  syncPolicy:
    automated:
      prune: true      # off by default even under "automated" — see Argo CD
      selfHeal: true    # off by default even under "automated" — see Argo CD
    syncOptions:
      - CreateNamespace=true
◆ Key idea

prune and selfHeal are two independent booleans, and both default to false — even once automated: {} is present. Miss either one and you get half a GitOps loop: leave out selfHeal and a hand-edited Deployment sits OutOfSync forever, un-reverted; leave out prune and a resource deleted from Git keeps running in the cluster, permanently orphaned. Both flags below are set on purpose, which is the entire point of this part.

Apply both manifests directly with kubectl — this is the one and only manual apply in this whole capstone. Every change after this bootstrap moment ships through cert-tracker-config, never through a command run by hand again:

kubectl apply -n argocd -f bootstrap/project.yaml
kubectl apply -n argocd -f bootstrap/application.yaml

argocd app get cert-tracker
argocd app wait cert-tracker --health --timeout 120
cert-tracker-config apps/cert-tracker/ base + overlays/dev desired state, versioned Argo CD AppProject: kubestronaut Application: cert-tracker prune: true selfHeal: true applies since automated cert-tracker namespace Deployment + Service the actual resources poll ~3min / webhook apply manual drift (kubectl edit) reverted within seconds — no new commit needed ghcr.io image cert-tracker:v0.1.0 built + pushed from the app repo pulled by kubelet at pod start Argo CD never moves image bytes — it only ever applies manifests.

Once argocd app get cert-tracker reports Synced and Healthy, confirm the pods are actually answering:

kubectl get pods -n cert-tracker
kubectl -n cert-tracker port-forward svc/cert-tracker 8081:80 &
curl localhost:8081/healthz
curl localhost:8081/status

Proving self-heal and prune actually happen

☺ Like you're 10: Break it by hand, twice, on purpose, and watch the robot fix both kinds of mess without you touching git push a second time.

First, prove self-heal. Scale the Deployment down by hand and watch it climb back with no new commit anywhere:

kubectl -n cert-tracker scale deployment/cert-tracker --replicas=0
kubectl -n cert-tracker get deploy cert-tracker -w
# replicas climbs back to 1 within seconds — Argo CD watches the live cluster continuously

Notice the speed: this correction lands in seconds, because selfHeal watches the live cluster through Kubernetes' own watch API, not through the Git poll interval. A change on the Git side is a different clock entirely — the next section relies on that distinction.

Now prove prune. Remove the Service from cert-tracker-config, push, and watch it vanish from the cluster on its own:

cd cert-tracker-config
git rm apps/cert-tracker/base/service.yaml
# also drop service.yaml from base/kustomization.yaml's resources list
git commit -am "test: (temporary) drop the Service to prove prune"
git push

argocd app sync cert-tracker      # or wait for the next ~3min poll
kubectl -n cert-tracker get svc cert-tracker
# Error from server (NotFound): services "cert-tracker" not found

That error is the entire point of this section — not a claim that prune works, proof. Put it back the GitOps-idiomatic way, by reverting the commit rather than hand-creating the Service:

git revert HEAD --no-edit
git push
argocd app sync cert-tracker
kubectl -n cert-tracker get svc cert-tracker    # back, recreated by the reconciler, not by you

What "done" looks like for Part 1, and where Part 2 picks up

☺ Like you're 10: A robot that's actually watching, a poster it actually trusts completely, and proof of both — not just a working demo you have to take on faith.

At the end of this part you have: cert-tracker running in its own namespace from an image built once and referenced by tag; a cert-tracker-config repo structured as base + overlay, with no cluster-scoped resources tracked in Git; an AppProject scoping exactly one repo and one destination; and an Application with prune and selfHeal both proven, not assumed. Nothing here gets thrown away:

PartWhat it does with Part 1's artifacts
2 — Progressive DeliveryReplaces the plain Deployment in apps/cert-tracker with an Argo Rollout, canarying releases through this exact same Application and config repo
3 — Mesh & PolicyPuts cert-tracker's traffic inside a mesh and adds Kyverno policies that gate what cert-tracker-config is even allowed to declare
4 — ObservabilityInstruments the /exams and /status routes built here with OpenTelemetry traces and Prometheus metrics
5 — The PortalCatalogs cert-tracker in Backstage, using this same repo pair as the source of truth the portal displays
🎬 At Mission Control
🤖

Recon the Robot: Application cert-tracker: Synced, Healthy. Two flags on, one loop closed.

🦊

Foxy: "Closed" meaning what, exactly? I could still kubectl edit my way into trouble, right?

🤖

Recon: Try it. Scale the Deployment to zero. I'll have it back before you've finished typing the next command.

👺

Gizmo: Or — hear me out — just flip selfHeal off while you're debugging. Way less annoying when you're poking at a pod. 🤑

🐢

Timmy: Turn it off deliberately, with a comment saying why, and turn it back on before you walk away. "Annoying" is the reconciler doing exactly its job — the moment you silence it out of convenience is the moment drift starts hiding again.

🦫

Benny the Beaver: Same lesson as branch protection, one layer down. Lock the door on main, lock the door on the cluster — neither one's trying to slow you down, just making sure the only way in is the one everyone agreed to.

🤖

Recon: Everything past "it runs" is next. Part 2 makes releases safe, not just declared.

Milestones

☺ Like you're 10: Tick each box only once you've actually watched it happen on your own screen, not because the step "sounds right."

Work these in order — each depends on the repo and cluster state from the one before. Progress saves in this browser.

0 / 13 milestones complete
1Confirm prerequisites: a reachable cluster, Docker, git, and the argocd CLI
kubectl cluster-info against a local kind/minikube cluster or your own; confirm docker, git, gh and argocd are all on your PATH.
Done when: kubectl get nodes returns at least one Ready node.
2Scaffold cert-tracker: /healthz, /exams, /status, seeded with all 16 exams
Create package.json and src/index.js exactly as shown above.
Done when: npm start boots the service and curl localhost:8080/exams returns all 16 exams with passedOn: null.
3Add the Dockerfile, build and push the image
docker build -t ghcr.io/kubestronaut/cert-tracker:v0.1.0 . then docker push, swapping in your own namespace.
Done when: the image pulls successfully from wherever you pushed it.
4Structure cert-tracker-config: base/ + overlays/dev/
Create the deployment, service, kustomization and patch files exactly as shown above — no Namespace manifest tracked.
Done when: kustomize build apps/cert-tracker/overlays/dev renders clean YAML locally, replicas patched to 1.
5Push both repos to GitHub
gh repo create cert-tracker --public --source=. --remote=origin --push, then the same for cert-tracker-config.
Done when: both repos exist on GitHub with one commit each.
6Install Argo CD; confirm argocd-server and the application-controller StatefulSet are Ready
kubectl create namespace argocd, apply the stable install manifest, then the two rollout status commands above.
Done when: both commands return successfully.
7Retrieve the initial admin secret and log in with the argocd CLI
Decode argocd-initial-admin-secret, port-forward argocd-server, run argocd login.
Done when: argocd version --short returns both client and server versions.
8Apply bootstrap/project.yaml — the kubestronaut AppProject
kubectl apply -n argocd -f bootstrap/project.yaml, exactly as shown above.
Done when: argocd proj get kubestronaut shows the one scoped source repo and destination.
9Apply bootstrap/application.yaml with prune and selfHeal both explicit
kubectl apply -n argocd -f bootstrap/application.yaml, exactly as shown above.
Done when: argocd app get cert-tracker reports the kubestronaut project and both sync-policy flags true.
10Watch the first sync go Synced + Healthy, then curl the running app
argocd app wait cert-tracker --health --timeout 120, then port-forward and curl /healthz and /status.
Done when: both endpoints respond, pods show Running in kubectl get pods -n cert-tracker.
11Prove self-heal: scale to 0 by hand, watch it come back with no new commit
kubectl -n cert-tracker scale deployment/cert-tracker --replicas=0, then watch it.
Done when: replicas returns to 1 within seconds, unprompted.
12Prove prune: delete the Service from Git, watch it vanish, then revert it back
Remove service.yaml, commit, push, sync, confirm the 404 — then git revert, push, sync again.
Done when: the Service is gone after the delete commit and back after the revert, both without a manual kubectl apply.
13Say out loud what state you're leaving for Part 2
Confirm: cert-tracker Synced and Healthy, both sync-policy flags proven with your own hands, no resource in the cluster that skipped the config repo.
Done when: you can describe this state without looking anything up — it's the exact starting point Part 2 assumes.
🐢 Timmy's checkpoint

1. Why does cert-tracker-config live in a separate repository from cert-tracker's own source, rather than a /deploy folder inside the same repo? 2. syncPolicy.automated was set from the start, yet prune and selfHeal both had to be listed explicitly. What do they default to, and what would you have observed if you'd left selfHeal out? 3. Scaling the Deployment to 0 by hand corrected within seconds — much faster than the Service deletion took to prune. Why the difference in speed? 4. What does clusterResourceWhitelist: [] combined with syncOptions: [CreateNamespace=true] actually buy you, compared to just tracking a Namespace manifest in Git? 5. Name two things this part deliberately left out that a later part adds, and why they weren't needed yet.

Check your answers
  1. Keeping the GitOps repo separate scopes the state store to exactly what Argo CD should read. cert-tracker runs CI and takes pull requests carrying arbitrary code; cert-tracker-config holds nothing but declarative manifests. Scoping AppProject.sourceRepos to the config repo alone is a real, auditable trust boundary — not "trust the whole app repo, code and all."
  2. Both default to false, even with automated: {} present. Leaving out selfHeal would still auto-apply new commits, but a manual kubectl edit or scale would sit there un-reverted — Argo CD would report OutOfSync and simply wait, since detecting drift and correcting it are two separate switches.
  3. selfHeal watches the live cluster continuously through Kubernetes' own watch API, so a change on the cluster side is caught within seconds. Picking up a new commit in cert-tracker-config depends on the poll interval — roughly three minutes by default, or sooner with argocd app sync or a webhook — so a live-side correction is near-instant while a Git-side one waits on that interval.
  4. It stops any Application in the kubestronaut project from creating or touching cluster-scoped resources at all — no Namespaces, no CRDs, nothing beyond the one namespace already named in destinations. CreateNamespace=true still gets you the one namespace you need, as a controlled side effect of the sync itself, without granting the project the much larger, general power to create arbitrary cluster-scoped objects.
  5. Any two of: a service mesh and Kyverno policy admission (Part 3), progressive delivery via an Argo Rollout in place of the plain Deployment (Part 2), and OpenTelemetry/Prometheus instrumentation (Part 4). None of them were needed to prove the reconciliation loop itself works, which is this part's only job.

Part 1 gave you a reconciled, self-healing, prune-capable foundation — proved with your own hands, not taken on faith. Continue to Capstone Part 2 — Progressive Delivery, where the plain Deployment built here becomes a real canary rollout. Or step back to Build Your Cert Tracker — Start Here to see how this capstone's five parts fit the rest of the hands-on labs, and revisit GitOps Philosophy and Argo CD for the concepts behind what you just built.