# Verify Your Install

Run the six checks in order. Each check depends on the previous one, which helps
you identify the source of a failure. Checks one through five require only the
installation. Check six also requires a Zuplo API key and project.

## Check the deployments

Gate on readiness before testing anything over the network.

```bash
kubectl wait --for=condition=Available deployment --all \
  -n zuplo-system --timeout=5m
kubectl wait --for=condition=Available deployment --all \
  -n zuplo --timeout=5m
```

```
deployment.apps/control-plane condition met
deployment.apps/deployer condition met
deployment.apps/gateway condition met
deployment.apps/storage condition met
deployment.apps/acme-forwarder condition met
deployment.apps/custom-error-pages condition met
deployment.apps/zuplo-cert-manager condition met
deployment.apps/zuplo-cert-manager-cainjector condition met
deployment.apps/zuplo-cert-manager-webhook condition met
deployment.apps/zuplo-haproxy-ingress condition met
deployment.apps/zuplo-kube-prometheus-stac-operator condition met
deployment.apps/zuplo-kube-state-metrics condition met
deployment.apps/zuplo-prometheus-adapter condition met
```

The `control-plane`, `deployer`, `gateway`, `storage`, and `acme-forwarder`
deployments are part of the Zuplo management plane. The other deployments come
from subcharts.

## Check the ingress

Send a request for a hostname that has no Ingress. This checks that the load
balancer can reach HAProxy.

```bash
LB=$(kubectl get svc zuplo-haproxy-ingress -n zuplo \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

curl -i "http://$LB/" -H 'Host: no-such-deployment.example.com'
```

```
HTTP/1.1 404 Not Found
content-type: text/html

{
  "type": "https://httpproblems.com/http-status/404",
  "title": "Not Found",
  "status": 404,
  "detail": "This Zuplo project does not exist"
}
```

The expected result is `404 Not Found` from HAProxy's default backend. A timeout
or refused connection means that traffic isn't reaching the ingress controller.

Use a hostname without an Ingress for this check. Hosts with an Ingress redirect
port 80 to HTTPS and return a `302` instead.

:::note

Requests for hosts with an Ingress are redirected from port 80 to 443. The
separate challenge Ingress created by cert-manager continues to serve ACME
challenge paths on port 80.

:::

## Check the control plane

The control plane turns your values into a `Configuration` resource, then
creates the management API's Ingress from it.

```bash
kubectl get configuration default -n zuplo-system -o yaml
```

```yaml
spec:
  account:
    name: acme-corp
  builder:
    registry: us-docker.pkg.dev/acme-corp/zuplo-gateways
    secretName: builder-secret
  deployments:
    subdomain: api.example.com
    certificates:
      certManager:
        issuer: zuplo-cluster-issuer
  managementApi:
    hostname: zuplo-admin.example.com
  ingressControllers:
    - haproxy
```

Confirm every value matches what you put in `zuplo-values.yaml`. A
single-cluster installation has no `workers` or `authorization` entry. Those
settings are for installations that distribute deployments across clusters.

Then confirm the reconciler acted on the rest:

```bash
kubectl get ingress -n zuplo-system
kubectl get clusterissuer
```

```
NAME              CLASS     HOSTS                       ADDRESS        PORTS     AGE
gateway-haproxy   haproxy   zuplo-admin.example.com     203.0.113.24   80, 443   62s

NAME                   READY   STATUS                                                 AGE
zuplo-cluster-issuer   True    The ACME account was registered with the ACME server    116s
```

`READY: True` for `zuplo-cluster-issuer` means that the chart registered an
account with the certificate authority. It doesn't indicate whether a
certificate has been issued.

:::note

`Configuration` doesn't report a status. Check the Ingress and ClusterIssuer
resources that it produces instead.

:::

## Check certificate issuance

Check that the certificate authority issued a certificate for the management API
hostname:

```bash
kubectl get certificate -A
```

```
NAMESPACE      NAME                   READY   SECRET                 AGE
zuplo-system   gateway-cert-manager   True    gateway-cert-manager   4m
```

`READY: True` means that the certificate authority validated your hostname and
issued a certificate. If it remains `False` for more than a few minutes, see
[Troubleshooting](./troubleshooting.md).

cert-manager retries automatically after you fix DNS. You don't need to restart
it.

## Check the management API

Send an unauthenticated request to check DNS, the load balancer, HAProxy, the
certificate, and authentication:

```bash
curl -i https://zuplo-admin.example.com/v1/deployments
```

```
HTTP/2 401

{
  "type": "https://httpproblems.com/http-status/401",
  "title": "Unauthorized",
  "status": 401,
  "detail": "No Authorization Header",
  ...
}
```

The expected result is `401 Unauthorized`. It confirms that `curl` completed a
TLS handshake with a trusted certificate and that the gateway requires an API
key. A TLS error indicates a certificate problem. A timeout indicates a DNS or
load balancer problem.

Confirm the certificate is the real one rather than HAProxy's self-signed
fallback:

```bash
echo | openssl s_client -connect zuplo-admin.example.com:443 \
  -servername zuplo-admin.example.com 2>/dev/null |
  openssl x509 -noout -subject -issuer
```

```
subject=CN=zuplo-admin.example.com
issuer=C=US, O=Let's Encrypt, CN=YR2
```

An issuer of `CN=kubernetes-ingress-ca` means HAProxy is serving its built-in
fallback certificate because no issued certificate is available. Repeat the
certificate issuance check.

## Deploy and call a project

The final check builds a gateway image in your cluster, stores it in your
registry, and serves it from your infrastructure.

Deploy your project with the Zuplo CLI. The `project` field in `zuplo.jsonc`
selects the project, and Zuplo routes the deployment to your cluster:

```bash
export ZUPLO_API_KEY='<your Zuplo API key>'

npx zuplo deploy
```

Zuplo compiles the project, then hands the compiled bundle to your cluster's
management API, which builds the container image and rolls it out.

While the command runs, watch the build Job in your cluster:

```bash
kubectl get jobs -n zuplo-system -w
```

```
NAME                       STATUS     COMPLETIONS   DURATION   AGE
build-basic-main-ca9748b-ztcwzdd   Running    0/1     32s   32s
build-basic-main-ca9748b-ztcwzdd   Complete   1/1     58s   63s
```

Confirm that the deployment uses an image from your registry:

```bash
kubectl get deploy -n zuplo -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[0].image}{"\n"}{end}'
```

```
basic-main-ca9748b	us-docker.pkg.dev/acme-corp/zuplo-gateways/basic-main-ca9748b:20260806-172436
```

Ask the management API what it is serving:

```bash
curl -H "Authorization: Bearer $ZUPLO_API_KEY" \
  https://zuplo-admin.example.com/v1/deployments
```

```json
{
  "data": [
    {
      "projectName": "basic",
      "deploymentName": "basic-main-ca9748b-haproxy",
      "deploymentUrl": "https://basic-main-ca9748b.api.example.com"
    }
  ]
}
```

Call the deployed API:

```bash
curl https://basic-main-ca9748b.api.example.com/hello
```

The first request to a deployment can fail while cert-manager issues its
certificate. Retry the request for up to a minute.

:::note

The CLI can misreport the status of deployments to self-hosted clusters. See
[Troubleshooting](./troubleshooting.md#failed-to-deploy-the-environment-but-the-cluster-shows-the-build-succeeding).
Use the in-cluster checks to confirm the result.

:::

## Resolve a failed check

[Troubleshooting](./troubleshooting.md) lists errors from these checks and their
causes.
