Skip to content

GitOps with ArgoCD

Naftiko Skipper is designed to work with ArgoCD. The operator manages the capability lifecycle inside Kubernetes — ArgoCD manages what gets deployed from Git.


How It Works

Developer pushes capability manifests to Git
ArgoCD detects change and syncs to cluster
Naftiko Skipper reconciles the Capability CR
Running capability with full observability

Users never run kubectl apply — they just push to Git.


Platform Setup (done once by the platform team)

The skipper repo ships three ArgoCD Applications applied in sync-wave order:

Wave Application Source
-1 naftiko-crds config/crds/manifests/
0 naftiko-defaults config/defaults/manifests/
1 naftiko-skipper helm/naftiko-skipper/

Install ArgoCD

kubectl create namespace argocd

kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml \
  --server-side --force-conflicts

kubectl rollout status deployment/argocd-server \
  -n argocd --timeout=120s

Expose the UI

Get the admin password first:

kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath="{.data.password}" | base64 -d ; echo

Choose the method that matches your cluster:

Any cluster — port-forward (simplest):

kubectl port-forward svc/argocd-server 8080:443 -n argocd &
# → open https://localhost:8080

minikube:

kubectl patch svc argocd-server -n argocd \
  --type='json' \
  -p='[{"op":"replace","path":"/spec/type","value":"NodePort"},
       {"op":"add","path":"/spec/ports/0/nodePort","value":30080}]'

MINIKUBE_IP=$(minikube ip)
docker run -d --name argocd-bridge --restart=always \
  -p 30080:30080 --network minikube \
  alpine/socat TCP-LISTEN:30080,fork,reuseaddr TCP:${MINIKUBE_IP}:30080
# → open http://localhost:30080

kind:

kubectl patch svc argocd-server -n argocd \
  --type='json' \
  -p='[{"op":"replace","path":"/spec/type","value":"NodePort"},
       {"op":"add","path":"/spec/ports/0/nodePort","value":30080}]'

NODE_IP=$(kubectl get nodes \
  -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
NETWORK=$(docker network ls | grep kind | awk '{print $2}')

docker run -d --name argocd-bridge --restart=always \
  -p 30080:30080 --network ${NETWORK} \
  alpine/socat TCP-LISTEN:30080,fork,reuseaddr TCP:${NODE_IP}:30080
# → open http://localhost:30080

Cloud cluster (EKS, GKE, AKS) — LoadBalancer:

kubectl patch svc argocd-server -n argocd \
  --type='json' \
  -p='[{"op":"replace","path":"/spec/type","value":"LoadBalancer"}]'

kubectl get svc argocd-server -n argocd \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
# → open http://<hostname>

Login with admin / password from above.

Install the CLI and login

# macOS
brew install argocd

# Linux
curl -sSL -o argocd-linux-amd64 \
  https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd

# Login
ARGOCD_PASSWORD=$(kubectl get secret argocd-initial-admin-secret \
  -n argocd -o jsonpath="{.data.password}" | base64 -d)

argocd login localhost:YOUR_PORT \
  --username admin \
  --password $ARGOCD_PASSWORD \
  --insecure

Apply the platform applications

# Wave -1: CRDs
kubectl apply -f \
  https://raw.githubusercontent.com/naftiko/fleet/main/skipper/argocd/crds-application.yaml

kubectl wait --for=condition=Established \
  crd/capabilities.naftiko.io \
  crd/capabilityclasses.naftiko.io \
  --timeout=60s

# Wave 0: CapabilityClass defaults (standard, premium, dev)
kubectl apply -f \
  https://raw.githubusercontent.com/naftiko/fleet/main/skipper/argocd/defaults-application.yaml

# Wave 1: Operator via Helm
kubectl apply -f \
  https://raw.githubusercontent.com/naftiko/fleet/main/skipper/argocd/operator-application.yaml

kubectl rollout status deployment/naftiko-skipper \
  -n naftiko-system --timeout=90s

Verify all three are Synced:

kubectl get applications -n argocd
# NAME               SYNC-STATUS   HEALTH
# naftiko-crds       Synced        Healthy
# naftiko-defaults   Synced        Healthy
# naftiko-skipper    Synced        Healthy

User Setup (done once per capabilities repository)

Each team maintains their own Git repository of capabilities. One ApplicationSet automatically creates one ArgoCD Application per capability directory.

1. Connect your repo

argocd repo add https://github.com/USER/my-capabilities.git

2. Apply the ApplicationSet

curl -sO https://raw.githubusercontent.com/naftiko/fleet/main/skipper/capabilities/applicationset.template.yaml

sed \
  -e 's|MY_CAPABILITIES|my-capabilities|g' \
  -e 's|REPO_URL|https://github.com/USER/my-capabilities.git|g' \
  applicationset.template.yaml | kubectl apply -f -

ArgoCD now watches the repo and creates one Application per directory under capabilities/. This command is run once — all future capabilities are deployed via Git push.


Capabilities Repository Structure

my-capabilities/
└── capabilities/
    ├── hello-world/
    │   ├── configmap.yaml      ← ikanos spec as a Kubernetes ConfigMap
    │   └── capability.yaml     ← Capability CR using specRef
    └── shipyard/
        ├── configmap.yaml
        ├── capability.yaml
        ├── bind-secret.yaml    ← Kubernetes Secret for binds
        ├── import-registry.yaml ← ConfigMap for registry import
        └── import-legacy.yaml  ← ConfigMap for legacy import

configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: hello-world-spec
  namespace: default
data:
  capability.yaml: |
    ikanos: "1.0.0-alpha4"
    info:
      display: Hello World
      labels:
        naftiko.io/tier: standard
    capability:
      exposes:
        - type: rest
          address: "0.0.0.0"
          port: 3001
          namespace: tutorial
          resources:
            hello:
              path: /hello
              operations:
                get-hello:
                  method: GET
                  outputParameters:
                    - name: message
                      type: string
                      value: "Hello, World!"
        - type: control
          address: "0.0.0.0"
          port: 9090
          observability:
            enabled: true
            metrics:
              local:
                enabled: true

capability.yaml

apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
  name: hello-world
  namespace: default
  labels:
    naftiko.io/tier: standard
  annotations:
    argocd.argoproj.io/sync-wave: "1"   # apply after ConfigMap (wave 0)
spec:
  specRef:
    configMap: hello-world-spec

Deploying a Capability

mkdir -p capabilities/my-new-capability

# create configmap.yaml and capability.yaml

git add capabilities/my-new-capability/
git commit -m "feat: add my-new-capability"
git push

ArgoCD detects the new directory → creates cap-my-new-capability → Skipper reconciles the Capability CR → capability is running in minutes.


Updating a Capability

# Edit the spec in configmap.yaml
nano capabilities/hello-world/configmap.yaml

git add capabilities/hello-world/configmap.yaml
git commit -m "fix: update hello-world spec"
git push

ArgoCD syncs the updated ConfigMap. Skipper detects the drift and reconciles the Deployment with the new spec.


Removing a Capability

git rm -r capabilities/my-new-capability/
git commit -m "remove: my-new-capability"
git push

ArgoCD prunes the Capability CR and its ConfigMap. Skipper's OwnerReference cascade-deletes the Deployment, Service, and ServiceMonitor automatically.


Sync Waves — Why Order Matters

ArgoCD sync waves ensure resources are applied in dependency order:

Wave -1  CRDs installed       → Kubernetes learns Capability and CapabilityClass types
Wave  0  CapabilityClasses    → standard / premium / dev tiers are available
Wave  1  Operator running     → Skipper watches for Capability CRs

Within a capability directory, use sync-wave annotations to ensure secrets and ConfigMaps exist before the Capability CR is applied:

# bind-secret.yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"

# configmap.yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"

# capability.yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "1"

Manual Reconcile

To force an immediate reconcile without changing the spec:

kubectl annotate capability <name> \
  reconcile-at=$(date +%s) --overwrite -n default

Useful after updating operator env vars (e.g. OTEL endpoint) or after restoring a deleted child resource.