diff --git a/.github/workflows/binary-release.yml b/.github/workflows/binary-release.yml new file mode 100644 index 0000000..6c58a16 --- /dev/null +++ b/.github/workflows/binary-release.yml @@ -0,0 +1,39 @@ +name: Release binaries + +# On a v* tag, build the CLI for linux (amd64, arm64) and darwin (amd64, arm64) and attach the archives to +# the GitHub release of that tag — so a pipeline (E10: policy as code) can `curl` the binary instead of building +# it. No goreleaser: four `go build`s and `gh release upload`. +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + binaries: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Build + run: | + set -eu + VERSION="${GITHUB_REF_NAME#v}" + mkdir -p dist + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do + os="${target%/*}"; arch="${target#*/}" + CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w -X main.version=${GITHUB_REF_NAME#v}" -o "dist/cf2cnp" ./cmd/cf2cnp + tar -C dist -czf "dist/cf2cnp_${VERSION}_${os}_${arch}.tar.gz" cf2cnp + rm dist/cf2cnp + done + (cd dist && sha256sum *.tar.gz > "cf2cnp_${VERSION}_checksums.txt") + ls -l dist + - name: Attach to the release of this tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "cf2cnp $GITHUB_REF_NAME" --notes "cf2cnp $GITHUB_REF_NAME — see the chart release for Helm." + gh release upload "$GITHUB_REF_NAME" dist/* --clobber diff --git a/.github/workflows/chart-releaser.yml b/.github/workflows/chart-releaser.yml new file mode 100644 index 0000000..7cd6369 --- /dev/null +++ b/.github/workflows/chart-releaser.yml @@ -0,0 +1,32 @@ +name: Release Helm Chart (GitHub Pages) + +# Publishes helm/cf2cnp as a classic Helm repository on the gh-pages branch (index.yaml) and as a GitHub +# release carrying the chart .tgz, whenever the chart version on the default branch changes. This is the +# fork's release path while the upstream pull request is pending; the OCI push (helm-publish.yml) stays. +on: + push: + branches: [develop, main] + paths: ['helm/**', '.github/workflows/chart-releaser.yml'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Configure git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - uses: azure/setup-helm@v4 + - uses: helm/chart-releaser-action@v1.7.0 + with: + charts_dir: helm + skip_existing: true + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3146173 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +# CI — the checks a change must pass before it is released. Three layers guard the spec: +# 1. `go test ./...`: unit tests, and TestGolden — the /generate answer for 14 real Hubble captures must be byte for +# byte what 0.6.3 answered (a deliberate output change regenerates the fixtures in the same commit). +# 2. Cilium's own Sanitize() runs inside those tests (internal/policy.Validate) — the agent's admission checks. +# 3. The CRD schema of the pinned Cilium module: the embedded copy must be identical to the module's, and every +# golden output must validate against it offline (kubectl-validate --local-crds, the same mechanism the PoC's +# policy-PR template uses). +# And one line that must never appear: the Kubernetes client machinery in the dependency graph (18 MB → 94 MB). +name: CI + +on: + push: + branches: [develop, main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Vet and test (unit, golden, Sanitize) — and say what ran, in the job summary + run: | + go vet ./... + set -o pipefail + go test ./... -count=1 -json | tee /tmp/go-test.json | grep -E '"Action":"(fail)"' || true + python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import json, collections + pk = collections.defaultdict(lambda: collections.Counter()); golden = []; failed = [] + for l in open('/tmp/go-test.json'): + try: e = json.loads(l) + except ValueError: continue + if e.get('Test') and e.get('Action') in ('pass', 'fail', 'skip'): + pk[e['Package'].split('/')[-1]][e['Action']] += 1 + if e['Action'] == 'fail': failed.append(e['Package'].split('/')[-1] + ' ' + e['Test']) + if e['Test'].startswith('TestGolden/'): golden.append((e['Test'][len('TestGolden/'):], e['Action'])) + print('## Tests\n'); print('| package | passed | failed | skipped |'); print('|---|---|---|---|') + for k, c in sorted(pk.items()): print(f"| {k} | {c['pass']} | {c['fail']} | {c['skip']} |") + print(f"\n**{sum(c['pass'] for c in pk.values())} passed, {sum(c['fail'] for c in pk.values())} failed.**") + if failed: print('\nFailed: ' + ', '.join(failed)) + print('\n## The 14 golden captures (byte-for-byte against 0.6.3, deliberate changes named in internal/testdata/golden/README.md)\n') + print('| capture | result |'); print('|---|---|') + for n, a in sorted(golden): print(f"| {n} | {'✅ identical' if a == 'pass' else '❌ differs'} |") + PY + ! grep -q '"Action":"fail"' /tmp/go-test.json + - name: The embedded CRD is the pinned module's, byte for byte + run: | + set -eu + dir=$(go list -m -f '{{.Dir}}' github.com/cilium/cilium) + cmp internal/crd/ciliumnetworkpolicies.yaml "$dir/pkg/k8s/apis/cilium.io/client/crds/v2/ciliumnetworkpolicies.yaml" + test "$(cat internal/crd/VERSION)" = "$(go list -m -f '{{.Version}}' github.com/cilium/cilium)" + echo "CRD: cilium $(cat internal/crd/VERSION)" + - name: Every golden output validates against the CRD schema (offline) + run: | + set -eu + crds=$(mktemp -d); cp internal/crd/ciliumnetworkpolicies.yaml "$crds/" + echo "## Every golden output against the CRD schema (kubectl-validate, cilium $(cat internal/crd/VERSION))" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY"; echo "| file | documents | schema |" >> "$GITHUB_STEP_SUMMARY"; echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY" + for f in internal/testdata/golden/*/expected.yaml; do + n=$(grep -c '^kind: CiliumNetworkPolicy' "$f") + if go run sigs.k8s.io/kubectl-validate@v0.0.4 --local-crds "$crds" "$f"; then r="✅ valid"; else r="❌ INVALID"; fi + echo "| $(basename "$(dirname "$f")") | $n | $r |" >> "$GITHUB_STEP_SUMMARY" + done + - name: The Kubernetes client machinery stays out of the binary + # Two small helpers ride along with Cilium's policy API (k8s.io/client-go/util/jsonpath and its forked template, + # via github.com/cilium/cilium/pkg/command); the line not to cross is the client itself and Cilium's k8s client + # package, which pull the apiserver machinery in (18 MB → 94 MB, docs/CRD-SPEC-FORENSICS.md §3). The size ceiling + # is the real guard. + run: | + set -eu + if go list -deps ./cmd/cf2cnp | grep -E '^k8s.io/client-go/(kubernetes|rest|tools/clientcmd|dynamic|discovery)|^github.com/cilium/cilium/pkg/k8s/client($|/)'; then + echo "the client packages are imported — see docs/CRD-SPEC-FORENSICS.md §3"; exit 1 + fi + go build -trimpath -ldflags="-s -w" -o /tmp/cf2cnp-size ./cmd/cf2cnp + size=$(stat -c %s /tmp/cf2cnp-size) + echo "binary: $size bytes" + test "$size" -lt 40000000 || { echo "the binary passed 40 MB: something heavy joined the graph"; exit 1; } + - name: The binary names the spec it supports, and validates every golden the way the agent would + run: | + go build -ldflags="-X main.version=ci" -o /tmp/cf2cnp ./cmd/cf2cnp + /tmp/cf2cnp version + /tmp/cf2cnp validate internal/testdata/golden/*/expected.yaml | tee /tmp/validate.txt + { echo "## cf2cnp validate (Sanitize, ObjectMeta, the protocol enum, apiVersion/kind, nodeSelector, specs)"; echo; echo '```'; /tmp/cf2cnp version; echo "$(grep -c ' ok$' /tmp/validate.txt) documents accepted, $(grep -vc ' ok$' /tmp/validate.txt) refused"; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + - name: The policy-PR template's calls work (merge takes --l7) + run: | + set -eu + out=$(mktemp -d) + /tmp/cf2cnp generate --input internal/testdata/l7-http-request.json --output "$out" + existing=$(find "$out" -name '*.yaml' | head -1) + /tmp/cf2cnp merge --l7 --existing "$existing" --input internal/testdata/l7-http-request.json --output "$out/merged.yaml" + grep -q 'http:' "$out/merged.yaml" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9bd957e..2200e0b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -61,4 +61,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || github.sha }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b1f069d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog — the fork's releases + +Every release below was built for a Cilium 1.20.1 ClusterMesh lab and proven there before it was tagged; each row +names the demo of [cilium-implementation-poc](https://github.com/ephico2real2/cilium-implementation-poc) that +exercises it (its README quotes the recorded transcript, the Hubble verdicts and the screenshots), and the review +record where two independent reviewers (Cursor, Codex) went through the claims. The CI of this repository prints a +test summary on every push (the workflow's job summary): the unit tests by package, the 14 golden captures that must +answer byte for byte, the CRD check, and `cf2cnp validate` over every golden output. + +## 0.7.0 — 2026-09-13 · Cilium's own policy types, validated the way the agent validates + +- **The policy document is `github.com/cilium/cilium/pkg/policy/api` at the pinned Cilium version (v1.20.1).** Every + field of the CiliumNetworkPolicy spec exists (the 0.6.x hand-written structs modelled 20 of the schema's 291 + paths); rendering keeps cf2cnp's field order through a precedence table (the kustomize/kyaml pattern); the CRD of + the same version is embedded and CI compares it byte for byte with the module's; `cf2cnp version` prints the spec + version the build supports. Output for the 14 recorded captures is byte-identical to 0.6.3 except two deliberate + changes named in `internal/testdata/golden/README.md`. +- **Validation:** Cilium's own `Sanitize()` on every generated policy (on a copy — the file stays as a user writes + it), plus the object's checks: ObjectMeta (DNS-1123 names, label keys), the CRD's protocol enum, `apiVersion` and + `kind`, no `nodeSelector` on a namespaced policy, no namespace on a clusterwide one, `specs` rule by rule. A + refusal is a 400 on the API and exit 1 on the CLI. New: `cf2cnp validate `. +- **`fromCIDR` for external sources:** an ingress flow from `reserved:world` (or `world-ipv4` / `world-ipv6` on a + dual-stack cluster) with an address becomes `fromCIDR: [/32]` (`/128` IPv6), what the receiver saw, instead + of `fromEntities: [world]`. +- **The DNS resolver rule from the flows:** the rule `toFQDNs` and `--dns-visibility` need names the resolver the + workload actually asked — kube-dns by `k8s-app`, the CoreDNS Helm chart by `app.kubernetes.io/name`, NodeLocal + DNSCache, OpenShift's `openshift-dns` on 5353 without a label (Cilium's DNS guide) — on protocol `ANY`, as Cilium's + examples write it (a truncated UDP answer retries over TCP). Profiles `--dns-profile auto|kubernetes|openshift`, + any resolver with `--dns-resolver [/