Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

ContainerDays-Hamburg-2025

Content of this repo

Thank you!

Thank you for attending the ContainerDays Conference in Hamburg and the presentation on Calico Whisker: Free Tool for Kubernetes Policy Troubleshooting and Network Visibility by Tigera.

We hope you enjoyed the presentation and please feel free to download the slides from here.

We also encourage you to leave a feedback here, about the presentation or Project Calico. You feedback is valuable!


Overview

In these demos, we'll demonstrate how you can leverage Calico Whisker, a web-based tool for viewing and filtering flow logs, to troubleshoot connectivity issues and author and maintain Calico network security policies.


Demo 1 - Policy Misconfiguration

  • Diagram:

Demo 1 - Diagram

  • Scenario: An app is deployed and a Calico Network Policy restricts allowed traffic only to TCP port 443

  • Problem: Traffic to core-dns is not allowed anywhere else, preventing egress traffic against FQDNs to succeed.

  • Observability: Thanks to Calico Whisker you can see the reason for the traffic failure and you know what to change in the policy to fix the misconfiguration of the policy.


Demo 2 - Policy Misconfiguration with Staged Network Policy

  • Diagram:

Demo 2 - Diagram

  • Scenario: An app is deployed and a Calico Staged Network Policy is under testing to restrict allowed traffic only to TCP port 443

  • Problem: Traffic to core-dns is not allowed anywhere else, preventing egress traffic against FQDNs to succeed if the staged policy is enforced.

  • Observability: Thanks to Calico Whisker you can see the reason for the traffic that would fail if Staged Network Policy would have been enforced, and you know what to change in the policy to prevent it!


Demo 3 - Supply Chain Attack

  • Diagram:

Demo 3 - Diagram

  • Scenario: A simple app has 3 components: frontend, backend, database. The frontend microservice should talk to the backend, which then talks to the database. Then the frontend app is updated with a new version (v2).

  • Problem: The new version unexpectedly tries to talk directly to the database — which could be a sign of malicious code slipped into the supply chain.

  • Defense: Because of a strict Calico NetworkPolicy, only the expected traffic from frontend to backend is allowed. The unexpected traffic is blocked, and the pod fails instead of exfiltrating data.

  • Observability: Thanks to Calico Whisker you can see the reason for the pod failure.


Calico Whisker is a simple but powerful solution that allows you to navigate, filter and observe your network flows.

Whisker is the recommended way for anyone who wants to access Flows generated by Calico in their cluster. It is also worth noting that we offer a free forever membership to our open source users with calicocloud.io that adds a lot more features to their arsenal.


Additional resources


Before you begin...

Requirements

  • Docker. Installation steps here
  • Kind. Installation steps here
  • For optional steps Calicoctl. Installation steps here
  • For optional steps jq. Installation steps here

High level tasks

  • Deploy a cluster on your local machine with kind
  • Install Calico Open Source v3.30.3 on the local cluster
  • Demo 1 - Policy Misconfiguration:
    • Deploy blue app in the blue namespace
    • Deploy a Calico Network Policy to allow Egress traffic from blue namespace to all destinations on TCP port 443
    • See denied traffic in Whisker
    • Edit the policy and verify connectivity from Whisker
  • Demo 2 - Supply Chain Attack:
    • Deploy and expose backend and database apps
    • Build 2 images which simulate 2 different versions of the frontend application:
      • v1: simple curl against backend service (expected behaviour)
      • v2: simple curl against database service (simulate unexpected behaviour)
    • Deploy frontend app in the my-app namespace using v1 image
    • Deploy a Calico Network Policy to allow Egress traffic from frontend in the my-app namespace to backend service on TCP port 80 and UDP port 53 for DNS resolution
    • Verify connectivity from Whisker
    • Upgrade deployment to v2 and see the pod failing and going into CrashLoopBackOff
    • Verify the reason from Whisker
    • Optional - Troubleshoot and find the unexpected traffic

Prerequisites

  1. Check if docker is running on your machine:

    docker version
    
  2. Check if kind is installed on your machine:

    kind version
    
  3. Create a cluster using kind:

    cat > values_kind.yaml <<EOF
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
    - role: worker
    - role: worker
    networking:
      disableDefaultCNI: true
      podSubnet: 192.168.0.0/16
    EOF
    
    kind create cluster --config values_kind.yaml --name containerdays2025
    
  4. Wait for nodes to be running:

    watch kubectl get nodes
    

    Sample of output:

    containerdays2025-control-plane   NotReady   control-plane   23s   v1.33.1
    containerdays2025-worker          NotReady   <none>          11s   v1.33.1
    containerdays2025-worker2         NotReady   <none>          11s   v1.33.1
    
  5. Deploy Calico Open Source v3.30.3:

    kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/operator-crds.yaml
    kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/tigera-operator.yaml
    kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/custom-resources.yaml
    
  6. Wait for all Calico Components to be running:

    watch kubectl get tigerastatus
    

    Sample of output:

    NAME        AVAILABLE   PROGRESSING   DEGRADED   SINCE
    apiserver   True        False         False      59s
    calico      True        False         False      29s
    goldmane    True        False         False      34s
    ippools     True        False         False      89s
    whisker     True        False         False      49s
    

Demo 1 - Policy Misconfiguration

Click on "Details" to expand the section

Details

1. Deploy blue app in the blue namespace:

kubectl create ns blue
kubectl run blue -n blue --image nginx

2. Verify connectivity from blue pod to an external domain:

kubectl exec -it blue -n blue -- curl -s --connect-timeout 5 https://www.tigera.io -I | grep HTTP

Sample of output:

HTTP/2 200

Note: HTTP/2 200 or HTTP/2 30x mean that the connection to the server was successful.

3. Deploy a Calico Network Policy to allow all egress traffic from blue namespace to TCP port 443:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: blue-app-policy
  namespace: blue
spec:
  selector: run == "blue"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      ports:
      - 443
EOF

4. Verify connectivity again:

kubectl exec -it blue -n blue -- bash -c 'while true; do curl -sv --connect-timeout 2 https://tigera.io; sleep 2; done'

As you can see, the request is failing and you should receive an output similar to this:

* Resolving timed out after 2000 milliseconds
* Closing connection 0
* Resolving timed out after 2000 milliseconds
* Closing connection 0
* Resolving timed out after 2000 milliseconds
* Closing connection 0
^Ccommand terminated with exit code 130

5. Keep the loop running and, from another bash window, enable Port Forward to access Calico Whisker UI:

kubectl port-forward -n calico-system service/whisker 8081:8081

Then, from your browser, visit http://127.0.0.1:8081/flow-logs. You should see denied flows coming in:

image

You can already see that the deny flows have UDP 53 as destination. This is happening because any FQDN has to be resolved in an IP address. If the DNS resolution fails, in this case due to a misconfiguration of a policy, the original traffic cannot be successful.

NOTE: The DNS resolution is denied because, as per kubernetes standard, if no policy is scoping a pod and its direction, everything is allowed. However, if there is a policy scoping the pod and its direction, every traffic that is not explicitly allowed will be denied!

You can see extra details about the denied flow by clicking on it:

image

6. Edit the policy to allow DNS resolution:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: blue-app-policy
  namespace: blue
spec:
  selector: run == "blue"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      ports:
      - 443
      - 53
  - action: Allow
    protocol: UDP
    destination:
      ports:
      - 53
EOF

7. Verify connectivity again:

kubectl exec -it blue -n blue -- bash -c 'while true; do curl -s --connect-timeout 2 https://learn.tigera.io -I | grep HTTP; sleep 2; done'

The request should succeed now:

HTTP/2 200
HTTP/2 200
HTTP/2 200

Note: HTTP/2 200 or HTTP/2 30x mean that the connection to the server was successful.

8. Clean-up

kubectl delete namespace blue

Demo 2 - Policy Misconfiguration with Calico Staged Policy

Click on "Details" to expand the section

Details

1. Deploy green app in the green namespace:

kubectl create ns green
kubectl run green -n green --image nginx

2. Verify connectivity from green pod to an external domain:

kubectl exec -it green -n green -- curl -s --connect-timeout 5 https://www.tigera.io -I | grep HTTP

Sample of output:

HTTP/2 200

Note: HTTP/2 200 or HTTP/2 30x mean that the connection to the server was successful.

3. Deploy a Calico Staged Network Policy to allow all egress traffic from green namespace to TCP port 443:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: StagedNetworkPolicy
metadata:
  name: green-app-policy
  namespace: green
spec:
  selector: run == "green"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      ports:
      - 443
EOF

4. Verify connectivity again:

kubectl exec -it green -n green -- bash -c 'while true; do curl -s --connect-timeout 2 https://tigera.io -I | grep HTTP; sleep 2; done'

As you can see, the request is succeeding and you should receive an output similar to this:

HTTP/2 301
HTTP/2 301
HTTP/2 301

5. Keep the loop running and, from another bash window, enable Port Forward to access Calico Whisker UI:

kubectl port-forward -n calico-system service/whisker 8081:8081

Then, from your browser, visit http://127.0.0.1:8081/flow-logs and filter by the Source green. You should see all Allow flows coming in:

image

However, if you check the green-app-policy Calico Staged policy you have applied, only TCP 443 traffic is allowed. Infact, if you expand the flow with destination UDP 53 you will see that the Staged policy would have denied such a traffic (you can find this info under Policies > Pending):

image

This is happening because any FQDN has to be resolved in an IP address. If the DNS resolution fails, in this case due to a misconfiguration of a policy, the original traffic cannot be successful.

NOTE: The DNS resolution is denied because, as per kubernetes standard, if no policy is scoping a pod and its direction, everything is allowed. However, if there is a policy scoping the pod and its direction, every traffic that is not explicitly allowed will be denied!

Fortunately, this is a Staged Network Policy!!!

6. Edit the policy to allow DNS resolution:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: StagedNetworkPolicy
metadata:
  name: green-app-policy
  namespace: green
spec:
  selector: run == "green"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      ports:
      - 443
      - 53
  - action: Allow
    protocol: UDP
    destination:
      ports:
      - 53
EOF

7. Verify connectivity again:

kubectl exec -it green -n green -- bash -c 'while true; do curl -s --connect-timeout 2 https://learn.tigera.io -I | grep HTTP; sleep 2; done'

The request should succeed again:

HTTP/2 302
HTTP/2 302
HTTP/2 302

and the staged network policy should no longer show a deny action:

image

Note: HTTP/2 200 or HTTP/2 30x mean that the connection to the server was successful.

8. Enforce a Calico Network Policy and delete the Staged Network Policy:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: green-app-policy
  namespace: green
spec:
  selector: run == "green"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      ports:
      - 443
      - 53
  - action: Allow
    protocol: UDP
    destination:
      ports:
      - 53
EOF

kubectl delete stagednetworkpolicies green-app-policy -n green

9. Clean-up

kubectl delete namespace green

Demo 3 - Supply Chain Attack Investigation

Click on "Details" to expand the section

Details

1. Deploy and expose database and backend microservices in the my-app namespace:

kubectl create namespace my-app
kubectl run backend -n my-app --image nginx
kubectl expose pod backend -n my-app --port 80
kubectl run database -n my-app --image nginx
kubectl expose pod database -n my-app --port 80

2. Build 2 images which simulate 2 different versions of the frontend application:

  • v1: simple curl against backend service (expected behaviour)
  • v2: simple curl against database service (simulate unexpected behaviour)

NOTE: This Dockerfile builds a minimal test container based on Alpine Linux 3.20.

  • It installs curl (using apk add --no-cache curl).
  • When the container starts, its default command runs:
    • A curl request with a 5-second connection timeout.
    • Then it sleeps for an hour (sleep 3600) to keep the container alive.
mkdir containerDays2025

mkdir containerDays2025/frontend-v1
cat <<'EOF' > containerDays2025/frontend-v1/Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl
CMD ["sh", "-c", "curl -s --connect-timeout 5 http://backend.my-app && sleep 3600"]
EOF

mkdir -p containerDays2025/frontend-v2
cat <<'EOF' > containerDays2025/frontend-v2/Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl
CMD ["sh", "-c", "curl -s --connect-timeout 5 http://database.my-app && curl -s --connect-timeout 5 http://backend.my-app && sleep 3600"]
EOF

KIND_CLUSTER=$(kind get clusters)

docker build -t frontend:v1 ./containerDays2025/frontend-v1
docker build -t frontend:v2 ./containerDays2025/frontend-v2

kind load docker-image frontend:v1 --name $KIND_CLUSTER
kind load docker-image frontend:v2 --name $KIND_CLUSTER

3. Deploy frontend in the my-app namespace using v1 image (image that sends curl requests to backend service):

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: frontend:v1
EOF

4. Deploy a Calico Network Policy to allow all egress traffic from frontend in the my-app namespace to TCP port 80, UDP port 53 and destination backend microservice:

cat << EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: frontend-policy
  namespace: my-app
spec:
  selector: app == "frontend"
  types:
  - Egress
  egress:
  - action: Allow
    protocol: TCP
    destination:
      selector: run == "backend"
      ports:
      - 80
  - action: Allow
    protocol: UDP
    destination:
      ports:
      - 53
EOF

5. Verify connectivity:

kubectl logs deploy/frontend -n my-app

The output should be similar to this:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Note: We got a reply from the nginx pod which means that the connection to the server was successful.

6. Upgrade the application to v2 and monitor the rollout of the pod:

kubectl set image deployment/frontend frontend=frontend:v2 -n my-app

kubectl get pod -n my-app -w

The output should be similar to this:

NAME                      READY   STATUS              RESTARTS   AGE
frontend-59f6fb945-zx8sn    0/1     ContainerCreating   0          0s
frontend-78c69ff454-s5m65   1/1     Running             0          3m59s
frontend-59f6fb945-zx8sn    0/1     ContainerCreating   0          1s
frontend-59f6fb945-zx8sn    1/1     Running             0          1s
frontend-78c69ff454-s5m65   1/1     Terminating         0          4m
frontend-59f6fb945-zx8sn    0/1     Error               0          6s
frontend-59f6fb945-zx8sn    1/1     Running             1 (1s ago)   7s
frontend-59f6fb945-zx8sn    0/1     Error               1 (6s ago)   12s
frontend-59f6fb945-zx8sn    0/1     CrashLoopBackOff    1 (14s ago)   25s
frontend-59f6fb945-zx8sn    1/1     Running             2 (15s ago)   26s
frontend-59f6fb945-zx8sn    0/1     Error               2 (20s ago)   31s
frontend-78c69ff454-s5m65   1/1     Terminating         0             4m30s
frontend-78c69ff454-s5m65   0/1     Error               0             4m30s
frontend-78c69ff454-s5m65   0/1     Error               0             4m31s
frontend-78c69ff454-s5m65   0/1     Error               0             4m31s
frontend-59f6fb945-zx8sn    0/1     CrashLoopBackOff    2 (11s ago)   41s

The new pod is failing and we can leverage Calico Whisker to understand why.

7. Enable Port Forward to access Calico Whisker UI:

kubectl port-forward -n calico-system service/whisker 8081:8081

Then, from your browser, visit http://127.0.0.1:8081/flow-logs. You should see denied flows coming in:

NOTE: If you don't see deny flows, restart the pod while Whisker UI is open.

image

As per kubernetes standard, if no policy is scoping a pod and its direction, everything is allowed. However, if there is a policy scoping the pod and its direction, every traffic that is not explicitly allowed will be denied!

In this case the v2 app is misbehaving compared to the v1 and the policy is blocking this unexpected traffic. The frontend app is trying to connect directly to the database, which is not what we want!

You can see extra details about the denied flow by clicking on it:

image

Below the diagram of what is happening:

Diagram

8. Optional: You could investigate this further to see why the pod is generating unexpected traffic.

NOTE: You need calicoctl and jq for this part!

Delete the existing frontend pod and sniff packets on the node and pod interface of the new pod:

echo "Restarting frontend deplyoment in my-app namespace"
kubectl rollout restart deploy frontend -n my-app
sleep 3
POD_NAME=$(kubectl get pod -n my-app -l app=frontend -o jsonpath='{.items[0].metadata.name}')
echo "The new pod name is: $POD_NAME"
sleep 3
NODE_NAME=$(kubectl get pod -n my-app $POD_NAME -o jsonpath='{.spec.nodeName}')
echo "The pod is on this node: $NODE_NAME"
sleep 3
IFACE_NAME=$(calicoctl get wep -n my-app -o json \
  | jq -r --arg pod "$POD_NAME" --arg node "$NODE_NAME" \
    '.items[] | select(.spec.pod==$pod and .spec.node==$node) | .spec.interfaceName' \
  | head -n1)
echo "The pod interface name is: $IFACE_NAME"
sleep 3
echo "Starting debug pod to sniff packets on the new pod's interface..."
kubectl debug -n my-app -it node/$NODE_NAME --image=nicolaka/netshoot -- bash -c "tcpdump -ni $IFACE_NAME"

Sample of output:

Restarting frontend deplyoment in my-app namespace
deployment.apps/frontend restarted
The new pod name is: frontend-6555d664b6-28g67
The pod is on this node: containerdays2025-worker
The pod interface name is: caliaac3e45f346
Starting debug pod to sniff packets on the new pod's interface...
--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".
Creating debugging pod node-debugger-containerdays2025-worker-xn9zn with container debugger on node containerdays2025-worker.
If you don't see a command prompt, try pressing enter.
10:36:14.267364 IP 192.168.7.5.43590 > 10.96.0.10.53: 41794+ [1au] A? database.my-app.my-app.svc.cluster.local. (81)
10:36:14.267499 IP 192.168.7.5.43590 > 10.96.0.10.53: 21126+ [1au] AAAA? database.my-app.my-app.svc.cluster.local. (81)
10:36:14.268029 IP 10.96.0.10.53 > 192.168.7.5.43590: 21126 NXDomain*- 0/1/1 (174)
10:36:14.268110 IP 10.96.0.10.53 > 192.168.7.5.43590: 41794 NXDomain*- 0/1/1 (174)
10:36:14.268159 IP 192.168.7.5.43590 > 10.96.0.10.53: 19925+ [1au] A? database.my-app.svc.cluster.local. (62)
10:36:14.268219 IP 192.168.7.5.43590 > 10.96.0.10.53: 28072+ [1au] AAAA? database.my-app.svc.cluster.local. (62)
10:36:14.268350 IP 10.96.0.10.53 > 192.168.7.5.43590: 28072*- 0/1/1 (155)
10:36:14.268415 IP 10.96.0.10.53 > 192.168.7.5.43590: 19925*- 1/0/1 A 10.96.83.142 (111)
10:36:14.268516 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089082303 ecr 0,nop,wscale 7], length 0
10:36:15.299963 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089083335 ecr 0,nop,wscale 7], length 0
10:36:16.323829 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089084359 ecr 0,nop,wscale 7], length 0
10:36:17.347923 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089085383 ecr 0,nop,wscale 7], length 0
10:36:18.371927 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089086407 ecr 0,nop,wscale 7], length 0

You can see that the new pod resoved the IP of the database service:

10:36:14.268159 IP 192.168.7.5.43590 > 10.96.0.10.53: 19925+ [1au] A? database.my-app.svc.cluster.local. (62)
10:36:14.268219 IP 192.168.7.5.43590 > 10.96.0.10.53: 28072+ [1au] AAAA? database.my-app.svc.cluster.local. (62)
10:36:14.268350 IP 10.96.0.10.53 > 192.168.7.5.43590: 28072*- 0/1/1 (155)
10:36:14.268415 IP 10.96.0.10.53 > 192.168.7.5.43590: 19925*- 1/0/1 A 10.96.83.142 (111)

and is sending SYN packets to it:

10:36:14.268516 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089082303 ecr 0,nop,wscale 7], length 0
10:36:15.299963 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089083335 ecr 0,nop,wscale 7], length 0
10:36:16.323829 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089084359 ecr 0,nop,wscale 7], length 0
10:36:17.347923 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089085383 ecr 0,nop,wscale 7], length 0
10:36:18.371927 IP 192.168.7.5.46520 > 10.96.83.142.80: Flags [S], seq 4007878619, win 65445, options [mss 65445,sackOK,TS val 4089086407 ecr 0,nop,wscale 7], length 0

which is not from what we expected!

9. Clean-up

kubectl delete namespace my-app

Try Calico Cloud Free Tier Forever!

Sign up for Calico Cloud Free Tier for Enterprise Grade Observability: https://www.calicocloud.io/home

Our Free option is ideal for one user and one cluster already running open source Calico. For teams who need to secure multiple clusters, use more advanced features, or retain logs beyond 24 hours, we offer a Pay-as-you-go version. Compare the plans here.

For more information, please visit Calico Cloud Free.


Lab clean-up

Delete the kind cluster with this command:

kind delete clusters containerdays2025

Congratulations! You have completed Calico Whisker ContainerDays Hamburg 2025 demo! Please leave a feedback here

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors